新增基于 Python 的构建脚本 `cbuild.py`,支持包管理、依赖解析和模块化编译。 同时添加 `.gitignore` 忽略 `build` 目录,并在 `justfile` 中更新构建命令。 移除了原有的 `lib/Makefile` 和主目录下的相关 make 规则,统一使用新构建系统。
513 lines
17 KiB
Python
513 lines
17 KiB
Python
# 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()
|