diff --git a/.gitignore b/.gitignore index cf61256..43bea61 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ note.md # cbuilder build + +# external +external/ diff --git a/tools/cbuild/cbuild.py b/tools/cbuild/cbuild.py index 9955766..986ac09 100644 --- a/tools/cbuild/cbuild.py +++ b/tools/cbuild/cbuild.py @@ -1,63 +1,50 @@ -"""cbuild.py""" +"""cbuild.py - 优化的轻量C构建系统""" from abc import ABC, abstractmethod import tomllib -import pprint import subprocess from pathlib import Path -from dataclasses import asdict, dataclass, field +from dataclasses import asdict, dataclass from enum import Enum, auto import logging import hashlib from graphlib import TopologicalSorter import shutil import argparse -from typing import Self import time import sys import json +import re 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, + COLORS = { + logging.DEBUG: "\x1b[36m", # 青色 + logging.INFO: "\x1b[32m", # 绿色 + logging.WARNING: "\x1b[33m", # 黄色 + logging.ERROR: "\x1b[31m", # 红色 + logging.CRITICAL: "\x1b[31;4m", # 红色加下划线 } def format(self, record): - log_fmt = self.FORMATS.get(record.levelno) - formatter = logging.Formatter(log_fmt) + color = self.COLORS.get(record.levelno, "\x1b[0m") + reset = "\x1b[0m" + fmt = f"{color}%(name)s - %(levelname)s:{reset} %(message)s" + if record.levelno in (logging.DEBUG, logging.CRITICAL): + fmt += " - (%(filename)s:%(lineno)d)" + formatter = logging.Formatter(fmt) return formatter.format(record) def setup_logger() -> logging.Logger: """设置日志记录器""" - _logger = logging.getLogger(__name__) - console_handler = logging.StreamHandler() - console_handler.setFormatter(ColorFormatter()) - _logger.addHandler(console_handler) - return _logger + inner_logger = logging.getLogger(__name__) + handler = logging.StreamHandler() + handler.setFormatter(ColorFormatter()) + inner_logger.addHandler(handler) + return inner_logger logger = setup_logger() @@ -79,282 +66,193 @@ class Feature: name: str description: str = "" - dependencies: list[str] = field(default_factory=list) + dependencies: list[str] | None = None + + def __post_init__(self): + if self.dependencies is None: + self.dependencies = [] class PackageConfig: - """包配置类, 用于解析和管理cbuild.toml配置""" + """包配置类""" CONFIG_FILE = "cbuild.toml" def __init__(self, config_path: Path): package_file = config_path / self.CONFIG_FILE if not package_file.exists(): - raise ValueError(f"Package path {package_file} does not exist") - with open(package_file, "rb") as file: - raw_config = tomllib.load(file) - self.config: dict = raw_config.get("package", {}) - self.path: Path = config_path + raise ValueError(f"配置文件 {package_file} 不存在") - def __str__(self) -> str: - return pprint.pformat(self.config) + with open(package_file, "rb") as f: + self.config = tomllib.load(f).get("package", {}) + self.path = config_path + + def __getitem__(self, key): + return self.config.get(key) @property def name(self) -> str: - """获取包的名称 - - Returns: - str: 包的名称,如果未定义则返回空字符串 - """ + """包的名称""" return self.config.get("name", "[unnamed package]") @property def version(self) -> str: - """获取包的版本 - - Returns: - str: 包的版本号,如果未定义则返回空字符串 - """ + """包的版本号 eg. 0.1.0""" return self.config.get("version", "0.0.0") - @property - def default_features(self) -> list[str]: - """获取默认启用的特性列表 - - Returns: - list[str]: 默认特性的名称列表 - """ - return self.config.get("default_features", []) - - @property - def features(self) -> list[Feature]: - """获取所有可用特性及其依赖 - - Returns: - list[Feature]: 特性对象列表 - """ - features_data = self.config.get("features", {}) - features = [] - for feature in features_data: - if isinstance(feature, str): - feature = Feature(name=feature) - elif isinstance(feature, dict): - name = feature.get("name", None) - if name is None: - continue - feature = Feature( - name=name, - description=feature.get("description", ""), - dependencies=feature.get("dependencies", []), - ) - features.append(feature) - return features - @property def dependencies(self) -> list[Dependency]: - """获取包的依赖列表 - - Returns: - list[Dependency]: 依赖对象列表 - """ - deps_data = self.config.get("dependencies", []) - dependencies = [] - for dep_dict in deps_data: - if isinstance(dep_dict, dict): - dependency = Dependency( - name=dep_dict.get("name", ""), - path=dep_dict.get("path", ""), - version=dep_dict.get("version", "0.0.0"), - optional=dep_dict.get("optional", False), + """获取当前的直接依赖""" + deps = [] + for dep in self.config.get("dependencies", []): + if isinstance(dep, dict): + deps.append( + Dependency( + name=dep.get("name", ""), + path=dep.get("path", ""), + version=dep.get("version", "0.0.0"), + optional=dep.get("optional", False), + ) ) - dependencies.append(dependency) - return dependencies - - @property - def authors(self) -> list[str]: - """获取作者列表 - - Returns: - list[str]: 作者列表 - """ - return self.config.get("authors", []) - - @property - def description(self) -> str: - """获取包的描述 - - Returns: - str: 包的描述文本 - """ - return self.config.get("description", "") + return deps class DependencyResolver: """依赖解析器""" def __init__(self, root_package: PackageConfig): - self.root_package = root_package + self.root = root_package self.deps: dict[str, PackageConfig] = {} - self.deps_graph: TopologicalSorter = TopologicalSorter() + self.graph = TopologicalSorter() self.dep_map: dict[str, list[str]] = {} self._resolved = False - def resolve(self) -> None: + def resolve(self): """解析所有依赖""" if self._resolved: return - # 使用广度优先搜索解析所有依赖 - queue = [self.root_package] + queue = [self.root] visited = set() while queue: - pkg_config = queue.pop(0) - pkg_name = pkg_config.name + pkg = queue.pop(0) + name = pkg.name - if pkg_name in visited: + if name in visited: continue - visited.add(pkg_name) + visited.add(name) - # 创建构建上下文 - if pkg_name not in self.deps: - self.deps[pkg_name] = pkg_config + self.deps[name] = pkg + self.dep_map.setdefault(name, []) - if pkg_name not in self.dep_map: - self.dep_map[pkg_name] = [] + for dep in pkg.dependencies: + dep_path = Path(pkg.path / dep.path) + dep_pkg = PackageConfig(dep_path) + dep_name = dep_pkg.name - # 解析直接依赖 - 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.graph.add(name, dep_name) + self.dep_map[name].append(dep_name) - # 添加依赖图关系 - self.deps_graph.add(pkg_name, dep_name) - self.dep_map[pkg_name].append(dep_name) - - # 如果是新依赖,加入队列继续解析 if dep_name not in visited and dep_name not in self.deps: - queue.append(dep_config) + queue.append(dep_pkg) - self.deps_graph.prepare() + self.graph.prepare() self._resolved = True - def print_tree(self) -> None: + def print_tree(self): """打印依赖树""" self.resolve() print("dependency tree:") - root_pkg = self.root_package - - def print_tree(pkg_name, prefix="", is_last=True): - """递归打印依赖树""" - - # 获取包配置 - pkg_config = self.deps[pkg_name] - - # 打印当前包 - if pkg_name == root_pkg.name: - print(f"{pkg_config.name} v{pkg_config.version}") - else: - connector = "└── " if is_last else "├── " - print(f"{prefix}{connector}{pkg_config.name} v{pkg_config.version}") - - # 计算子节点的前缀 - if pkg_name == root_pkg.name: + def _print(pkg_name, prefix="", is_last=True): + pkg = self.deps[pkg_name] + connector = "└── " if is_last else "├── " + if pkg_name == self.root.name: + print(f"{pkg.name} v{pkg.version}") child_prefix = "" else: + print(f"{prefix}{connector}{pkg.name} v{pkg.version}") child_prefix = prefix + (" " if is_last else "│ ") - # 递归打印依赖 - dependencies = self.get_dependencies_of(pkg_name) - for i, dep_name in enumerate(dependencies): - is_last_child = i == len(dependencies) - 1 - print_tree(dep_name, child_prefix, is_last_child) + deps = self.dep_map.get(pkg_name, []) + for i, dep_name in enumerate(deps): + _print(dep_name, child_prefix, i == len(deps) - 1) - # 从根包开始打印树 - print_tree(root_pkg.name) + _print(self.root.name) - def get_dependencies_of(self, package_name: str) -> list[str]: - """获取指定包的直接依赖列表 - - Args: - package_name (str): 包名称 - - Returns: - list[str]: 直接依赖的包名列表 - """ - return self.dep_map.get(package_name, []) - - def get_sorted_dependencies(self) -> list[PackageConfig]: - """获取按拓扑排序的依赖列表(不包括根包)""" - self.resolve() - - sorted_names = list(self.deps_graph.static_order()) - return [ - self.deps[name] - for name in sorted_names - if name != self.root_package.name and name in self.deps - ] - - def get_all_contexts(self) -> list[PackageConfig]: - """获取所有上下文(包括根包)""" + def get_all_packages(self) -> list[PackageConfig]: + """获取所有包""" if not self._resolved: self.resolve() + return list(self.deps.values()) - return [resolved_dep for resolved_dep in self.deps.values()] + +class BuildMode(Enum): + """构建模式""" + + TEST = "test" + DEV = "dev" + DEBUG = "debug" + RELEASE = "release" + NONE = "none" @dataclass(frozen=True) -class BuildPath: - """path""" +class BuildPaths: + """构建路径""" - root_path: Path - tests_path: Path - output_path: Path - object_path: Path - src_path: Path - inc_path: Path - default_bin_path: Path - default_lib_path: Path + root: Path + src: Path + include: Path + tests: Path + output: Path + objects: Path + main_bin: Path + main_lib: Path + extra_bins: Path @classmethod - def from_root_path(cls, root_path: Path) -> Self: - """_summary_ + def create(cls, root: Path, mode: BuildMode = BuildMode.DEV): + """创建构建路径""" + src = root / "src" + output = root / "build" - Args: - output_path (Path): _description_ + # 根据模式确定输出目录 + if mode == BuildMode.RELEASE: + output = output / "release" + elif mode == BuildMode.DEBUG: + output = output / "debug" + elif mode == BuildMode.TEST: + output = output / "test" + else: + output = output / "dev" - Returns: - Self: _description_ - """ - src_path = root_path / "src" - output_path = root_path / "build" 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, - default_bin_path=src_path / "main.c", - default_lib_path=src_path / "lib.c", + root=root, + src=src, + include=root / "include", + tests=root / "tests", + output=output, + objects=output / "obj", + main_bin=src / "main.c", + main_lib=src / "lib.c", + extra_bins=src / "bin", ) class TargetType(Enum): - """目标文件类型枚举""" + """目标类型""" - MAIN_EXECUTABLE = auto() - EXECUTABLE = auto() - TEST_EXECUTABLE = auto() - STATIC_LIBRARY = auto() - SHARED_LIBRARY = auto() + MAIN_EXEC = auto() + EXEC = auto() + TEST_EXEC = auto() + STATIC_LIB = auto() @dataclass class Target: - """目标文件信息""" + """构建目标""" name: str type: TargetType @@ -363,150 +261,126 @@ class Target: output: Path -class CBuildContext: - """构建上下文,管理所有包的信息""" +class BuildContext: + """构建上下文""" - def __init__( - self, - package: PackageConfig, - build_path: BuildPath | None = None, - need_cache: bool = True, - ): + def __init__(self, package: PackageConfig, mode: BuildMode = BuildMode.DEV): self.package = package - 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) + self.paths = BuildPaths.create(package.path, mode) + self.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, walk_up=True) for p in unique_paths - ] + def get_sources(self, pattern: str = "**/*.c") -> list[Path]: + """获取源文件""" + if self.paths.src.exists(): + return list(self.paths.src.glob(pattern)) + return [] - @property - def sources_path(self) -> list[Path]: - """获取所有包的源文件路径""" - return list(self.path.src_path.glob("**/*.c")) + def get_bin_sources(self) -> list[Path]: + """获取额外的可执行文件源""" + if self.paths.extra_bins.exists(): + return list(self.paths.extra_bins.glob("*.c")) + return [] - @property - def tests_path(self) -> list[Path]: - """获取所有测试源文件路径 eg. `tests/test_*.c`""" - test_sources = [] - test_path = self.path.root_path / "tests" - if test_path.exists(): - for file in test_path.glob("test_*.c"): - test_sources.append(file) - return test_sources + def get_test_sources(self) -> list[Path]: + """获取测试源文件""" + if self.paths.tests.exists(): + return list(self.paths.tests.glob("test_*.c")) + return [] - @property - def includes(self) -> list[Path]: - """获取包的包含路径""" - deps = [CBuildContext(i) for i in 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_includes(self) -> list[Path]: + """获取包含路径""" + includes = [] + for pkg in self.resolver.get_all_packages(): + inc_path = pkg.path / "include" + if inc_path.exists(): + includes.append(inc_path) + return includes def get_targets(self) -> list[Target]: """获取所有构建目标""" targets = [] - execute_ext = "" - if sys.platform == "win32": - execute_ext = ".exe" + ext = ".exe" if sys.platform == "win32" else "" - # 添加主可执行文件目标 - if self.path.default_bin_path.exists(): + # 主可执行文件 + if self.paths.main_bin.exists(): targets.append( Target( name=self.package.name, - 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}{execute_ext}", + type=TargetType.MAIN_EXEC, + source=self.paths.main_bin, + object=self.get_object_path(self.paths.main_bin), + output=self.paths.output / f"{self.package.name}{ext}", ) ) - # 添加静态库目标 - if self.path.default_lib_path.exists(): + # 静态库 + if self.paths.main_lib.exists(): targets.append( Target( name=f"lib{self.package.name}", - type=TargetType.STATIC_LIBRARY, - source=self.path.default_lib_path, - object=self.get_object_path(self.path.default_lib_path), - output=self.path.output_path / f"lib{self.package.name}.a", + type=TargetType.STATIC_LIB, + source=self.paths.main_lib, + object=self.get_object_path(self.paths.main_lib), + output=self.paths.output / f"lib{self.package.name}.a", ) ) - # 添加测试目标 - if self.path.tests_path.exists(): - for test_source in self.tests_path: - targets.append( - Target( - name=test_source.stem, - type=TargetType.TEST_EXECUTABLE, - source=test_source, - object=self.get_object_path(test_source), - output=self.path.output_path - / f"{test_source.stem}{execute_ext}", - ) + # 额外可执行文件 + for src in self.get_bin_sources(): + targets.append( + Target( + name=src.stem, + type=TargetType.EXEC, + source=src, + object=self.get_object_path(src), + output=self.paths.output / f"{src.stem}{ext}", ) + ) + + # 测试文件 + for test in self.get_test_sources(): + targets.append( + Target( + name=test.stem, + type=TargetType.TEST_EXEC, + source=test, + object=self.get_object_path(test), + output=self.paths.output / f"{test.stem}{ext}", + ) + ) return targets - def get_self_build_objs(self) -> list[Path]: - """获取当前包的源文件路径""" - objs = [] - for path in self.sources_path: - if path == self.path.default_bin_path: - continue - if path == self.path.default_lib_path: - continue - objs.append(path) - return objs - - def get_object_path(self, source_path: Path) -> Path: - """将源文件路径映射到对象文件路径 - - Args: - source_path (Path): 源文件路径(.c文件) - - Returns: - Path: 对应的对象文件路径(.o文件) - """ - # 确保输出目录存在 - objects_dir = self.path.object_path - objects_dir.mkdir(parents=True, exist_ok=True) + def get_object_path(self, source: Path) -> Path: + """获取对象文件路径""" + self.paths.objects.mkdir(parents=True, exist_ok=True) try: - # 尝试生成相对于根目录的路径结构,避免冲突 - relative_path = source_path.relative_to(self.path.root_path) - # 将路径分隔符替换为特殊字符,防止文件系统问题 - safe_relative_path = str(relative_path).replace("/", "_").replace("\\", "_") - object_path = objects_dir / Path(safe_relative_path).with_suffix(".o") + rel_path = source.relative_to(self.paths.root) + safe_name = str(rel_path).replace("/", "_").replace("\\", "_") + return self.paths.objects / Path(safe_name).with_suffix(".o") except ValueError: - # 如果源文件完全不在项目目录下,则使用完整路径哈希 - full_path_str = str(source_path.absolute()) - path_hash = hashlib.md5(full_path_str.encode()).hexdigest()[:8] - object_filename = f"{source_path.stem}_{path_hash}{source_path.suffix}.o" - object_path = objects_dir / object_filename + # 源文件不在项目目录中 + path_hash = hashlib.md5(str(source.absolute()).encode()).hexdigest()[:8] + return self.paths.objects / f"{source.stem}_{path_hash}.o" - # 确保对象文件的目录存在 - object_path.parent.mkdir(parents=True, exist_ok=True) - return object_path + def get_compile_objects(self) -> list[tuple[Path, Path]]: + """获取需要编译的源文件和目标文件""" + objects = [] - def get_build_objs(self) -> list[tuple[Path, Path]]: - """获取所有需要编译的源文件及目标文件(排除主文件即main.c, lib.c, bin/*.c)""" - deps = [CBuildContext(i) for i in self.deps_resolver.get_all_contexts()] - objs = [] - for dep in deps: - objs.extend(dep.get_self_build_objs()) - self._path_collection(objs) + # 获取所有包的源文件(排除主文件) + for pkg in self.resolver.get_all_packages(): + ctx = BuildContext(pkg) + for src in ctx.get_sources(): + if src in (ctx.paths.main_bin, ctx.paths.main_lib): + continue + if ctx.paths.extra_bins.exists() and src.is_relative_to( + ctx.paths.extra_bins + ): + continue + objects.append((src, self.get_object_path(src))) - return [ - (source_path, self.get_object_path(source_path)) for source_path in objs - ] + return objects @dataclass @@ -519,188 +393,112 @@ class CacheEntry: class BuildCache: - """构建缓存管理器""" + """构建缓存""" def __init__(self, build_path: Path): - self.cache_dir = build_path / Path("cache") + self.cache_dir = build_path / "cache" self.cache_dir.mkdir(parents=True, exist_ok=True) - self.cache_file = self.cache_dir / Path("cache.json") - self.cache: dict[str, CacheEntry] = {} + self.cache_file = self.cache_dir / "cache.json" + self.cache = {} def load(self): - """加载缓存数据""" - if not self.cache_file.exists() or self.cache_file.stat().st_size == 0: - self.cache_file.touch() - self.cache = {} + """加载缓存""" + if not self.cache_file.exists(): return - with self.cache_file.open("r", encoding="utf-8") as f: - data: dict = json.load(f) - self.cache = {key: CacheEntry(**entry_data) for key, entry_data in data.items()} - - def clear(self): - """清空缓存数据""" - self.cache.clear() + try: + with open(self.cache_file, "r", encoding="utf-8") as f: + data = json.load(f) + self.cache = {k: CacheEntry(**v) for k, v in data.items()} + except OSError, json.JSONDecodeError, TypeError: + self.cache = {} def save(self): - """保存缓存数据""" - serializable_cache = {key: asdict(entry) for key, entry in self.cache.items()} - with self.cache_file.open("w", encoding="utf-8") as f: - json.dump(serializable_cache, f) + """保存缓存""" + with open(self.cache_file, "w", encoding="utf-8") as f: + json.dump({k: asdict(v) for k, v in self.cache.items()}, f) - def _calculate_file_hash(self, file_path: Path) -> str: - """计算文件内容哈希""" - hash_md5 = hashlib.md5() - with open(file_path, "rb") as f: - hashlib.file_digest(f, "md5") - return hash_md5.hexdigest() + def needs_rebuild(self, source: Path, obj: Path) -> bool: + """检查是否需要重新构建""" + key = str(source) - def needs_rebuild(self, source_path: Path, object_path: Path) -> bool: - """检查是否需要重新编译 - TODO 头文件并不会被计算 - """ - source_key = str(source_path) - - # 如果没有缓存记录,需要重新编译 - if source_key not in self.cache: + if key not in self.cache: return True - cache_entry = self.cache[source_key] + entry = self.cache[key] - # 检查源文件是否更新 - if source_path.stat().st_mtime > cache_entry.mtime: + # 检查文件是否更新 + if source.stat().st_mtime > entry.mtime: return True # 检查对象文件是否存在 - if not object_path.exists(): + if not obj.exists(): return True - if cache_entry.hash != self._calculate_file_hash(source_path): - return True + # 检查哈希 + with open(source, "rb") as f: + current_hash = hashlib.md5(f.read()).hexdigest() - return False + return current_hash != entry.hash - def update_cache(self, source_path: Path, object_path: Path): - """更新缓存记录""" - source_key = str(source_path) + def update(self, source: Path, obj: Path): + """更新缓存""" + with open(source, "rb") as f: + file_hash = hashlib.md5(f.read()).hexdigest() - self.cache[source_key] = CacheEntry( - mtime=source_path.stat().st_mtime, - hash=self._calculate_file_hash(source_path), - obj_path=str(object_path.absolute()), + self.cache[str(source)] = CacheEntry( + mtime=source.stat().st_mtime, hash=file_hash, obj_path=str(obj.absolute()) ) -class CompilerBuildMode(Enum): - """编译模式""" - - TEST = "test" - DEV = "dev" - DEBUG = "debug" - RELEASE = "release" - NONE = "none" - - class Compiler(ABC): """编译器抽象类""" def __init__(self): - self.recorded_commands: list[str] = [] - self.should_record = False + self.recorded = [] + self.recording = False def enable_recording(self, enable=True): - """启用或禁用命令记录""" - self.should_record = enable + """启用命令记录""" + self.recording = enable if enable: - self.recorded_commands.clear() + self.recorded.clear() - def get_recorded_commands(self): - """获取记录的命令列表""" - return self.recorded_commands.copy() + def record(self, cmd): + """记录命令""" + if self.recording: + self.recorded.append(" ".join(cmd) if isinstance(cmd, list) else cmd) - def get_default_flags(self, mode: CompilerBuildMode) -> list[str]: - """获取指定模式的默认标志""" - logger.debug("get default flags for mode: %s is not supported", mode.name) - return [] - - @abstractmethod - def compile( - self, sources: Path, output: Path, includes: list[Path], flags: list[str] - ): - """编译源文件""" - - @abstractmethod - def link( - self, objects: list[Path], libraries: list[str], flags: list[str], output: Path - ): - """链接对象文件""" - - def cmd(self, cmd: str | list[str]): - """执行命令并处理错误输出""" - if self.should_record: - self.recorded_commands.append( - " ".join(cmd) if isinstance(cmd, list) else cmd - ) + def run(self, cmd): + """运行命令""" + self.record(cmd) + logger.debug("执行命令: %s", cmd) try: - 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 - logger.fatal("command running error: [%d]`%s`", e.returncode, cmd_str) + logger.fatal("命令执行失败 [%d]: %s", e.returncode, e.cmd) raise + @abstractmethod + def get_flags(self, mode: BuildMode) -> list[str]: + """获取编译标志""" -class DummyCompiler(Compiler): - """Dummy编译器""" - + @abstractmethod def compile( - self, sources: Path, output: Path, includes: list[Path], flags: list[str] + self, source: Path, output: Path, includes: list[Path], flags: list[str] ): """编译源文件""" - pass - def link( - self, objects: list[Path], libraries: list[str], flags: list[str], output: Path - ): + @abstractmethod + def link(self, objects: list[Path], output: Path, flags: list[str]): """链接对象文件""" - pass - - -class ClangCompiler(Compiler): - """Clang编译器""" - - def compile( - self, sources: Path, output: Path, includes: list[Path], flags: list[str] - ): - """编译源文件""" - cmd = ["clang"] - cmd.extend(flags) - cmd.extend(["-c", str(sources), "-o", str(output)]) - cmd.extend(f"-I{inc}" for inc in includes) - self.cmd(cmd) - - def link( - self, objects: list[Path], libraries: list[str], flags: list[str], output: Path - ): - """链接对象文件""" - cmd = ["clang"] - cmd.extend(flags) - cmd.extend(["-o", str(output)]) - cmd.extend(str(obj) for obj in objects) - cmd.extend(lib for lib in libraries) - self.cmd(cmd) class GccCompiler(Compiler): - """Gcc编译器""" + """GCC编译器""" - def __init__(self) -> None: - super().__init__() - self.default_flags = { - CompilerBuildMode.NONE: [], - CompilerBuildMode.TEST: [ + def get_flags(self, mode: BuildMode) -> list[str]: + flags = { + BuildMode.TEST: [ "-DTEST_MODE", "-O2", "-g", @@ -708,374 +506,461 @@ class GccCompiler(Compiler): "-Wall", "-Wextra", ], - CompilerBuildMode.DEV: ["-DDEV_MODE", "-O0", "-g", "-Wall", "-Wextra"], - CompilerBuildMode.DEBUG: [ + BuildMode.DEV: ["-DDEV_MODE", "-O0", "-g", "-Wall", "-Wextra"], + BuildMode.DEBUG: [ "-DDEBUG_MODE", "-O0", "-g", - "-fsanitize=address", - "-fsanitize=undefined", - "-fno-omit-frame-pointer", "-Wall", "-Wextra", "-Werror", ], - CompilerBuildMode.RELEASE: ["-O2", "-flto"], + BuildMode.RELEASE: ["-O2", "-flto"], + BuildMode.NONE: [], } - - def get_default_flags(self, mode: CompilerBuildMode) -> list[str]: - """获取指定模式的默认标志""" - return self.default_flags[mode] + return flags.get(mode, []) def compile( - self, sources: Path, output: Path, includes: list[Path], flags: list[str] + self, source: Path, output: Path, includes: list[Path], flags: list[str] ): - """编译源文件""" - cmd = ["gcc"] - cmd.extend(flags) - cmd.extend(["-c", str(sources), "-o", str(output)]) - cmd.extend(f"-I{inc}" for inc in includes) - self.cmd(cmd) + cmd = ["gcc"] + flags + ["-c", str(source), "-o", str(output)] + cmd += [f"-I{inc}" for inc in includes] + self.run(cmd) - def link( - self, objects: list[Path], libraries: list[str], flags: list[str], output: Path + def link(self, objects: list[Path], output: Path, flags: list[str]): + cmd = ["gcc"] + flags + ["-o", str(output)] + [str(obj) for obj in objects] + self.run(cmd) + + +class ClangCompiler(Compiler): + """Clang编译器""" + + def get_flags(self, mode: BuildMode) -> list[str]: + flags = { + BuildMode.TEST: [ + "-DTEST_MODE", + "-O2", + "-g", + "--coverage", + "-Wall", + "-Wextra", + ], + BuildMode.DEV: ["-DDEV_MODE", "-O0", "-g", "-Wall", "-Wextra"], + BuildMode.DEBUG: [ + "-DDEBUG_MODE", + "-O0", + "-g", + "-Wall", + "-Wextra", + "-Werror", + ], + BuildMode.RELEASE: ["-O2"], + BuildMode.NONE: [], + } + return flags.get(mode, []) + + def compile( + self, source: Path, output: Path, includes: list[Path], flags: list[str] ): - """链接对象文件""" - cmd = ["gcc"] - cmd.extend(flags) - cmd.extend(["-o", str(output)]) - cmd.extend(str(obj) for obj in objects) - cmd.extend(lib for lib in libraries) - self.cmd(cmd) + cmd = ["clang"] + flags + ["-c", str(source), "-o", str(output)] + cmd += [f"-I{inc}" for inc in includes] + self.run(cmd) + + def link(self, objects: list[Path], output: Path, flags: list[str]): + cmd = ["clang"] + flags + ["-o", str(output)] + [str(obj) for obj in objects] + self.run(cmd) -class CPackageBuilder: +class DummyCompiler(Compiler): + """虚拟编译器(用于测试)""" + + def get_flags(self, mode: BuildMode) -> list[str]: + return [] + + def compile( + self, source: Path, output: Path, includes: list[Path], flags: list[str] + ): + pass + + def link(self, objects: list[Path], output: Path, flags: list[str]): + pass + + +class PackageBuilder: """包构建器""" def __init__( - self, - package_path: Path, - compiler: Compiler, - mode: CompilerBuildMode = CompilerBuildMode.DEV, - need_cache: bool = True, + self, package_path: Path, compiler: Compiler, mode: BuildMode = BuildMode.DEV ): self.package = PackageConfig(package_path) - self.context = CBuildContext(self.package, None) - self.compiler: Compiler = compiler - # FIXME hack context - self.cache = BuildCache(self.context.path.output_path) if need_cache else None + self.context = BuildContext(self.package, mode) + self.compiler = compiler + self.cache = BuildCache(self.context.paths.output) self.mode = mode - self.global_flags = self.compiler.get_default_flags(mode) + self.flags = self.compiler.get_flags(mode) - def _compile( - self, sources: Path, output: Path, includes: list[Path], flags: list[str] - ) -> None: - """缓存编译""" - if self.cache is not None and self.cache.needs_rebuild(sources, output): - self.compiler.compile(sources, output, includes, flags) - self.cache.update_cache(sources, output) + def _compile(self, source: Path, output: Path): + """编译单个源文件""" + if self.cache.needs_rebuild(source, output): + self.compiler.compile( + source, output, self.context.get_includes(), self.flags + ) + self.cache.update(source, output) else: - logger.debug("Skipping %s", sources) + logger.debug("跳过 %s", source) - def _build_ctx(self) -> list[Path]: - """构建上下文""" - # 确保输出目录存在 - self.context.path.output_path.mkdir(parents=True, exist_ok=True) - self.context.path.object_path.mkdir(parents=True, exist_ok=True) - path_map = self.context.get_build_objs() - - for src, obj in path_map: - self._compile(src, obj, self.context.includes, self.global_flags) - return [pair[1] for pair in path_map] - - 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]): + def build(self, target_types: list[TargetType]): """构建包""" - start_time = time.time() - if self.cache is not None: - self.cache.load() + start = time.time() + self.cache.load() - object_files = self._build_ctx() - targets = self.context.get_targets() - for target in targets: - if target.type not in targets_type: + # 确保输出目录存在 + self.context.paths.output.mkdir(parents=True, exist_ok=True) + self.context.paths.objects.mkdir(parents=True, exist_ok=True) + + # 编译所有依赖的源文件 + objects = [] + for src, obj in self.context.get_compile_objects(): + self._compile(src, obj) + objects.append(obj) + + # 构建目标 + exec_types = {TargetType.MAIN_EXEC, TargetType.EXEC, TargetType.TEST_EXEC} + for target in self.context.get_targets(): + if target.type not in target_types: + continue + if target.type not in exec_types: continue - match target.type: - case TargetType.MAIN_EXECUTABLE: - self._compile( - target.source, - target.object, - self.context.includes, - self.global_flags, - ) - object_files.append(target.object) - self.compiler.link( - object_files, [], self.global_flags, target.output - ) - object_files.remove(target.object) - case TargetType.TEST_EXECUTABLE: - self._compile( - target.source, - target.object, - self.context.includes, - self.global_flags, - ) - object_files.append(target.object) - self.compiler.link( - object_files, [], self.global_flags, target.output - ) - object_files.remove(target.object) + self._compile(target.source, target.object) + objects.append(target.object) + self.compiler.link(objects, target.output, self.flags) + objects.pop() - if self.cache is not None: - self.cache.save() - # 计算构建时间 - 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("Finished %s target(s) in %s", self.mode.value, time_str) + self.cache.save() + elapsed = time.time() - start + time_str = f"{elapsed:.2f}s" if elapsed < 60 else f"{elapsed / 60:.1f}m" + logger.info("完成 %s 目标,耗时 %s", self.mode.value, time_str) def run(self): - """运行项目""" + """运行主程序""" targets = [ - target - for target in self.context.get_targets() - if target.type == TargetType.MAIN_EXECUTABLE + t for t in self.context.get_targets() if t.type == TargetType.MAIN_EXEC ] if len(targets) != 1: - logger.error("not have target to run") + logger.error("没有可运行的目标") + return subprocess.run(targets[0].output, check=False) def clean(self): """清理构建产物""" - # 统计要删除的文件数量和总大小 - cleaned_files = 0 - total_size = 0 - # 删除整个输出目录,这会清除所有构建产物 - if self.context.path.output_path.exists(): - # 遍历目录统计文件数量和大小 - 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) - logger.info("已清理构建目录: %s", self.context.path.output_path) + if self.context.paths.output.exists(): + total_size = 0 + file_count = 0 + for file in self.context.paths.output.rglob("*"): + if file.is_file(): + file_count += 1 + total_size += file.stat().st_size + shutil.rmtree(self.context.paths.output) + logger.info("已清理构建目录: %s", self.context.paths.output) logger.info( - "Cleaned %s files, Size %s", - cleaned_files, - self._format_size(total_size), + "清理了 %d 个文件,总大小 %s", file_count, self._format_size(total_size) ) else: logger.info("没有找到构建目录,无需清理") def tree(self): - """打印构建树""" - self.context.deps_resolver.print_tree() + """打印依赖树""" + self.context.resolver.print_tree() - def tests(self): + def tests(self, filter_str: str = ""): """运行测试""" targets = [ - target - for target in self.context.get_targets() - if target.type == TargetType.TEST_EXECUTABLE + t for t in self.context.get_targets() if t.type == TargetType.TEST_EXEC ] - passed = 0 - failed = 0 - for target in targets: - name = target.name - logger.info("运行测试: %s", name) - logger.debug("test run %s", target.output) + + if filter_str: try: - result = subprocess.run( - target.output, - check=True, - # capture_output=True, - # text=True, - timeout=30, + pattern = re.compile(filter_str) + targets = [t for t in targets if pattern.search(t.name)] + logger.debug( + "使用正则表达式过滤测试: '%s',匹配到 %d 个测试", + filter_str, + len(targets), ) + except re.error as e: + logger.error("无效的正则表达式 '%s': %s", filter_str, e) + return + + if not targets: + logger.info("没有找到匹配的测试") + return + + passed = failed = 0 + for target in targets: + logger.info("运行测试: %s", target.name) + try: + result = subprocess.run(target.output, check=True, timeout=30) if result.returncode == 0: - print(f" ✓ 测试 {name} 通过") + print(f" ✓ 测试 {target.name} 通过") passed += 1 else: - print(f" ✗ 测试 {name} 失败") - if result.stdout: - print(f" 输出: {result.stdout}") - if result.stderr: - print(f" 错误: {result.stderr}") + print(f" ✗ 测试 {target.name} 失败") failed += 1 except subprocess.TimeoutExpired: - print(f" ✗ 测试 {name} 超时") + print(f" ✗ 测试 {target.name} 超时") failed += 1 except subprocess.SubprocessError as e: - print(f" ✗ 测试 {name} 运行异常: {e}") + print(f" ✗ 测试 {target.name} 运行异常: {e}") failed += 1 + print(f"\n测试结果: {passed} 通过, {failed} 失败") + def _format_size(self, size_bytes): + """格式化文件大小""" + if size_bytes == 0: + return "0B" + units = ["B", "KB", "MB", "GB"] + i = 0 + while size_bytes >= 1024 and i < len(units) - 1: + size_bytes /= 1024.0 + i += 1 + return ( + f"{size_bytes:.1f}{units[i]}" if i > 0 else f"{int(size_bytes)}{units[i]}" + ) -def setup_argparse(): - """设置分级命令行参数解析器""" - parser = argparse.ArgumentParser( - description="Simple C Package Manager", prog="cbuild" + +def init_project(project_path: Path, name: str | None = None, is_lib: bool = False): + """初始化新项目""" + if name is None: + name = project_path.name + name = str(name) # 确保 name 是字符串 + + # 创建目录 + (project_path / "src").mkdir(parents=True, exist_ok=True) + (project_path / "include").mkdir(parents=True, exist_ok=True) + (project_path / "tests").mkdir(parents=True, exist_ok=True) + + # 创建 cbuild.toml + toml_content = f"""[package] +name = "{name}" +version = "0.1.0" +authors = [] +description = "" + +# dependencies = [] +# features = {{}} +# default_features = [] +""" + (project_path / "cbuild.toml").write_text(toml_content, encoding="utf-8") + + # 创建源文件 + if is_lib: + (project_path / "src" / "lib.c").write_text( + """#include + +void hello_from_lib() { + printf("Hello from library!\\n"); +} +""", + encoding="utf-8", + ) + + header_content = f"""#ifndef __{name.upper()}_H__ +#define __{name.upper()}_H__ + +void hello_from_lib(); + +#endif +""" + (project_path / "include" / f"{name}.h").write_text( + header_content, encoding="utf-8" + ) + else: + (project_path / "src" / "main.c").write_text( + """#include + +int main() { + printf("Hello, World!\\n"); + return 0; +} +""", + encoding="utf-8", + ) + + # 创建测试文件 + (project_path / "tests" / f"test_{name}.c").write_text( + """#include + +void test_example() { + printf("Test passed!\\n"); +} + +int main() { + test_example(); + return 0; +} +""", + encoding="utf-8", ) - # 全局选项 - parser.add_argument( - "--verbose", "-v", action="store_true", help="Enable verbose logging" - ) - parser.add_argument("--path", "-p", default=".", help="Path to the package") + logger.info("项目 '%s' 初始化完成!", name) + logger.info("项目结构:") + logger.info(" %s/", project_path) + logger.info(" ├── cbuild.toml") + logger.info(" ├── src/") + logger.info(" │ └── %s", "lib.c" if is_lib else "main.c") + logger.info(" ├── include/") + if is_lib: + logger.info(" │ └── %s.h", name) + logger.info(" └── tests/") + logger.info(" └── test_%s.c", name) - # 创建子命令解析器 - subparsers = parser.add_subparsers( - dest="command", help="Available commands", metavar="COMMAND" - ) - # Build 子命令 - build_parser = subparsers.add_parser("build", help="Build the project") - build_parser.add_argument( - "--compiler", - "-c", - choices=["gcc", "clang", "smcc"], - default="gcc", - help="Compiler to use (default: gcc)", - ) - build_parser.add_argument( - "--record", - "-r", - action="store_true", - help="Record the compiler command and output to stdout", - ) +def create_parser(): + """创建命令行解析器""" + parser = argparse.ArgumentParser(description="轻量C构建系统", prog="cbuild") + parser.add_argument("--verbose", "-v", action="store_true", help="详细输出") + parser.add_argument("--path", "-p", default=".", help="项目路径") - # Run 子命令 - run_parser = subparsers.add_parser("run", help="Build and run the project") - run_parser.add_argument( - "--compiler", - "-c", - choices=["gcc", "clang", "smcc"], - default="gcc", - help="Compiler to use (default: gcc)", - ) - run_parser.add_argument( - "--args", nargs=argparse.REMAINDER, help="Arguments to pass to the program" - ) - run_parser.add_argument( - "--record", - "-r", - action="store_true", - help="Record the compiler command and output to stdout", - ) + subparsers = parser.add_subparsers(dest="command", required=True, metavar="COMMAND") - # Test 子命令 - test_parser = subparsers.add_parser("test", help="Build and run tests") - test_parser.add_argument( - "--compiler", - "-c", - choices=["gcc", "clang", "smcc"], - default="gcc", - help="Compiler to use (default: gcc)", - ) - test_parser.add_argument("--filter", help="Filter tests by name pattern") - test_parser.add_argument( - "--record", - "-r", - action="store_true", - help="Record the compiler command and output to stdout", - ) + def add_common_args(subparser): + subparser.add_argument( + "--compiler", "-c", choices=["gcc", "clang"], default="gcc", help="编译器" + ) + subparser.add_argument("--record", "-r", action="store_true", help="记录命令") + mode_group = subparser.add_mutually_exclusive_group() + mode_group.add_argument( + "--debug", + action="store_const", + dest="mode", + const=BuildMode.DEBUG, + help="调试模式", + ) + mode_group.add_argument( + "--release", + action="store_const", + dest="mode", + const=BuildMode.RELEASE, + help="发布模式", + ) + mode_group.add_argument( + "--dev", + action="store_const", + dest="mode", + const=BuildMode.DEV, + help="开发模式", + ) + mode_group.add_argument( + "--test", + action="store_const", + dest="mode", + const=BuildMode.TEST, + help="测试模式", + ) - # Clean 子命令 - subparsers.add_parser("clean", help="Clean build artifacts") + # build 命令 + build_parser = subparsers.add_parser("build", help="构建项目") + add_common_args(build_parser) + build_parser.set_defaults(mode=BuildMode.DEV) - # Tree 子命令 - subparsers.add_parser("tree", help="Show dependency tree") + # run 命令 + run_parser = subparsers.add_parser("run", help="运行项目") + add_common_args(run_parser) + run_parser.set_defaults(mode=BuildMode.DEV) + + # test 命令 + test_parser = subparsers.add_parser("test", help="运行测试") + add_common_args(test_parser) + test_parser.add_argument("--filter", default="", help="过滤测试") + test_parser.set_defaults(mode=BuildMode.TEST) + + # clean 命令 + clean_parser = subparsers.add_parser("clean", help="清理构建产物") + add_common_args(clean_parser) + clean_parser.add_argument("--all", action="store_true", help="清理所有模式") + + # tree 命令 + tree_parser = subparsers.add_parser("tree", help="显示依赖树") + add_common_args(tree_parser) + + # init 命令 + init_parser = subparsers.add_parser("init", help="初始化项目") + init_parser.add_argument("--name", "-n", help="项目名称") + init_parser.add_argument("--lib", action="store_true", help="创建库项目") return parser def main(): - """main function with hierarchical command processing""" - parser = setup_argparse() + """主函数""" + parser = create_parser() args = parser.parse_args() # 设置日志级别 - if args.verbose: - logger.setLevel(logging.DEBUG) - else: - logger.setLevel(logging.INFO) + logger.setLevel(logging.DEBUG if args.verbose else logging.INFO) - # 初始化编译器 - compiler = DummyCompiler() - if hasattr(args, "compiler") and args.compiler is not None: - if args.compiler == "gcc": - compiler = GccCompiler() - elif args.compiler == "clang": - compiler = ClangCompiler() - elif args.compiler == "smcc": - # TODO self compiler - raise ValueError("Invalid compiler") - else: - raise ValueError("Invalid compiler") + # 处理 init 命令 + if args.command == "init": + init_project(Path(args.path), args.name, args.lib) + return + + # 选择编译器 + compiler_map = { + "gcc": GccCompiler(), + "clang": ClangCompiler(), + } + compiler = compiler_map.get(args.compiler, GccCompiler()) - # 启用命令记录 if hasattr(args, "record") and args.record: compiler.enable_recording() + # 获取构建模式 + mode = getattr(args, "mode", BuildMode.DEV) + # 创建构建器 - package_path = Path.cwd() / Path(args.path) - builder = CPackageBuilder(package_path, compiler) + builder = PackageBuilder(Path(args.path), compiler, mode) - # 处理子命令 - match args.command: - case "build": - builder.build( - [ - TargetType.MAIN_EXECUTABLE, - TargetType.TEST_EXECUTABLE, - TargetType.STATIC_LIBRARY, - TargetType.SHARED_LIBRARY, - TargetType.EXECUTABLE, - ] - ) - case "run": - bin_path = builder.context.path.default_bin_path - if not bin_path.exists(): - logger.error("%s not exist", bin_path) - return - builder.build([TargetType.MAIN_EXECUTABLE]) - builder.run() - case "test": - builder.build([TargetType.TEST_EXECUTABLE]) - builder.tests() - case "clean": + # 执行命令 + if args.command == "build": + builder.build( + [ + TargetType.MAIN_EXEC, + TargetType.EXEC, + TargetType.TEST_EXEC, + TargetType.STATIC_LIB, + ] + ) + elif args.command == "run": + builder.build([TargetType.MAIN_EXEC]) + builder.run() + elif args.command == "test": + builder.build([TargetType.TEST_EXEC]) + builder.tests(getattr(args, "filter", "")) + elif args.command == "clean": + if hasattr(args, "all") and args.all: + # 清理所有模式 + for clean_mode in [ + BuildMode.DEV, + BuildMode.DEBUG, + BuildMode.RELEASE, + BuildMode.TEST, + ]: + clean_builder = PackageBuilder(Path(args.path), compiler, clean_mode) + clean_builder.clean() + else: builder.clean() - case "tree": - builder.tree() - case _: - logger.error("unknown command: %s", args.command) + elif args.command == "tree": + builder.tree() - # 输出记录的命令(如果有) + # 输出记录的命令 if hasattr(args, "record") and args.record: - cmds = compiler.get_recorded_commands() - logger.info("Recorded compiler commands: [len is %s]", len(cmds)) + cmds = compiler.recorded + logger.info("记录的命令 (%d 条):", len(cmds)) for cmd in cmds: print(cmd) if __name__ == "__main__": - # builder = CPackageBuilder(Path("./runtime/libcore/"), ClangCompiler()) - # builder.build() main()