feat(cbuild): 引入构建缓存机制与编译模式支持

新增了基于文件修改时间和内容哈希的构建缓存功能,能够有效避免不必要的重复编译。同时增加了多种编译模式(如 debug、release、test 等)及其对应的默认编译选项,提升了构建过程的灵活性和效率。

- 添加 `BuildCache` 类用于管理缓存逻辑
- 支持通过 `CompilerBuildMode` 枚举选择不同构建模式
- 在 `CPackageBuilder` 中集成缓存判断与更新流程
- 优化日志输出及部分代码结构以提升可读性
This commit is contained in:
zzy
2025-11-22 17:03:48 +08:00
parent fa5611dabd
commit 67af0c6bf2

View File

@@ -1,10 +1,11 @@
"""cbuild.py""" """cbuild.py"""
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
import tomllib import tomllib
import pprint import pprint
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from dataclasses import dataclass, field from dataclasses import asdict, dataclass, field
from enum import Enum, auto from enum import Enum, auto
import logging import logging
import hashlib import hashlib
@@ -14,18 +15,21 @@ import argparse
from typing import Self from typing import Self
import time import time
import sys import sys
import json
class ColorFormatter(logging.Formatter): class ColorFormatter(logging.Formatter):
"""颜色格式化""" """颜色格式化"""
# 基础颜色定义对应ANSI颜色码 # 基础颜色定义对应ANSI颜色码
cyan = "\x1b[36m" # DEBUG - 青色 cyan = "\x1b[36m" # DEBUG - 青色
green = "\x1b[32m" # INFO - 绿色 green = "\x1b[32m" # INFO - 绿色
blue = "\x1b[34m" # TRACE - 蓝色 blue = "\x1b[34m" # TRACE - 蓝色
yellow = "\x1b[33m" # WARN - 黄色 yellow = "\x1b[33m" # WARN - 黄色
red = "\x1b[31m" # ERROR - 红色 red = "\x1b[31m" # ERROR - 红色
line_red = "\x1b[31;4m" # FATAL - 红色加下划线 line_red = "\x1b[31;4m" # FATAL - 红色加下划线
bold = "\x1b[1m" # 粗体 bold = "\x1b[1m" # 粗体
reset = "\x1b[0m" # 重置 reset = "\x1b[0m" # 重置
fmt = "%(name)s - %(levelname)s: " fmt = "%(name)s - %(levelname)s: "
time = "%(asctime)s" time = "%(asctime)s"
@@ -33,12 +37,12 @@ class ColorFormatter(logging.Formatter):
file = " - (%(filename)s:%(lineno)d)" file = " - (%(filename)s:%(lineno)d)"
FORMATS = { FORMATS = {
logging.DEBUG: bold + cyan + fmt + reset + msg + file, logging.DEBUG: bold + cyan + fmt + reset + msg + file,
logging.INFO: bold + green + fmt + reset + msg, logging.INFO: bold + green + fmt + reset + msg,
logging.WARNING: bold + yellow + fmt + reset + msg, logging.WARNING: bold + yellow + fmt + reset + msg,
logging.ERROR: bold + red + fmt + reset + msg, logging.ERROR: bold + red + fmt + reset + msg,
logging.CRITICAL: bold + line_red + fmt + reset + msg + file, logging.CRITICAL: bold + line_red + fmt + reset + msg + file,
logging.FATAL: bold + line_red + fmt + reset + msg + file, logging.FATAL: bold + line_red + fmt + reset + msg + file,
} }
def format(self, record): def format(self, record):
@@ -46,29 +50,41 @@ class ColorFormatter(logging.Formatter):
formatter = logging.Formatter(log_fmt) formatter = logging.Formatter(log_fmt)
return formatter.format(record) return formatter.format(record)
logger = logging.getLogger(__name__)
console_handler = logging.StreamHandler() def setup_logger() -> logging.Logger:
color_formatter = ColorFormatter() """设置日志记录器"""
console_handler.setFormatter(color_formatter) _logger = logging.getLogger(__name__)
logger.addHandler(console_handler) console_handler = logging.StreamHandler()
console_handler.setFormatter(ColorFormatter())
_logger.addHandler(console_handler)
return _logger
logger = setup_logger()
@dataclass @dataclass
class Dependency: class Dependency:
"""依赖配置""" """依赖配置"""
name: str name: str
path: str path: str
version: str = "0.0.0" version: str = "0.0.0"
optional: bool = False optional: bool = False
@dataclass @dataclass
class Feature: class Feature:
"""特性配置""" """特性配置"""
name: str name: str
description: str = "" description: str = ""
dependencies: list[str] = field(default_factory=list) dependencies: list[str] = field(default_factory=list)
class PackageConfig: class PackageConfig:
"""包配置类, 用于解析和管理cbuild.toml配置""" """包配置类, 用于解析和管理cbuild.toml配置"""
CONFIG_FILE = "cbuild.toml" CONFIG_FILE = "cbuild.toml"
def __init__(self, config_path: Path): def __init__(self, config_path: Path):
@@ -129,7 +145,7 @@ class PackageConfig:
feature = Feature( feature = Feature(
name=name, name=name,
description=feature.get("description", ""), description=feature.get("description", ""),
dependencies=feature.get("dependencies", []) dependencies=feature.get("dependencies", []),
) )
features.append(feature) features.append(feature)
return features return features
@@ -149,7 +165,7 @@ class PackageConfig:
name=dep_dict.get("name", ""), name=dep_dict.get("name", ""),
path=dep_dict.get("path", ""), path=dep_dict.get("path", ""),
version=dep_dict.get("version", "0.0.0"), version=dep_dict.get("version", "0.0.0"),
optional=dep_dict.get("optional", False) optional=dep_dict.get("optional", False),
) )
dependencies.append(dependency) dependencies.append(dependency)
return dependencies return dependencies
@@ -172,8 +188,10 @@ class PackageConfig:
""" """
return self.config.get("description", "") return self.config.get("description", "")
class DependencyResolver: class DependencyResolver:
"""依赖解析器""" """依赖解析器"""
def __init__(self, root_package: PackageConfig): def __init__(self, root_package: PackageConfig):
self.root_package = root_package self.root_package = root_package
self.resolved_deps: dict[str, PackageConfig] = {} self.resolved_deps: dict[str, PackageConfig] = {}
@@ -224,19 +242,26 @@ class DependencyResolver:
self.resolve() self.resolve()
sorted_names = list(self.deps_graph.static_order()) sorted_names = list(self.deps_graph.static_order())
return [CBuildContext(self.resolved_deps[name]) for name in sorted_names return [
if name != self.root_package.name and name in self.resolved_deps] CBuildContext(self.resolved_deps[name])
for name in sorted_names
if name != self.root_package.name and name in self.resolved_deps
]
def get_all_contexts(self) -> list[CBuildContext]: def get_all_contexts(self) -> list[CBuildContext]:
"""获取所有上下文(包括根包)""" """获取所有上下文(包括根包)"""
if not self._resolved: if not self._resolved:
self.resolve() self.resolve()
return [CBuildContext(resolved_dep) for resolved_dep in self.resolved_deps.values()] return [
CBuildContext(resolved_dep) for resolved_dep in self.resolved_deps.values()
]
@dataclass(frozen=True) @dataclass(frozen=True)
class BuildPath: class BuildPath:
"""path""" """path"""
root_path: Path root_path: Path
tests_path: Path tests_path: Path
output_path: Path output_path: Path
@@ -259,46 +284,60 @@ class BuildPath:
src_path = root_path / "src" src_path = root_path / "src"
output_path = root_path / "build" output_path = root_path / "build"
return cls( return cls(
root_path = root_path, root_path=root_path,
tests_path = root_path / "tests", tests_path=root_path / "tests",
inc_path = root_path / "include", inc_path=root_path / "include",
output_path = output_path , output_path=output_path,
object_path = output_path / "obj", object_path=output_path / "obj",
src_path = src_path, src_path=src_path,
default_bin_path = src_path / "main.c", default_bin_path=src_path / "main.c",
default_lib_path = src_path / "lib.c", default_lib_path=src_path / "lib.c",
) )
class TargetType(Enum): class TargetType(Enum):
"""目标文件类型枚举""" """目标文件类型枚举"""
MAIN_EXECUTABLE = auto() MAIN_EXECUTABLE = auto()
EXECUTABLE = auto() EXECUTABLE = auto()
TEST_EXECUTABLE = auto() TEST_EXECUTABLE = auto()
STATIC_LIBRARY = auto() STATIC_LIBRARY = auto()
SHARED_LIBRARY = auto() SHARED_LIBRARY = auto()
@dataclass @dataclass
class Target: class Target:
"""目标文件信息""" """目标文件信息"""
name: str name: str
type: TargetType type: TargetType
source: Path source: Path
object: Path object: Path
output: Path output: Path
class CBuildContext: class CBuildContext:
"""构建上下文,管理所有包的信息""" """构建上下文,管理所有包的信息"""
def __init__(self, package: PackageConfig, build_path: BuildPath | None = None):
def __init__(
self,
package: PackageConfig,
build_path: BuildPath | None = None,
need_cache: bool = True,
):
self.package = package self.package = package
self.relative_path = Path.cwd() self.relative_path = Path.cwd()
self.path = BuildPath.from_root_path(package.path) \ self.path = (
if build_path is None else build_path BuildPath.from_root_path(package.path) if build_path is None else build_path
)
self.deps_resolver = DependencyResolver(package) self.deps_resolver = DependencyResolver(package)
def _path_collection(self, path: list[Path]) -> list[Path]: def _path_collection(self, path: list[Path]) -> list[Path]:
p = [str(p.resolve()) for p in path if p.exists()] p = [str(p.resolve()) for p in path if p.exists()]
unique_paths = set(p) unique_paths = set(p)
return [Path(p).relative_to(self.relative_path, walk_up=True) for p in unique_paths] return [
Path(p).relative_to(self.relative_path, walk_up=True) for p in unique_paths
]
@property @property
def sources_path(self) -> list[Path]: def sources_path(self) -> list[Path]:
@@ -331,34 +370,41 @@ class CBuildContext:
# 添加主可执行文件目标 # 添加主可执行文件目标
if self.path.default_bin_path.exists(): if self.path.default_bin_path.exists():
targets.append(Target( targets.append(
name=self.package.name, Target(
type=TargetType.MAIN_EXECUTABLE, name=self.package.name,
source=self.path.default_bin_path, type=TargetType.MAIN_EXECUTABLE,
object=self.get_object_path(self.path.default_bin_path), source=self.path.default_bin_path,
output=self.path.output_path / f"{self.package.name}{execute_ext}", object=self.get_object_path(self.path.default_bin_path),
)) output=self.path.output_path / f"{self.package.name}{execute_ext}",
)
)
# 添加静态库目标 # 添加静态库目标
if self.path.default_lib_path.exists(): if self.path.default_lib_path.exists():
targets.append(Target( targets.append(
name=f"lib{self.package.name}", Target(
type=TargetType.STATIC_LIBRARY, name=f"lib{self.package.name}",
source=self.path.default_lib_path, type=TargetType.STATIC_LIBRARY,
object=self.get_object_path(self.path.default_lib_path), source=self.path.default_lib_path,
output=self.path.output_path / f"lib{self.package.name}.a" 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(): if self.path.tests_path.exists():
for test_source in self.tests_path: for test_source in self.tests_path:
targets.append(Target( targets.append(
name=test_source.stem, Target(
type=TargetType.TEST_EXECUTABLE, name=test_source.stem,
source=test_source, type=TargetType.TEST_EXECUTABLE,
object=self.get_object_path(test_source), source=test_source,
output=self.path.output_path / f"{test_source.stem}{execute_ext}" object=self.get_object_path(test_source),
)) output=self.path.output_path
/ f"{test_source.stem}{execute_ext}",
)
)
return targets return targets
@@ -373,15 +419,6 @@ class CBuildContext:
objs.append(path) objs.append(path)
return objs return objs
def get_build_components(self) -> list[tuple[Path, Path]]:
"""获取所有需要编译的源文件及目标文件(排除主文件即main.c, lib.c, bin/*.c)"""
deps = self.deps_resolver.get_all_contexts()
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:
"""将源文件路径映射到对象文件路径 """将源文件路径映射到对象文件路径
@@ -399,8 +436,8 @@ class CBuildContext:
# 尝试生成相对于根目录的路径结构,避免冲突 # 尝试生成相对于根目录的路径结构,避免冲突
relative_path = source_path.relative_to(self.path.root_path) relative_path = source_path.relative_to(self.path.root_path)
# 将路径分隔符替换为特殊字符,防止文件系统问题 # 将路径分隔符替换为特殊字符,防止文件系统问题
safe_relative_path = str(relative_path).replace('/', '_').replace('\\', '_') safe_relative_path = str(relative_path).replace("/", "_").replace("\\", "_")
object_path = objects_dir / Path(safe_relative_path).with_suffix('.o') object_path = objects_dir / Path(safe_relative_path).with_suffix(".o")
except ValueError: except ValueError:
# 如果源文件完全不在项目目录下,则使用完整路径哈希 # 如果源文件完全不在项目目录下,则使用完整路径哈希
full_path_str = str(source_path.absolute()) full_path_str = str(source_path.absolute())
@@ -412,47 +449,150 @@ class CBuildContext:
object_path.parent.mkdir(parents=True, exist_ok=True) object_path.parent.mkdir(parents=True, exist_ok=True)
return object_path return object_path
def get_build_objs(self) -> list[tuple[Path, Path]]:
"""获取所有需要编译的源文件及目标文件(排除主文件即main.c, lib.c, bin/*.c)"""
deps = self.deps_resolver.get_all_contexts()
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
]
@dataclass @dataclass
class CacheEntry: class CacheEntry:
"""缓存条目""" """缓存条目"""
source_hash: str
object_hash: str mtime: float
compile_time: float hash: str
includes_hash: str obj_path: str
class BuildCache: class BuildCache:
"""构建缓存管理器""" """构建缓存管理器"""
pass
def __init__(self, build_path: Path):
self.cache_dir = build_path / 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] = {}
def load(self):
"""加载缓存数据"""
if not self.cache_file.exists() or self.cache_file.stat().st_size == 0:
self.cache_file.touch()
self.cache = {}
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()
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)
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: Path, object_path: Path) -> bool:
"""检查是否需要重新编译
TODO 头文件并不会被计算
"""
source_key = str(source_path)
# 如果没有缓存记录,需要重新编译
if source_key not in self.cache:
return True
cache_entry = self.cache[source_key]
# 检查源文件是否更新
if source_path.stat().st_mtime > cache_entry.mtime:
return True
# 检查对象文件是否存在
if not object_path.exists():
return True
if cache_entry.hash != self._calculate_file_hash(source_path):
return True
return False
def update_cache(self, source_path: Path, object_path: Path):
"""更新缓存记录"""
source_key = str(source_path)
self.cache[source_key] = CacheEntry(
mtime=source_path.stat().st_mtime,
hash=self._calculate_file_hash(source_path),
obj_path=str(object_path.absolute()),
)
class CompilerBuildMode(Enum):
"""编译模式"""
TEST = "test"
DEV = "dev"
DEBUG = "debug"
RELEASE = "release"
NONE = "none"
class Compiler(ABC): class Compiler(ABC):
"""编译器抽象类""" """编译器抽象类"""
def get_default_flags(self, mode: CompilerBuildMode) -> list[str]:
"""获取指定模式的默认标志"""
logger.debug("get default flags for mode: %s is not supported", mode.name)
return []
@abstractmethod @abstractmethod
def compile(self, sources: Path, output: Path, includes: list[Path], flags: list[str]): def compile(
self, sources: Path, output: Path, includes: list[Path], flags: list[str]
):
"""编译源文件""" """编译源文件"""
@abstractmethod @abstractmethod
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
):
"""链接对象文件""" """链接对象文件"""
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]): def cmd(self, cmd: str | list[str]):
"""执行命令并处理错误输出""" """执行命令并处理错误输出"""
try: try:
logger.debug("command: `%s`", ' '.join(cmd) if isinstance(cmd, list) else cmd) logger.debug(
"command: `%s`", " ".join(cmd) if isinstance(cmd, list) else cmd
)
subprocess.run(cmd, check=True) subprocess.run(cmd, check=True)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
# 输出详细错误信息帮助调试 # 输出详细错误信息帮助调试
cmd_str = ' '.join(e.cmd) if isinstance(e.cmd, list) else e.cmd cmd_str = " ".join(e.cmd) if isinstance(e.cmd, list) else e.cmd
logger.error("command running error: [%d]`%s`", e.returncode, cmd_str) logger.fatal("command running error: [%d]`%s`", e.returncode, cmd_str)
raise raise
class ClangCompiler(Compiler): class ClangCompiler(Compiler):
"""Clang编译器""" """Clang编译器"""
def compile(self, sources: Path, output: Path, includes: list[Path], flags: list[str]):
def compile(
self, sources: Path, output: Path, includes: list[Path], flags: list[str]
):
"""编译源文件""" """编译源文件"""
cmd = ["clang"] cmd = ["clang"]
cmd.extend(flags) cmd.extend(flags)
@@ -460,7 +600,9 @@ class ClangCompiler(Compiler):
cmd.extend(f"-I{inc}" for inc in includes) cmd.extend(f"-I{inc}" for inc in includes)
self.cmd(cmd) self.cmd(cmd)
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 = ["clang"] cmd = ["clang"]
cmd.extend(flags) cmd.extend(flags)
@@ -469,9 +611,44 @@ class ClangCompiler(Compiler):
cmd.extend(lib for lib in libraries) cmd.extend(lib for lib in libraries)
self.cmd(cmd) self.cmd(cmd)
class GccCompiler(Compiler): class GccCompiler(Compiler):
"""Gcc编译器""" """Gcc编译器"""
def compile(self, sources: Path, output: Path, includes: list[Path], flags: list[str]):
def __init__(self) -> None:
super().__init__()
self.default_flags = {
CompilerBuildMode.NONE: [],
CompilerBuildMode.TEST: [
"-DTEST_MODE",
"-O2",
"-g",
"--coverage",
"-Wall",
"-Wextra",
],
CompilerBuildMode.DEV: ["-DDEV_MODE", "-O0", "-g", "-Wall", "-Wextra"],
CompilerBuildMode.DEBUG: [
"-DDEBUG_MODE",
"-O0",
"-g",
"-fsanitize=address",
"-fsanitize=undefined",
"-fno-omit-frame-pointer",
"-Wall",
"-Wextra",
"-Werror",
],
CompilerBuildMode.RELEASE: ["-O2", "-flto"],
}
def get_default_flags(self, mode: CompilerBuildMode) -> list[str]:
"""获取指定模式的默认标志"""
return self.default_flags[mode]
def compile(
self, sources: Path, output: Path, includes: list[Path], flags: list[str]
):
"""编译源文件""" """编译源文件"""
cmd = ["gcc"] cmd = ["gcc"]
cmd.extend(flags) cmd.extend(flags)
@@ -479,7 +656,9 @@ class GccCompiler(Compiler):
cmd.extend(f"-I{inc}" for inc in includes) cmd.extend(f"-I{inc}" for inc in includes)
self.cmd(cmd) self.cmd(cmd)
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"]
cmd.extend(flags) cmd.extend(flags)
@@ -488,21 +667,43 @@ class GccCompiler(Compiler):
cmd.extend(lib for lib in libraries) cmd.extend(lib for lib in libraries)
self.cmd(cmd) self.cmd(cmd)
class CPackageBuilder: class CPackageBuilder:
"""包构建器""" """包构建器"""
def __init__(self, package_path: Path, compiler: Compiler):
def __init__(
self,
package_path: Path,
compiler: Compiler,
mode: CompilerBuildMode = CompilerBuildMode.DEV,
need_cache: bool = True,
):
self.package = PackageConfig(package_path) self.package = PackageConfig(package_path)
self.context = CBuildContext(self.package, None) self.context = CBuildContext(self.package, None)
self.compiler: Compiler = compiler self.compiler: Compiler = compiler
self.global_flags = ["-g"] # FIXME hack context
self.cache = BuildCache(self.context.path.output_path) if need_cache else None
self.global_flags = self.compiler.get_default_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)
else:
logger.debug("Skipping %s", sources)
def _build_ctx(self) -> list[Path]: def _build_ctx(self) -> list[Path]:
"""构建上下文""" """构建上下文"""
# 确保输出目录存在 # 确保输出目录存在
self.context.path.output_path.mkdir(parents=True, exist_ok=True) self.context.path.output_path.mkdir(parents=True, exist_ok=True)
self.context.path.object_path.mkdir(parents=True, exist_ok=True) self.context.path.object_path.mkdir(parents=True, exist_ok=True)
path_map = self.context.get_build_components() path_map = self.context.get_build_objs()
self.compiler.compile_all(path_map, self.context.includes, self.global_flags)
for src, obj in path_map:
self._compile(src, obj, self.context.includes, self.global_flags)
return [pair[1] for pair in path_map] return [pair[1] for pair in path_map]
def _format_size(self, size_bytes): def _format_size(self, size_bytes):
@@ -523,6 +724,8 @@ class CPackageBuilder:
def build(self, targets_type: list[TargetType]): def build(self, targets_type: list[TargetType]):
"""构建包""" """构建包"""
start_time = time.time() start_time = time.time()
if self.cache is not None:
self.cache.load()
object_files = self._build_ctx() object_files = self._build_ctx()
targets = self.context.get_targets() targets = self.context.get_targets()
@@ -532,28 +735,49 @@ class CPackageBuilder:
match target.type: match target.type:
case TargetType.MAIN_EXECUTABLE: case TargetType.MAIN_EXECUTABLE:
self.compiler.compile(target.source, target.object, self.context.includes, self._compile(
self.global_flags) target.source,
target.object,
self.context.includes,
self.global_flags,
)
object_files.append(target.object) object_files.append(target.object)
self.compiler.link(object_files, [], self.global_flags, target.output) self.compiler.link(
object_files, [], self.global_flags, target.output
)
object_files.remove(target.object) object_files.remove(target.object)
case TargetType.TEST_EXECUTABLE: case TargetType.TEST_EXECUTABLE:
self.compiler.compile(target.source, target.object, self.context.includes, self._compile(
self.global_flags) target.source,
target.object,
self.context.includes,
self.global_flags,
)
object_files.append(target.object) object_files.append(target.object)
self.compiler.link(object_files, [], self.global_flags, target.output) self.compiler.link(
object_files, [], self.global_flags, target.output
)
object_files.remove(target.object) object_files.remove(target.object)
if self.cache is not None:
self.cache.save()
# 计算构建时间 # 计算构建时间
elapsed_time = time.time() - start_time elapsed_time = time.time() - start_time
time_str = f"{elapsed_time:.2f}s" if elapsed_time < 60 else f"{elapsed_time/60:.1f}m" time_str = (
f"{elapsed_time:.2f}s" if elapsed_time < 60 else f"{elapsed_time / 60:.1f}m"
)
# 显示完成信息 # 显示完成信息
logger.info("\tFinished dev [unoptimized + debuginfo] target(s) in %s", logger.info(
time_str) "\tFinished dev [unoptimized + debuginfo] target(s) in %s", time_str
)
def run(self): def run(self):
"""运行项目""" """运行项目"""
targets = [target for target in self.context.get_targets() \ targets = [
if target.type == TargetType.MAIN_EXECUTABLE] target
for target in self.context.get_targets()
if target.type == TargetType.MAIN_EXECUTABLE
]
if len(targets) != 1: if len(targets) != 1:
logger.error("not have target to run") logger.error("not have target to run")
subprocess.run(targets[0].output, check=False) subprocess.run(targets[0].output, check=False)
@@ -571,10 +795,12 @@ class CPackageBuilder:
cleaned_files += 1 cleaned_files += 1
total_size += file_path.stat().st_size total_size += file_path.stat().st_size
shutil.rmtree(self.context.path.output_path) shutil.rmtree(self.context.path.output_path)
logger.info("已清理构建目录: %s", logger.info("已清理构建目录: %s", self.context.path.output_path)
self.context.path.output_path) logger.info(
logger.info("Cleaned %s files, Size %s", "Cleaned %s files, Size %s",
cleaned_files, self._format_size(total_size)) cleaned_files,
self._format_size(total_size),
)
else: else:
logger.info("没有找到构建目录,无需清理") logger.info("没有找到构建目录,无需清理")
@@ -595,14 +821,20 @@ class CPackageBuilder:
# 这里我们简化处理,只显示直接依赖 # 这里我们简化处理,只显示直接依赖
for pkg_config in resolver.resolved_deps.values(): for pkg_config in resolver.resolved_deps.values():
if pkg_config.name != root_pkg.name: # 跳过根包 if pkg_config.name != root_pkg.name: # 跳过根包
indent = "├── " if pkg_config.name != list(resolver.resolved_deps.keys())[-1]\ indent = (
"├── "
if pkg_config.name != list(resolver.resolved_deps.keys())[-1]
else "└── " else "└── "
)
print(f"{indent}{pkg_config.name} v{pkg_config.version}") print(f"{indent}{pkg_config.name} v{pkg_config.version}")
def tests(self): def tests(self):
"""运行测试""" """运行测试"""
targets = [target for target in self.context.get_targets() \ targets = [
if target.type == TargetType.TEST_EXECUTABLE] target
for target in self.context.get_targets()
if target.type == TargetType.TEST_EXECUTABLE
]
passed = 0 passed = 0
failed = 0 failed = 0
for target in targets: for target in targets:
@@ -610,11 +842,13 @@ class CPackageBuilder:
logger.info("运行测试: %s", name) logger.info("运行测试: %s", name)
logger.debug("test run %s", target.output) logger.debug("test run %s", target.output)
try: try:
result = subprocess.run(target.output, result = subprocess.run(
check=True, target.output,
# capture_output=True, check=True,
# text=True, # capture_output=True,
timeout=30) # text=True,
timeout=30,
)
if result.returncode == 0: if result.returncode == 0:
print(f" ✓ 测试 {name} 通过") print(f" ✓ 测试 {name} 通过")
passed += 1 passed += 1
@@ -637,13 +871,15 @@ class CPackageBuilder:
"""执行命令""" """执行命令"""
match cmd: match cmd:
case "build": case "build":
self.build([ self.build(
TargetType.MAIN_EXECUTABLE, [
TargetType.TEST_EXECUTABLE, TargetType.MAIN_EXECUTABLE,
TargetType.STATIC_LIBRARY, TargetType.TEST_EXECUTABLE,
TargetType.SHARED_LIBRARY, TargetType.STATIC_LIBRARY,
TargetType.EXECUTABLE, TargetType.SHARED_LIBRARY,
]) TargetType.EXECUTABLE,
]
)
case "run": case "run":
bin_path = self.context.path.default_bin_path bin_path = self.context.path.default_bin_path
if not bin_path.exists(): if not bin_path.exists():
@@ -661,18 +897,36 @@ class CPackageBuilder:
case _: case _:
logger.error("unknown command: %s", cmd) logger.error("unknown command: %s", cmd)
def main(): def main():
"""main""" """main"""
parser = argparse.ArgumentParser(description="Simple C Package Manager", parser = argparse.ArgumentParser(
prog="cbuild") description="Simple C Package Manager", prog="cbuild"
parser.add_argument("command", choices=["build", "run", "test", "clean", "tree"], )
help="Command to execute") parser.add_argument(
parser.add_argument("--compiler", "-c", choices=["gcc", "clang", "smcc"], default="gcc", "command",
help="Compiler to use (default: gcc)") choices=["build", "run", "test", "clean", "tree"],
parser.add_argument("--verbose", "-V", action="store_true", help="Command to execute",
help="enable the logging to debug (default: false)") )
parser.add_argument("--path", "-p", default=".", parser.add_argument(
help="Path to the package (default: current directory)") "--compiler",
"-c",
choices=["gcc", "clang", "smcc"],
default="gcc",
help="Compiler to use (default: gcc)",
)
parser.add_argument(
"--verbose",
"-V",
action="store_true",
help="enable the logging to debug (default: false)",
)
parser.add_argument(
"--path",
"-p",
default=".",
help="Path to the package (default: current directory)",
)
args = parser.parse_args() args = parser.parse_args()
@@ -695,6 +949,7 @@ def main():
builder = CPackageBuilder(package_path, compiler) builder = CPackageBuilder(package_path, compiler)
builder.cmd(args.command) builder.cmd(args.command)
if __name__ == "__main__": if __name__ == "__main__":
# builder = CPackageBuilder(Path("./runtime/libcore/"), ClangCompiler()) # builder = CPackageBuilder(Path("./runtime/libcore/"), ClangCompiler())
# builder.build() # builder.build()