feat(cbuild): 添加构建路径去重与相对化处理

新增 `_path_collection` 方法用于统一处理路径的解析、去重及相对化,
优化对象文件路径生成逻辑,支持更安全的路径映射机制,防止文件冲突。
同时添加对构建目录的清理功能(clean 命令),完善构建生命周期管理。

主要变更包括:
- 引入 `hashlib` 模块以支持路径哈希命名
- 重构 `get_build_components` 和 `get_object_path` 方法
- 新增 `clean` 命令及相关实现
- 改进命令行参数支持,增加 "clean" 选项
This commit is contained in:
zzy
2025-11-21 18:03:10 +08:00
parent a3322f0d4c
commit d6941e1d2f

View File

@@ -1,4 +1,4 @@
# build.py
"""cbuild.py"""
from abc import ABC, abstractmethod
import tomllib
import pprint
@@ -7,6 +7,7 @@ from pathlib import Path
from dataclasses import dataclass, field
from enum import Enum, auto
import logging
import hashlib
import argparse
import sys
@@ -186,9 +187,15 @@ class CBuildContext:
"""构建上下文,管理所有包的信息"""
def __init__(self, package: PackageConfig, build_path: BuildPath | None = None):
self.package = package
self.relative_path = Path.cwd()
self.path = BuildPath.from_root_path(package.path) \
if build_path is None else build_path
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]
@property
def sources_path(self) -> list[Path]:
"""获取所有包的源文件路径"""
@@ -212,8 +219,7 @@ class CBuildContext:
deps = self.get_dependencies()
for dep in deps:
includes.extend(dep.includes)
return [inc for inc in includes if inc.exists()]
return self._path_collection(includes)
def get_targets(self) -> list[Target]:
"""获取所有构建目标"""
@@ -254,21 +260,28 @@ class CBuildContext:
return targets
def get_build_components(self) -> list[tuple[Path, Path]]:
"""获取所有需要编译的源文件及目标文件(排除主文件即main.c, lib.c, bin/*.c)"""
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, 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())
objs.append(path)
return objs
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())
objs = []
for dep in deps:
objs.extend(dep.get_self_build_objs())
self._path_collection(objs)
return [(source_path, self.get_object_path(source_path)) for source_path in objs]
def get_object_path(self, source_path: Path) -> Path:
"""将源文件路径映射到对象文件路径
@@ -282,16 +295,22 @@ class CBuildContext:
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
# 尝试生成相对于根目录的路径结构,避免冲突
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')
except ValueError:
# 如果源文件不在src目录下,使用文件名作为对象文件名
return objects_dir / source_path.with_suffix('.o').name
# 如果源文件完全不在项目目录下,使用完整路径哈希
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
# 确保对象文件的目录存在
object_path.parent.mkdir(parents=True, exist_ok=True)
return object_path
def get_dependencies(self) -> list[Self]:
"""_summary_
@@ -333,7 +352,6 @@ class Compiler(ABC):
for source, output in sources:
self.compile(source, output, includes, flags)
def cmd(self, cmd: str | list[str]):
"""执行命令并处理错误输出"""
try:
@@ -377,10 +395,9 @@ class GccCompiler(Compiler):
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(str(obj) for obj in objects)
cmd.extend(lib for lib in libraries)
self.cmd(cmd)
@@ -434,6 +451,16 @@ class CPackageBuilder:
logging.error("not have target to run")
subprocess.run(targets[0].output, check=False)
def clean(self):
"""清理构建产物"""
# 删除整个输出目录,这会清除所有构建产物
if self.context.path.output_path.exists():
import shutil
shutil.rmtree(self.context.path.output_path)
print(f"已清理构建目录: {self.context.path.output_path}")
else:
print("没有找到构建目录,无需清理")
def tests(self):
"""运行测试"""
targets = [target for target in self.context.get_targets() \
@@ -471,7 +498,7 @@ class CPackageBuilder:
def main():
"""main"""
parser = argparse.ArgumentParser(description="Simple C Package Manager")
parser.add_argument("command", choices=["build", "run", "test"],
parser.add_argument("command", choices=["build", "run", "test", "clean"],
help="Command to execute")
parser.add_argument("--verbose", "-v", action="store_true",
help="enable the logging to debug (defalut: false)")
@@ -502,6 +529,10 @@ def main():
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)