feat(cbuild): 重构构建系统并迁移至 tools/cbuild

将 `cbuild.py` 迁移至 `tools/cbuild/` 并进行大量功能增强。引入依赖解析器、支持颜色日志输出、
改进包配置默认值处理、完善构建目标识别与拓扑排序依赖管理。同时添加 `.gitignore` 和
`pyproject.toml` 以支持标准 Python 包结构,并更新 README 文档。

新增命令支持:tree(显示依赖树)、clean(带文件统计)、test(运行测试)等,
优化了 Windows 平台下的可执行文件扩展名处理逻辑。

移除了旧的 `wc.py` 行数统计脚本。
This commit is contained in:
zzy
2025-11-22 15:08:49 +08:00
parent d6941e1d2f
commit 63f6f13883
8 changed files with 270 additions and 134 deletions

View File

@@ -1,6 +1,5 @@
[package]
name = "smcc_lex"
version = "0.1.0"
dependencies = [
{ name = "libcore", path = "../../runtime/libcore" },
]
dependencies = [{ name = "libcore", path = "../../runtime/libcore" }]

View File

@@ -1,14 +1,11 @@
[package]
name = "libcore"
version = "0.1.0"
default_features = [
"std_impl",
]
features = [
"std_impl",
]
default_features = ["std_impl"]
features = ["std_impl"]
dependencies = [
# TODO define some to disable stdio for self-contained build
{ name = "log", path = "../log" }
{ name = "log", path = "../log" },
]

View File

@@ -6,6 +6,7 @@
#include "core_mem.h"
#include "core_str.h"
struct core_stream;
typedef struct core_stream core_stream_t;
#define core_stream_eof (-1)

10
tools/cbuild/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv

9
tools/cbuild/README.md Normal file
View File

@@ -0,0 +1,9 @@
# cbuild
## dependencies
[uv](https://docs.astral.sh/uv/)
一个用 Rust 编写的极速 Python 包和项目管理工具。
## install
使用 `uv pip install -e .``cbuild` 添加到 python 环境

View File

@@ -8,10 +8,49 @@ from dataclasses import dataclass, field
from enum import Enum, auto
import logging
import hashlib
from graphlib import TopologicalSorter
import shutil
import argparse
import sys
from typing import Self
import time
import sys
class ColorFormatter(logging.Formatter):
"""颜色格式化"""
# 基础颜色定义对应ANSI颜色码
cyan = "\x1b[36m" # DEBUG - 青色
green = "\x1b[32m" # INFO - 绿色
blue = "\x1b[34m" # TRACE - 蓝色
yellow = "\x1b[33m" # WARN - 黄色
red = "\x1b[31m" # ERROR - 红色
line_red = "\x1b[31;4m" # FATAL - 红色加下划线
bold = "\x1b[1m" # 粗体
reset = "\x1b[0m" # 重置
fmt = "%(name)s - %(levelname)s: "
time = "%(asctime)s"
msg = "%(message)s"
file = " - (%(filename)s:%(lineno)d)"
FORMATS = {
logging.DEBUG: bold + cyan + fmt + reset + msg + file,
logging.INFO: bold + green + fmt + reset + msg,
logging.WARNING: bold + yellow + fmt + reset + msg,
logging.ERROR: bold + red + fmt + reset + msg,
logging.CRITICAL: bold + line_red + fmt + reset + msg + file,
logging.FATAL: bold + line_red + fmt + reset + msg + file,
}
def format(self, record):
log_fmt = self.FORMATS.get(record.levelno)
formatter = logging.Formatter(log_fmt)
return formatter.format(record)
logger = logging.getLogger(__name__)
console_handler = logging.StreamHandler()
color_formatter = ColorFormatter()
console_handler.setFormatter(color_formatter)
logger.addHandler(console_handler)
@dataclass
class Dependency:
@@ -51,7 +90,7 @@ class PackageConfig:
Returns:
str: 包的名称如果未定义则返回空字符串
"""
return self.config.get("name", "")
return self.config.get("name", "[unnamed package]")
@property
def version(self) -> str:
@@ -60,7 +99,7 @@ class PackageConfig:
Returns:
str: 包的版本号如果未定义则返回空字符串
"""
return self.config.get("version", "")
return self.config.get("version", "0.0.0")
@property
def default_features(self) -> list[str]:
@@ -133,6 +172,68 @@ class PackageConfig:
"""
return self.config.get("description", "")
class DependencyResolver:
"""依赖解析器"""
def __init__(self, root_package: PackageConfig):
self.root_package = root_package
self.resolved_deps: dict[str, PackageConfig] = {}
self.deps_graph: TopologicalSorter = TopologicalSorter()
self._resolved = False
def resolve(self) -> dict[str, PackageConfig]:
"""解析所有依赖"""
if self._resolved:
return self.resolved_deps
# 使用广度优先搜索解析所有依赖
queue = [self.root_package]
visited = set()
while queue:
pkg_config = queue.pop(0)
pkg_name = pkg_config.name
if pkg_name in visited:
continue
visited.add(pkg_name)
# 创建构建上下文
if pkg_name not in self.resolved_deps:
self.resolved_deps[pkg_name] = pkg_config
# 解析直接依赖
for dep in pkg_config.dependencies:
dep_path = Path(pkg_config.path / dep.path)
dep_config = PackageConfig(dep_path)
dep_name = dep_config.name
# 添加依赖图关系
self.deps_graph.add(pkg_name, dep_name)
# 如果是新依赖,加入队列继续解析
if dep_name not in visited and dep_name not in self.resolved_deps:
queue.append(dep_config)
self.deps_graph.prepare()
self._resolved = True
return self.resolved_deps
def get_sorted_dependencies(self) -> list[CBuildContext]:
"""获取按拓扑排序的依赖列表(不包括根包)"""
if not self._resolved:
self.resolve()
sorted_names = list(self.deps_graph.static_order())
return [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]:
"""获取所有上下文(包括根包)"""
if not self._resolved:
self.resolve()
return [CBuildContext(resolved_dep) for resolved_dep in self.resolved_deps.values()]
@dataclass(frozen=True)
class BuildPath:
"""path"""
@@ -141,6 +242,7 @@ class BuildPath:
output_path: Path
object_path: Path
src_path: Path
inc_path: Path
default_bin_path: Path
default_lib_path: Path
@@ -159,6 +261,7 @@ class BuildPath:
return cls(
root_path = root_path,
tests_path = root_path / "tests",
inc_path = root_path / "include",
output_path = output_path ,
object_path = output_path / "obj",
src_path = src_path,
@@ -190,11 +293,12 @@ class CBuildContext:
self.relative_path = Path.cwd()
self.path = BuildPath.from_root_path(package.path) \
if build_path is None else build_path
self.deps_resolver = DependencyResolver(package)
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]
return [Path(p).relative_to(self.relative_path, walk_up=True) for p in unique_paths]
@property
def sources_path(self) -> list[Path]:
@@ -214,18 +318,16 @@ class CBuildContext:
@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)
deps = self.deps_resolver.get_all_contexts()
includes = [inc.path.inc_path for inc in deps if inc.path.inc_path.exists()]
return self._path_collection(includes)
def get_targets(self) -> list[Target]:
"""获取所有构建目标"""
targets = []
# TODO
ext = ".exe"
execute_ext = ""
if sys.platform == "win32":
execute_ext = ".exe"
# 添加主可执行文件目标
if self.path.default_bin_path.exists():
@@ -234,7 +336,7 @@ class CBuildContext:
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}",
output=self.path.output_path / f"{self.package.name}{execute_ext}",
))
# 添加静态库目标
@@ -255,7 +357,7 @@ class CBuildContext:
type=TargetType.TEST_EXECUTABLE,
source=test_source,
object=self.get_object_path(test_source),
output=self.path.output_path / f"{test_source.stem}{ext}"
output=self.path.output_path / f"{test_source.stem}{execute_ext}"
))
return targets
@@ -273,9 +375,7 @@ class CBuildContext:
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())
deps = self.deps_resolver.get_all_contexts()
objs = []
for dep in deps:
objs.extend(dep.get_self_build_objs())
@@ -312,19 +412,6 @@ class CBuildContext:
object_path.parent.mkdir(parents=True, exist_ok=True)
return object_path
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:
"""缓存条目"""
@@ -355,12 +442,12 @@ class Compiler(ABC):
def cmd(self, cmd: str | list[str]):
"""执行命令并处理错误输出"""
try:
logging.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)
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)
logger.error("command running error: [%d]`%s`", e.returncode, cmd_str)
raise
class ClangCompiler(Compiler):
@@ -403,9 +490,6 @@ class GccCompiler(Compiler):
class CPackageBuilder:
"""包构建器"""
# TODO
EXT = ".exe"
def __init__(self, package_path: Path, compiler: Compiler):
self.package = PackageConfig(package_path)
self.context = CBuildContext(self.package, None)
@@ -417,17 +501,35 @@ class CPackageBuilder:
# 确保输出目录存在
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):
def _format_size(self, size_bytes):
"""格式化文件大小显示"""
if size_bytes == 0:
return "0B"
size_names = ["B", "KB", "MB", "GB"]
i = 0
while size_bytes >= 1024 and i < len(size_names) - 1:
size_bytes /= 1024.0
i += 1
if i == 0:
return f"{int(size_bytes)}{size_names[i]}"
return f"{size_bytes:.1f}{size_names[i]}"
def build(self, targets_type: list[TargetType]):
"""构建包"""
start_time = time.time()
object_files = self._build_ctx()
targets = self.context.get_targets()
for target in targets:
if target.type not in targets_type:
continue
match target.type:
case TargetType.MAIN_EXECUTABLE:
self.compiler.compile(target.source, target.object, self.context.includes,
@@ -441,25 +543,61 @@ class CPackageBuilder:
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...")
# 计算构建时间
elapsed_time = time.time() - start_time
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",
time_str)
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")
logger.error("not have target to run")
subprocess.run(targets[0].output, check=False)
def clean(self):
"""清理构建产物"""
# 统计要删除的文件数量和总大小
cleaned_files = 0
total_size = 0
# 删除整个输出目录,这会清除所有构建产物
if self.context.path.output_path.exists():
import shutil
# 遍历目录统计文件数量和大小
for file_path in self.context.path.output_path.rglob("*"):
if file_path.is_file():
cleaned_files += 1
total_size += file_path.stat().st_size
shutil.rmtree(self.context.path.output_path)
print(f"已清理构建目录: {self.context.path.output_path}")
logger.info("已清理构建目录: %s",
self.context.path.output_path)
logger.info("Cleaned %s files, Size %s",
cleaned_files, self._format_size(total_size))
else:
print("没有找到构建目录,无需清理")
logger.info("没有找到构建目录,无需清理")
def tree(self):
"""打印构建树 - 仿照 cargo 的风格"""
# TODO 递归显示
print("dependency tree:")
# 解析依赖
resolver = self.context.deps_resolver
resolver.resolve()
# 获取根包
root_pkg = resolver.root_package
print(f"{root_pkg.name} v{root_pkg.version}")
# 获取依赖图的节点和边
# 这里我们简化处理,只显示直接依赖
for pkg_config in resolver.resolved_deps.values():
if pkg_config.name != root_pkg.name: # 跳过根包
indent = "├── " if pkg_config.name != list(resolver.resolved_deps.keys())[-1]\
else "└── "
print(f"{indent}{pkg_config.name} v{pkg_config.version}")
def tests(self):
"""运行测试"""
@@ -469,8 +607,8 @@ class CPackageBuilder:
failed = 0
for target in targets:
name = target.name
print(f"运行测试: {name}")
logging.debug("test run %s", target.output)
logger.info("运行测试: %s", name)
logger.debug("test run %s", target.output)
try:
result = subprocess.run(target.output,
check=True,
@@ -495,47 +633,67 @@ class CPackageBuilder:
failed += 1
print(f"\n测试结果: {passed} 通过, {failed} 失败")
def cmd(self, cmd: str):
"""执行命令"""
match cmd:
case "build":
self.build([
TargetType.MAIN_EXECUTABLE,
TargetType.TEST_EXECUTABLE,
TargetType.STATIC_LIBRARY,
TargetType.SHARED_LIBRARY,
TargetType.EXECUTABLE,
])
case "run":
bin_path = self.context.path.default_bin_path
if not bin_path.exists():
logger.error("%s not exist", bin_path)
return
self.build([TargetType.MAIN_EXECUTABLE])
self.run()
case "test":
self.build([TargetType.TEST_EXECUTABLE])
self.tests()
case "clean":
self.clean()
case "tree":
self.tree()
case _:
logger.error("unknown command: %s", cmd)
def main():
"""main"""
parser = argparse.ArgumentParser(description="Simple C Package Manager")
parser.add_argument("command", choices=["build", "run", "test", "clean"],
parser = argparse.ArgumentParser(description="Simple C Package Manager",
prog="cbuild")
parser.add_argument("command", choices=["build", "run", "test", "clean", "tree"],
help="Command to execute")
parser.add_argument("--verbose", "-v", action="store_true",
help="enable the logging to debug (defalut: false)")
parser.add_argument("--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()
package_path = Path(args.path)
package_path = Path.cwd() / Path(args.path)
if args.verbose:
logging.getLogger().setLevel(logging.NOTSET)
compiler = GccCompiler()
logger.setLevel(logging.DEBUG)
else:
logger.setLevel(logging.INFO)
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()
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)
match args.compiler:
case "gcc":
compiler = GccCompiler()
case "clang":
compiler = ClangCompiler()
case "smcc":
# TODO self compiler
raise ValueError("Invalid compiler")
case _:
raise ValueError("Invalid compiler")
builder = CPackageBuilder(package_path, compiler)
builder.cmd(args.command)
if __name__ == "__main__":
# builder = CPackageBuilder(Path("./runtime/libcore/"), ClangCompiler())

View File

@@ -0,0 +1,10 @@
[project]
name = "cbuild"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.14"
dependencies = []
[project.scripts]
cbuild = "cbuild:main"

48
wc.py
View File

@@ -1,48 +0,0 @@
"""统计目录下C/C++文件的行数(write by AI)"""
import os
def count_lines(file_path):
"""统计单个文件的代码行数"""
try:
with open(file_path, 'rb') as f: # 二进制模式读取避免编码问题
return sum(1 for _ in f)
except UnicodeDecodeError:
print(f"警告:无法解码文件 {file_path}(可能不是文本文件)")
return 0
except Exception as e:
print(f"读取 {file_path} 出错: {str(e)}")
return 0
def scan_files(directory, exclude_dirs=None):
"""扫描目录获取所有C/C++文件"""
if exclude_dirs is None:
exclude_dirs = ['.git', 'venv', '__pycache__'] # 默认排除的目录
c_files = []
for root, dirs, files in os.walk(directory):
# 跳过排除目录
dirs[:] = [d for d in dirs if d not in exclude_dirs]
for file in files:
if file.endswith(('.c', '.h')):
full_path = os.path.join(root, file)
c_files.append(full_path)
return c_files
def main():
"""main function"""
target_dir = input("请输入要扫描的目录路径(留空为当前目录): ") or '.'
files = scan_files(target_dir)
total_lines = 0
print("\n统计结果:")
for idx, file in enumerate(files, 1):
lines = count_lines(file)
total_lines += lines
print(f"{idx:4d}. {file} ({lines} 行)")
print(f"\n总计: {len(files)} 个C/C++文件,共 {total_lines} 行代码")
if __name__ == "__main__":
main()