feat(cbuild): 重构构建系统并迁移至 tools/cbuild
将 `cbuild.py` 迁移至 `tools/cbuild/` 并进行大量功能增强。引入依赖解析器、支持颜色日志输出、 改进包配置默认值处理、完善构建目标识别与拓扑排序依赖管理。同时添加 `.gitignore` 和 `pyproject.toml` 以支持标准 Python 包结构,并更新 README 文档。 新增命令支持:tree(显示依赖树)、clean(带文件统计)、test(运行测试)等, 优化了 Windows 平台下的可执行文件扩展名处理逻辑。 移除了旧的 `wc.py` 行数统计脚本。
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
[package]
|
||||
name = "smcc_lex"
|
||||
version = "0.1.0"
|
||||
|
||||
dependencies = [
|
||||
{ name = "libcore", path = "../../runtime/libcore" },
|
||||
]
|
||||
dependencies = [{ name = "libcore", path = "../../runtime/libcore" }]
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
[package]
|
||||
name = "libcore"
|
||||
version = "0.1.0"
|
||||
|
||||
default_features = [
|
||||
"std_impl",
|
||||
]
|
||||
features = [
|
||||
"std_impl",
|
||||
]
|
||||
default_features = ["std_impl"]
|
||||
features = ["std_impl"]
|
||||
|
||||
dependencies = [
|
||||
# TODO define some to disable stdio for self-contained build
|
||||
{ name = "log", path = "../log" }
|
||||
{ name = "log", path = "../log" },
|
||||
]
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "core_mem.h"
|
||||
#include "core_str.h"
|
||||
|
||||
struct core_stream;
|
||||
typedef struct core_stream core_stream_t;
|
||||
|
||||
#define core_stream_eof (-1)
|
||||
|
||||
10
tools/cbuild/.gitignore
vendored
Normal file
10
tools/cbuild/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
9
tools/cbuild/README.md
Normal file
9
tools/cbuild/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
# cbuild
|
||||
|
||||
## dependencies
|
||||
[uv](https://docs.astral.sh/uv/)
|
||||
一个用 Rust 编写的极速 Python 包和项目管理工具。
|
||||
|
||||
## install
|
||||
使用 `uv pip install -e .` 将 `cbuild` 添加到 python 环境
|
||||
@@ -8,10 +8,49 @@ from dataclasses import dataclass, field
|
||||
from enum import Enum, auto
|
||||
import logging
|
||||
import hashlib
|
||||
|
||||
from graphlib import TopologicalSorter
|
||||
import shutil
|
||||
import argparse
|
||||
import sys
|
||||
from typing import Self
|
||||
import time
|
||||
import sys
|
||||
|
||||
class ColorFormatter(logging.Formatter):
|
||||
"""颜色格式化"""
|
||||
# 基础颜色定义(对应ANSI颜色码)
|
||||
cyan = "\x1b[36m" # DEBUG - 青色
|
||||
green = "\x1b[32m" # INFO - 绿色
|
||||
blue = "\x1b[34m" # TRACE - 蓝色
|
||||
yellow = "\x1b[33m" # WARN - 黄色
|
||||
red = "\x1b[31m" # ERROR - 红色
|
||||
line_red = "\x1b[31;4m" # FATAL - 红色加下划线
|
||||
bold = "\x1b[1m" # 粗体
|
||||
reset = "\x1b[0m" # 重置
|
||||
|
||||
fmt = "%(name)s - %(levelname)s: "
|
||||
time = "%(asctime)s"
|
||||
msg = "%(message)s"
|
||||
file = " - (%(filename)s:%(lineno)d)"
|
||||
|
||||
FORMATS = {
|
||||
logging.DEBUG: bold + cyan + fmt + reset + msg + file,
|
||||
logging.INFO: bold + green + fmt + reset + msg,
|
||||
logging.WARNING: bold + yellow + fmt + reset + msg,
|
||||
logging.ERROR: bold + red + fmt + reset + msg,
|
||||
logging.CRITICAL: bold + line_red + fmt + reset + msg + file,
|
||||
logging.FATAL: bold + line_red + fmt + reset + msg + file,
|
||||
}
|
||||
|
||||
def format(self, record):
|
||||
log_fmt = self.FORMATS.get(record.levelno)
|
||||
formatter = logging.Formatter(log_fmt)
|
||||
return formatter.format(record)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
console_handler = logging.StreamHandler()
|
||||
color_formatter = ColorFormatter()
|
||||
console_handler.setFormatter(color_formatter)
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
@dataclass
|
||||
class Dependency:
|
||||
@@ -51,7 +90,7 @@ class PackageConfig:
|
||||
Returns:
|
||||
str: 包的名称,如果未定义则返回空字符串
|
||||
"""
|
||||
return self.config.get("name", "")
|
||||
return self.config.get("name", "[unnamed package]")
|
||||
|
||||
@property
|
||||
def version(self) -> str:
|
||||
@@ -60,7 +99,7 @@ class PackageConfig:
|
||||
Returns:
|
||||
str: 包的版本号,如果未定义则返回空字符串
|
||||
"""
|
||||
return self.config.get("version", "")
|
||||
return self.config.get("version", "0.0.0")
|
||||
|
||||
@property
|
||||
def default_features(self) -> list[str]:
|
||||
@@ -133,6 +172,68 @@ class PackageConfig:
|
||||
"""
|
||||
return self.config.get("description", "")
|
||||
|
||||
class DependencyResolver:
|
||||
"""依赖解析器"""
|
||||
def __init__(self, root_package: PackageConfig):
|
||||
self.root_package = root_package
|
||||
self.resolved_deps: dict[str, PackageConfig] = {}
|
||||
self.deps_graph: TopologicalSorter = TopologicalSorter()
|
||||
self._resolved = False
|
||||
|
||||
def resolve(self) -> dict[str, PackageConfig]:
|
||||
"""解析所有依赖"""
|
||||
if self._resolved:
|
||||
return self.resolved_deps
|
||||
|
||||
# 使用广度优先搜索解析所有依赖
|
||||
queue = [self.root_package]
|
||||
visited = set()
|
||||
|
||||
while queue:
|
||||
pkg_config = queue.pop(0)
|
||||
pkg_name = pkg_config.name
|
||||
|
||||
if pkg_name in visited:
|
||||
continue
|
||||
visited.add(pkg_name)
|
||||
|
||||
# 创建构建上下文
|
||||
if pkg_name not in self.resolved_deps:
|
||||
self.resolved_deps[pkg_name] = pkg_config
|
||||
|
||||
# 解析直接依赖
|
||||
for dep in pkg_config.dependencies:
|
||||
dep_path = Path(pkg_config.path / dep.path)
|
||||
dep_config = PackageConfig(dep_path)
|
||||
dep_name = dep_config.name
|
||||
|
||||
# 添加依赖图关系
|
||||
self.deps_graph.add(pkg_name, dep_name)
|
||||
|
||||
# 如果是新依赖,加入队列继续解析
|
||||
if dep_name not in visited and dep_name not in self.resolved_deps:
|
||||
queue.append(dep_config)
|
||||
|
||||
self.deps_graph.prepare()
|
||||
self._resolved = True
|
||||
return self.resolved_deps
|
||||
|
||||
def get_sorted_dependencies(self) -> list[CBuildContext]:
|
||||
"""获取按拓扑排序的依赖列表(不包括根包)"""
|
||||
if not self._resolved:
|
||||
self.resolve()
|
||||
|
||||
sorted_names = list(self.deps_graph.static_order())
|
||||
return [CBuildContext(self.resolved_deps[name]) for name in sorted_names
|
||||
if name != self.root_package.name and name in self.resolved_deps]
|
||||
|
||||
def get_all_contexts(self) -> list[CBuildContext]:
|
||||
"""获取所有上下文(包括根包)"""
|
||||
if not self._resolved:
|
||||
self.resolve()
|
||||
|
||||
return [CBuildContext(resolved_dep) for resolved_dep in self.resolved_deps.values()]
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BuildPath:
|
||||
"""path"""
|
||||
@@ -141,6 +242,7 @@ class BuildPath:
|
||||
output_path: Path
|
||||
object_path: Path
|
||||
src_path: Path
|
||||
inc_path: Path
|
||||
default_bin_path: Path
|
||||
default_lib_path: Path
|
||||
|
||||
@@ -159,6 +261,7 @@ class BuildPath:
|
||||
return cls(
|
||||
root_path = root_path,
|
||||
tests_path = root_path / "tests",
|
||||
inc_path = root_path / "include",
|
||||
output_path = output_path ,
|
||||
object_path = output_path / "obj",
|
||||
src_path = src_path,
|
||||
@@ -190,11 +293,12 @@ class CBuildContext:
|
||||
self.relative_path = Path.cwd()
|
||||
self.path = BuildPath.from_root_path(package.path) \
|
||||
if build_path is None else build_path
|
||||
self.deps_resolver = DependencyResolver(package)
|
||||
|
||||
def _path_collection(self, path: list[Path]) -> list[Path]:
|
||||
p = [str(p.resolve()) for p in path if p.exists()]
|
||||
unique_paths = set(p)
|
||||
return [Path(p).relative_to(self.relative_path) for p in unique_paths]
|
||||
return [Path(p).relative_to(self.relative_path, walk_up=True) for p in unique_paths]
|
||||
|
||||
@property
|
||||
def sources_path(self) -> list[Path]:
|
||||
@@ -214,18 +318,16 @@ class CBuildContext:
|
||||
@property
|
||||
def includes(self) -> list[Path]:
|
||||
"""获取包的包含路径"""
|
||||
includes = [self.path.root_path / "include"]
|
||||
# check folders available
|
||||
deps = self.get_dependencies()
|
||||
for dep in deps:
|
||||
includes.extend(dep.includes)
|
||||
deps = self.deps_resolver.get_all_contexts()
|
||||
includes = [inc.path.inc_path for inc in deps if inc.path.inc_path.exists()]
|
||||
return self._path_collection(includes)
|
||||
|
||||
def get_targets(self) -> list[Target]:
|
||||
"""获取所有构建目标"""
|
||||
targets = []
|
||||
# TODO
|
||||
ext = ".exe"
|
||||
execute_ext = ""
|
||||
if sys.platform == "win32":
|
||||
execute_ext = ".exe"
|
||||
|
||||
# 添加主可执行文件目标
|
||||
if self.path.default_bin_path.exists():
|
||||
@@ -234,7 +336,7 @@ class CBuildContext:
|
||||
type=TargetType.MAIN_EXECUTABLE,
|
||||
source=self.path.default_bin_path,
|
||||
object=self.get_object_path(self.path.default_bin_path),
|
||||
output=self.path.output_path / f"{self.package.name}{ext}",
|
||||
output=self.path.output_path / f"{self.package.name}{execute_ext}",
|
||||
))
|
||||
|
||||
# 添加静态库目标
|
||||
@@ -255,7 +357,7 @@ class CBuildContext:
|
||||
type=TargetType.TEST_EXECUTABLE,
|
||||
source=test_source,
|
||||
object=self.get_object_path(test_source),
|
||||
output=self.path.output_path / f"{test_source.stem}{ext}"
|
||||
output=self.path.output_path / f"{test_source.stem}{execute_ext}"
|
||||
))
|
||||
|
||||
return targets
|
||||
@@ -273,9 +375,7 @@ class CBuildContext:
|
||||
|
||||
def get_build_components(self) -> list[tuple[Path, Path]]:
|
||||
"""获取所有需要编译的源文件及目标文件(排除主文件即main.c, lib.c, bin/*.c)"""
|
||||
# logging.debug("[+] build_components: %s", objs)
|
||||
deps = [self]
|
||||
deps.extend(self.get_dependencies())
|
||||
deps = self.deps_resolver.get_all_contexts()
|
||||
objs = []
|
||||
for dep in deps:
|
||||
objs.extend(dep.get_self_build_objs())
|
||||
@@ -312,19 +412,6 @@ class CBuildContext:
|
||||
object_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
return object_path
|
||||
|
||||
def get_dependencies(self) -> list[Self]:
|
||||
"""_summary_
|
||||
|
||||
Returns:
|
||||
list[CBuildContext]: _description_
|
||||
"""
|
||||
deps = []
|
||||
for dep in self.package.dependencies:
|
||||
ctx = CBuildContext(PackageConfig(Path(self.package.path / dep.path)))
|
||||
deps.append(ctx)
|
||||
deps.extend(ctx.get_dependencies())
|
||||
return deps
|
||||
|
||||
@dataclass
|
||||
class CacheEntry:
|
||||
"""缓存条目"""
|
||||
@@ -355,12 +442,12 @@ class Compiler(ABC):
|
||||
def cmd(self, cmd: str | list[str]):
|
||||
"""执行命令并处理错误输出"""
|
||||
try:
|
||||
logging.debug("command: `%s`", ' '.join(cmd) if isinstance(cmd, list) else cmd)
|
||||
logger.debug("command: `%s`", ' '.join(cmd) if isinstance(cmd, list) else cmd)
|
||||
subprocess.run(cmd, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
# 输出详细错误信息帮助调试
|
||||
cmd_str = ' '.join(e.cmd) if isinstance(e.cmd, list) else e.cmd
|
||||
logging.error("command running error: [%d]`%s`", e.returncode, cmd_str)
|
||||
logger.error("command running error: [%d]`%s`", e.returncode, cmd_str)
|
||||
raise
|
||||
|
||||
class ClangCompiler(Compiler):
|
||||
@@ -403,9 +490,6 @@ class GccCompiler(Compiler):
|
||||
|
||||
class CPackageBuilder:
|
||||
"""包构建器"""
|
||||
# TODO
|
||||
EXT = ".exe"
|
||||
|
||||
def __init__(self, package_path: Path, compiler: Compiler):
|
||||
self.package = PackageConfig(package_path)
|
||||
self.context = CBuildContext(self.package, None)
|
||||
@@ -417,17 +501,35 @@ class CPackageBuilder:
|
||||
# 确保输出目录存在
|
||||
self.context.path.output_path.mkdir(parents=True, exist_ok=True)
|
||||
self.context.path.object_path.mkdir(parents=True, exist_ok=True)
|
||||
# TODO use cache and add dependency include and flags
|
||||
deps = self.context.get_dependencies()
|
||||
path_map = self.context.get_build_components()
|
||||
self.compiler.compile_all(path_map, self.context.includes, self.global_flags)
|
||||
return [pair[1] for pair in path_map]
|
||||
|
||||
def build(self):
|
||||
def _format_size(self, size_bytes):
|
||||
"""格式化文件大小显示"""
|
||||
if size_bytes == 0:
|
||||
return "0B"
|
||||
|
||||
size_names = ["B", "KB", "MB", "GB"]
|
||||
i = 0
|
||||
while size_bytes >= 1024 and i < len(size_names) - 1:
|
||||
size_bytes /= 1024.0
|
||||
i += 1
|
||||
|
||||
if i == 0:
|
||||
return f"{int(size_bytes)}{size_names[i]}"
|
||||
return f"{size_bytes:.1f}{size_names[i]}"
|
||||
|
||||
def build(self, targets_type: list[TargetType]):
|
||||
"""构建包"""
|
||||
start_time = time.time()
|
||||
|
||||
object_files = self._build_ctx()
|
||||
targets = self.context.get_targets()
|
||||
for target in targets:
|
||||
if target.type not in targets_type:
|
||||
continue
|
||||
|
||||
match target.type:
|
||||
case TargetType.MAIN_EXECUTABLE:
|
||||
self.compiler.compile(target.source, target.object, self.context.includes,
|
||||
@@ -441,25 +543,61 @@ class CPackageBuilder:
|
||||
object_files.append(target.object)
|
||||
self.compiler.link(object_files, [], self.global_flags, target.output)
|
||||
object_files.remove(target.object)
|
||||
logging.info("Building is Ok...")
|
||||
# 计算构建时间
|
||||
elapsed_time = time.time() - start_time
|
||||
time_str = f"{elapsed_time:.2f}s" if elapsed_time < 60 else f"{elapsed_time/60:.1f}m"
|
||||
# 显示完成信息
|
||||
logger.info("\tFinished dev [unoptimized + debuginfo] target(s) in %s",
|
||||
time_str)
|
||||
|
||||
def run(self):
|
||||
"""运行项目"""
|
||||
targets = [target for target in self.context.get_targets() \
|
||||
if target.type == TargetType.MAIN_EXECUTABLE]
|
||||
if len(targets) != 1:
|
||||
logging.error("not have target to run")
|
||||
logger.error("not have target to run")
|
||||
subprocess.run(targets[0].output, check=False)
|
||||
|
||||
def clean(self):
|
||||
"""清理构建产物"""
|
||||
# 统计要删除的文件数量和总大小
|
||||
cleaned_files = 0
|
||||
total_size = 0
|
||||
# 删除整个输出目录,这会清除所有构建产物
|
||||
if self.context.path.output_path.exists():
|
||||
import shutil
|
||||
# 遍历目录统计文件数量和大小
|
||||
for file_path in self.context.path.output_path.rglob("*"):
|
||||
if file_path.is_file():
|
||||
cleaned_files += 1
|
||||
total_size += file_path.stat().st_size
|
||||
shutil.rmtree(self.context.path.output_path)
|
||||
print(f"已清理构建目录: {self.context.path.output_path}")
|
||||
logger.info("已清理构建目录: %s",
|
||||
self.context.path.output_path)
|
||||
logger.info("Cleaned %s files, Size %s",
|
||||
cleaned_files, self._format_size(total_size))
|
||||
else:
|
||||
print("没有找到构建目录,无需清理")
|
||||
logger.info("没有找到构建目录,无需清理")
|
||||
|
||||
def tree(self):
|
||||
"""打印构建树 - 仿照 cargo 的风格"""
|
||||
# TODO 递归显示
|
||||
print("dependency tree:")
|
||||
|
||||
# 解析依赖
|
||||
resolver = self.context.deps_resolver
|
||||
resolver.resolve()
|
||||
|
||||
# 获取根包
|
||||
root_pkg = resolver.root_package
|
||||
print(f"{root_pkg.name} v{root_pkg.version}")
|
||||
|
||||
# 获取依赖图的节点和边
|
||||
# 这里我们简化处理,只显示直接依赖
|
||||
for pkg_config in resolver.resolved_deps.values():
|
||||
if pkg_config.name != root_pkg.name: # 跳过根包
|
||||
indent = "├── " if pkg_config.name != list(resolver.resolved_deps.keys())[-1]\
|
||||
else "└── "
|
||||
print(f"{indent}{pkg_config.name} v{pkg_config.version}")
|
||||
|
||||
def tests(self):
|
||||
"""运行测试"""
|
||||
@@ -469,8 +607,8 @@ class CPackageBuilder:
|
||||
failed = 0
|
||||
for target in targets:
|
||||
name = target.name
|
||||
print(f"运行测试: {name}")
|
||||
logging.debug("test run %s", target.output)
|
||||
logger.info("运行测试: %s", name)
|
||||
logger.debug("test run %s", target.output)
|
||||
try:
|
||||
result = subprocess.run(target.output,
|
||||
check=True,
|
||||
@@ -495,47 +633,67 @@ class CPackageBuilder:
|
||||
failed += 1
|
||||
print(f"\n测试结果: {passed} 通过, {failed} 失败")
|
||||
|
||||
def cmd(self, cmd: str):
|
||||
"""执行命令"""
|
||||
match cmd:
|
||||
case "build":
|
||||
self.build([
|
||||
TargetType.MAIN_EXECUTABLE,
|
||||
TargetType.TEST_EXECUTABLE,
|
||||
TargetType.STATIC_LIBRARY,
|
||||
TargetType.SHARED_LIBRARY,
|
||||
TargetType.EXECUTABLE,
|
||||
])
|
||||
case "run":
|
||||
bin_path = self.context.path.default_bin_path
|
||||
if not bin_path.exists():
|
||||
logger.error("%s not exist", bin_path)
|
||||
return
|
||||
self.build([TargetType.MAIN_EXECUTABLE])
|
||||
self.run()
|
||||
case "test":
|
||||
self.build([TargetType.TEST_EXECUTABLE])
|
||||
self.tests()
|
||||
case "clean":
|
||||
self.clean()
|
||||
case "tree":
|
||||
self.tree()
|
||||
case _:
|
||||
logger.error("unknown command: %s", cmd)
|
||||
|
||||
def main():
|
||||
"""main"""
|
||||
parser = argparse.ArgumentParser(description="Simple C Package Manager")
|
||||
parser.add_argument("command", choices=["build", "run", "test", "clean"],
|
||||
parser = argparse.ArgumentParser(description="Simple C Package Manager",
|
||||
prog="cbuild")
|
||||
parser.add_argument("command", choices=["build", "run", "test", "clean", "tree"],
|
||||
help="Command to execute")
|
||||
parser.add_argument("--verbose", "-v", action="store_true",
|
||||
help="enable the logging to debug (defalut: false)")
|
||||
parser.add_argument("--compiler", "-c", choices=["gcc", "clang", "smcc"], default="gcc",
|
||||
help="Compiler to use (default: gcc)")
|
||||
parser.add_argument("--verbose", "-V", action="store_true",
|
||||
help="enable the logging to debug (default: false)")
|
||||
parser.add_argument("--path", "-p", default=".",
|
||||
help="Path to the package (default: current directory)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
package_path = Path(args.path)
|
||||
package_path = Path.cwd() / Path(args.path)
|
||||
if args.verbose:
|
||||
logging.getLogger().setLevel(logging.NOTSET)
|
||||
compiler = GccCompiler()
|
||||
logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
try:
|
||||
if args.command == "build":
|
||||
builder = CPackageBuilder(package_path, compiler)
|
||||
builder.build()
|
||||
print("build is Ok...")
|
||||
elif args.command == "run":
|
||||
builder = CPackageBuilder(package_path, compiler)
|
||||
bin_path = builder.context.path.default_bin_path
|
||||
if not bin_path.exists():
|
||||
print(f"{bin_path} not exist")
|
||||
return
|
||||
builder.build()
|
||||
builder.run()
|
||||
elif args.command == "test":
|
||||
builder = CPackageBuilder(package_path, compiler)
|
||||
builder.build()
|
||||
builder.tests()
|
||||
elif args.command == "clean":
|
||||
builder = CPackageBuilder(package_path, compiler)
|
||||
builder.clean()
|
||||
print("clean is Ok...")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
match args.compiler:
|
||||
case "gcc":
|
||||
compiler = GccCompiler()
|
||||
case "clang":
|
||||
compiler = ClangCompiler()
|
||||
case "smcc":
|
||||
# TODO self compiler
|
||||
raise ValueError("Invalid compiler")
|
||||
case _:
|
||||
raise ValueError("Invalid compiler")
|
||||
builder = CPackageBuilder(package_path, compiler)
|
||||
builder.cmd(args.command)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# builder = CPackageBuilder(Path("./runtime/libcore/"), ClangCompiler())
|
||||
10
tools/cbuild/pyproject.toml
Normal file
10
tools/cbuild/pyproject.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[project]
|
||||
name = "cbuild"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = []
|
||||
|
||||
[project.scripts]
|
||||
cbuild = "cbuild:main"
|
||||
48
wc.py
48
wc.py
@@ -1,48 +0,0 @@
|
||||
"""统计目录下C/C++文件的行数(write by AI)"""
|
||||
import os
|
||||
|
||||
def count_lines(file_path):
|
||||
"""统计单个文件的代码行数"""
|
||||
try:
|
||||
with open(file_path, 'rb') as f: # 二进制模式读取避免编码问题
|
||||
return sum(1 for _ in f)
|
||||
except UnicodeDecodeError:
|
||||
print(f"警告:无法解码文件 {file_path}(可能不是文本文件)")
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"读取 {file_path} 出错: {str(e)}")
|
||||
return 0
|
||||
|
||||
def scan_files(directory, exclude_dirs=None):
|
||||
"""扫描目录获取所有C/C++文件"""
|
||||
if exclude_dirs is None:
|
||||
exclude_dirs = ['.git', 'venv', '__pycache__'] # 默认排除的目录
|
||||
|
||||
c_files = []
|
||||
for root, dirs, files in os.walk(directory):
|
||||
# 跳过排除目录
|
||||
dirs[:] = [d for d in dirs if d not in exclude_dirs]
|
||||
|
||||
for file in files:
|
||||
if file.endswith(('.c', '.h')):
|
||||
full_path = os.path.join(root, file)
|
||||
c_files.append(full_path)
|
||||
return c_files
|
||||
|
||||
def main():
|
||||
"""main function"""
|
||||
target_dir = input("请输入要扫描的目录路径(留空为当前目录): ") or '.'
|
||||
|
||||
files = scan_files(target_dir)
|
||||
total_lines = 0
|
||||
|
||||
print("\n统计结果:")
|
||||
for idx, file in enumerate(files, 1):
|
||||
lines = count_lines(file)
|
||||
total_lines += lines
|
||||
print(f"{idx:4d}. {file} ({lines} 行)")
|
||||
|
||||
print(f"\n总计: {len(files)} 个C/C++文件,共 {total_lines} 行代码")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user