# build.py from abc import ABC, abstractmethod import tomllib import pprint import subprocess from pathlib import Path from dataclasses import dataclass, field from enum import Enum, auto import logging import argparse import sys from typing import Self @dataclass class Dependency: """依赖配置""" name: str path: str version: str = "0.0.0" optional: bool = False @dataclass class Feature: """特性配置""" name: str description: str = "" dependencies: list[str] = field(default_factory=list) 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 def __str__(self) -> str: return pprint.pformat(self.config) @property def name(self) -> str: """获取包的名称 Returns: str: 包的名称,如果未定义则返回空字符串 """ return self.config.get("name", "") @property def version(self) -> str: """获取包的版本 Returns: str: 包的版本号,如果未定义则返回空字符串 """ return self.config.get("version", "") @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) ) 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", "") @dataclass(frozen=True) class BuildPath: """path""" root_path: Path tests_path: Path output_path: Path object_path: Path src_path: Path default_bin_path: Path default_lib_path: Path @classmethod def from_root_path(cls, root_path: Path) -> Self: """_summary_ Args: output_path (Path): _description_ Returns: Self: _description_ """ src_path = root_path / "src" output_path = root_path / "build" return cls( root_path = root_path, tests_path = root_path / "tests", 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", ) class TargetType(Enum): """目标文件类型枚举""" MAIN_EXECUTABLE = auto() EXECUTABLE = auto() TEST_EXECUTABLE = auto() STATIC_LIBRARY = auto() SHARED_LIBRARY = auto() @dataclass class Target: """目标文件信息""" name: str type: TargetType source: Path object: Path output: Path class CBuildContext: """构建上下文,管理所有包的信息""" def __init__(self, package: PackageConfig, build_path: BuildPath | None = None): self.package = package self.path = BuildPath.from_root_path(package.path) \ if build_path is None else build_path @property def sources_path(self) -> list[Path]: """获取所有包的源文件路径""" return list(self.path.src_path.glob("**/*.c")) @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 @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) return [inc for inc in includes if inc.exists()] def get_targets(self) -> list[Target]: """获取所有构建目标""" targets = [] # TODO ext = ".exe" # 添加主可执行文件目标 if self.path.default_bin_path.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}{ext}", )) # 添加静态库目标 if self.path.default_lib_path.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" )) # 添加测试目标 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}{ext}" )) return targets def get_build_components(self) -> list[tuple[Path, Path]]: """获取所有需要编译的源文件及目标文件(排除主文件即main.c, lib.c, bin/*.c)""" 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, self.get_object_path(path))) # logging.debug("[+] build_components: %s", objs) deps = self.get_dependencies() for dep in deps: objs.extend(dep.get_build_components()) 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) # 生成相对于src目录的路径结构 try: relative_path = source_path.relative_to(self.path.src_path) object_path = objects_dir / relative_path.with_suffix('.o') # 确保对象文件的目录存在 object_path.parent.mkdir(parents=True, exist_ok=True) return object_path except ValueError: # 如果源文件不在src目录下,使用文件名作为对象文件名 return objects_dir / source_path.with_suffix('.o').name 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: """缓存条目""" source_hash: str object_hash: str compile_time: float includes_hash: str class BuildCache: """构建缓存管理器""" pass class Compiler(ABC): """编译器抽象类""" @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 compile_all(self, sources: list[tuple[Path, Path]], includes: list[Path], flags: list[str]): """编译所有源文件""" for source, output in sources: self.compile(source, output, includes, flags) def cmd(self, cmd: str | list[str]): """执行命令并处理错误输出""" try: logging.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) raise 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编译器""" def compile(self, sources: 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) def link(self, objects: list[Path], libraries: list[str], flags: list[str], output: Path): """链接对象文件""" cmd = ["gcc"] objs = set(str(i.absolute()) for i in objects) cmd.extend(flags) cmd.extend(["-o", str(output)]) cmd.extend(objs) cmd.extend(lib for lib in libraries) self.cmd(cmd) class CPackageBuilder: """包构建器""" # TODO EXT = ".exe" def __init__(self, package_path: Path, compiler: Compiler): self.package = PackageConfig(package_path) self.context = CBuildContext(self.package, None) self.compiler: Compiler = compiler self.global_flags = ["-g"] 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) # 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): """构建包""" object_files = self._build_ctx() targets = self.context.get_targets() for target in targets: match target.type: case TargetType.MAIN_EXECUTABLE: self.compiler.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.compiler.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) logging.info("Building is Ok...") 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") subprocess.run(targets[0].output, check=False) def tests(self): """运行测试""" targets = [target for target in self.context.get_targets() \ if target.type == TargetType.TEST_EXECUTABLE] passed = 0 failed = 0 for target in targets: name = target.name print(f"运行测试: {name}") logging.debug("test run %s", target.output) try: result = subprocess.run(target.output, check=True, # capture_output=True, # text=True, timeout=30) if result.returncode == 0: print(f" ✓ 测试 {name} 通过") passed += 1 else: print(f" ✗ 测试 {name} 失败") if result.stdout: print(f" 输出: {result.stdout}") if result.stderr: print(f" 错误: {result.stderr}") failed += 1 except subprocess.TimeoutExpired: print(f" ✗ 测试 {name} 超时") failed += 1 except subprocess.SubprocessError as e: print(f" ✗ 测试 {name} 运行异常: {e}") failed += 1 print(f"\n测试结果: {passed} 通过, {failed} 失败") def main(): """main""" parser = argparse.ArgumentParser(description="Simple C Package Manager") parser.add_argument("command", choices=["build", "run", "test"], help="Command to execute") parser.add_argument("--verbose", "-v", action="store_true", help="enable the logging to debug (defalut: false)") parser.add_argument("--path", "-p", default=".", help="Path to the package (default: current directory)") args = parser.parse_args() package_path = Path(args.path) if args.verbose: logging.getLogger().setLevel(logging.NOTSET) compiler = GccCompiler() 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() except Exception as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) if __name__ == "__main__": # builder = CPackageBuilder(Path("./runtime/libcore/"), ClangCompiler()) # builder.build() main()