From 63f6f138835d4c5e4f3cf51b24ddb7d1a8d4ab5d Mon Sep 17 00:00:00 2001 From: zzy <2450266535@qq.com> Date: Sat, 22 Nov 2025 15:08:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(cbuild):=20=E9=87=8D=E6=9E=84=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E7=B3=BB=E7=BB=9F=E5=B9=B6=E8=BF=81=E7=A7=BB=E8=87=B3?= =?UTF-8?q?=20tools/cbuild?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 `cbuild.py` 迁移至 `tools/cbuild/` 并进行大量功能增强。引入依赖解析器、支持颜色日志输出、 改进包配置默认值处理、完善构建目标识别与拓扑排序依赖管理。同时添加 `.gitignore` 和 `pyproject.toml` 以支持标准 Python 包结构,并更新 README 文档。 新增命令支持:tree(显示依赖树)、clean(带文件统计)、test(运行测试)等, 优化了 Windows 平台下的可执行文件扩展名处理逻辑。 移除了旧的 `wc.py` 行数统计脚本。 --- libs/lexer/cbuild.toml | 5 +- runtime/libcore/cbuild.toml | 11 +- runtime/libcore/include/core_stream.h | 1 + tools/cbuild/.gitignore | 10 + tools/cbuild/README.md | 9 + cbuild.py => tools/cbuild/cbuild.py | 310 +++++++++++++++++++------- tools/cbuild/pyproject.toml | 10 + wc.py | 48 ---- 8 files changed, 270 insertions(+), 134 deletions(-) create mode 100644 tools/cbuild/.gitignore create mode 100644 tools/cbuild/README.md rename cbuild.py => tools/cbuild/cbuild.py (64%) create mode 100644 tools/cbuild/pyproject.toml delete mode 100644 wc.py diff --git a/libs/lexer/cbuild.toml b/libs/lexer/cbuild.toml index 04391f6..6bc10cc 100644 --- a/libs/lexer/cbuild.toml +++ b/libs/lexer/cbuild.toml @@ -1,6 +1,5 @@ [package] name = "smcc_lex" +version = "0.1.0" -dependencies = [ - { name = "libcore", path = "../../runtime/libcore" }, -] +dependencies = [{ name = "libcore", path = "../../runtime/libcore" }] diff --git a/runtime/libcore/cbuild.toml b/runtime/libcore/cbuild.toml index d4e9a1b..361b1c4 100644 --- a/runtime/libcore/cbuild.toml +++ b/runtime/libcore/cbuild.toml @@ -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" }, ] diff --git a/runtime/libcore/include/core_stream.h b/runtime/libcore/include/core_stream.h index edeaa0a..7806c51 100644 --- a/runtime/libcore/include/core_stream.h +++ b/runtime/libcore/include/core_stream.h @@ -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) diff --git a/tools/cbuild/.gitignore b/tools/cbuild/.gitignore new file mode 100644 index 0000000..88efd90 --- /dev/null +++ b/tools/cbuild/.gitignore @@ -0,0 +1,10 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv \ No newline at end of file diff --git a/tools/cbuild/README.md b/tools/cbuild/README.md new file mode 100644 index 0000000..078d5d1 --- /dev/null +++ b/tools/cbuild/README.md @@ -0,0 +1,9 @@ + +# cbuild + +## dependencies +[uv](https://docs.astral.sh/uv/) +一个用 Rust 编写的极速 Python 包和项目管理工具。 + +## install +使用 `uv pip install -e .` 将 `cbuild` 添加到 python 环境 diff --git a/cbuild.py b/tools/cbuild/cbuild.py similarity index 64% rename from cbuild.py rename to tools/cbuild/cbuild.py index 6925ebc..15eb6ab 100644 --- a/cbuild.py +++ b/tools/cbuild/cbuild.py @@ -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()) diff --git a/tools/cbuild/pyproject.toml b/tools/cbuild/pyproject.toml new file mode 100644 index 0000000..504a6c3 --- /dev/null +++ b/tools/cbuild/pyproject.toml @@ -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" diff --git a/wc.py b/wc.py deleted file mode 100644 index 27a6805..0000000 --- a/wc.py +++ /dev/null @@ -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()