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