feat(cbuild): 引入构建缓存机制与编译模式支持
新增了基于文件修改时间和内容哈希的构建缓存功能,能够有效避免不必要的重复编译。同时增加了多种编译模式(如 debug、release、test 等)及其对应的默认编译选项,提升了构建过程的灵活性和效率。 - 添加 `BuildCache` 类用于管理缓存逻辑 - 支持通过 `CompilerBuildMode` 枚举选择不同构建模式 - 在 `CPackageBuilder` 中集成缓存判断与更新流程 - 优化日志输出及部分代码结构以提升可读性
This commit is contained in:
@@ -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,9 +15,12 @@ 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 - 绿色
|
||||||
@@ -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__)
|
|
||||||
|
def setup_logger() -> logging.Logger:
|
||||||
|
"""设置日志记录器"""
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
console_handler = logging.StreamHandler()
|
console_handler = logging.StreamHandler()
|
||||||
color_formatter = ColorFormatter()
|
console_handler.setFormatter(ColorFormatter())
|
||||||
console_handler.setFormatter(color_formatter)
|
_logger.addHandler(console_handler)
|
||||||
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
|
||||||
@@ -269,36 +294,50 @@ class BuildPath:
|
|||||||
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(
|
||||||
|
Target(
|
||||||
name=self.package.name,
|
name=self.package.name,
|
||||||
type=TargetType.MAIN_EXECUTABLE,
|
type=TargetType.MAIN_EXECUTABLE,
|
||||||
source=self.path.default_bin_path,
|
source=self.path.default_bin_path,
|
||||||
object=self.get_object_path(self.path.default_bin_path),
|
object=self.get_object_path(self.path.default_bin_path),
|
||||||
output=self.path.output_path / f"{self.package.name}{execute_ext}",
|
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(
|
||||||
|
Target(
|
||||||
name=f"lib{self.package.name}",
|
name=f"lib{self.package.name}",
|
||||||
type=TargetType.STATIC_LIBRARY,
|
type=TargetType.STATIC_LIBRARY,
|
||||||
source=self.path.default_lib_path,
|
source=self.path.default_lib_path,
|
||||||
object=self.get_object_path(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"
|
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(
|
||||||
|
Target(
|
||||||
name=test_source.stem,
|
name=test_source.stem,
|
||||||
type=TargetType.TEST_EXECUTABLE,
|
type=TargetType.TEST_EXECUTABLE,
|
||||||
source=test_source,
|
source=test_source,
|
||||||
object=self.get_object_path(test_source),
|
object=self.get_object_path(test_source),
|
||||||
output=self.path.output_path / f"{test_source.stem}{execute_ext}"
|
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(
|
||||||
|
target.output,
|
||||||
check=True,
|
check=True,
|
||||||
# capture_output=True,
|
# capture_output=True,
|
||||||
# text=True,
|
# text=True,
|
||||||
timeout=30)
|
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.MAIN_EXECUTABLE,
|
||||||
TargetType.TEST_EXECUTABLE,
|
TargetType.TEST_EXECUTABLE,
|
||||||
TargetType.STATIC_LIBRARY,
|
TargetType.STATIC_LIBRARY,
|
||||||
TargetType.SHARED_LIBRARY,
|
TargetType.SHARED_LIBRARY,
|
||||||
TargetType.EXECUTABLE,
|
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()
|
||||||
|
|||||||
Reference in New Issue
Block a user