From 9515afd2163bd9af719dd446c7d280c6899ad3dd Mon Sep 17 00:00:00 2001 From: bernard Date: Tue, 23 Dec 2025 21:43:22 +0800 Subject: [PATCH 1/2] Add ebuild (Embedded Build System) --- ebuild/README.md | 249 ++++++++++++++++ ebuild/__init__.py | 18 ++ ebuild/attach.py | 166 +++++++++++ ebuild/config.py | 253 ++++++++++++++++ ebuild/configs/aarch64_gcc.py | 39 +++ ebuild/configs/arm_gcc.py | 59 ++++ ebuild/configs/linux_gcc.py | 23 ++ ebuild/configs/riscv_gcc.py | 43 +++ ebuild/exporter.py | 148 +++++++++ ebuild/groups.py | 198 +++++++++++++ ebuild/helpers.py | 78 +++++ ebuild/kconfig.py | 201 +++++++++++++ ebuild/options.py | 61 ++++ ebuild/packages.py | 79 +++++ ebuild/system.py | 363 +++++++++++++++++++++++ ebuild/targets/__init__.py | 30 ++ ebuild/targets/keil.py | 544 ++++++++++++++++++++++++++++++++++ ebuild/targets/utils.py | 71 +++++ ebuild/toolchain.py | 260 ++++++++++++++++ 19 files changed, 2883 insertions(+) create mode 100644 ebuild/README.md create mode 100644 ebuild/__init__.py create mode 100644 ebuild/attach.py create mode 100644 ebuild/config.py create mode 100644 ebuild/configs/aarch64_gcc.py create mode 100644 ebuild/configs/arm_gcc.py create mode 100644 ebuild/configs/linux_gcc.py create mode 100644 ebuild/configs/riscv_gcc.py create mode 100644 ebuild/exporter.py create mode 100644 ebuild/groups.py create mode 100644 ebuild/helpers.py create mode 100644 ebuild/kconfig.py create mode 100644 ebuild/options.py create mode 100644 ebuild/packages.py create mode 100644 ebuild/system.py create mode 100644 ebuild/targets/__init__.py create mode 100644 ebuild/targets/keil.py create mode 100644 ebuild/targets/utils.py create mode 100644 ebuild/toolchain.py diff --git a/ebuild/README.md b/ebuild/README.md new file mode 100644 index 0000000..a9b3bf2 --- /dev/null +++ b/ebuild/README.md @@ -0,0 +1,249 @@ +# EBuild 嵌入式构建系统 + +EBuild (Embedded Build System) 是一个基于 SCons 的统一嵌入式构建框架,为嵌入式项目提供配置管理、工具链配置、组件注册和工程导出等一站式解决方案。 + +## 特性 + +- **统一构建入口** - 通过 `PrepareBuilding`/`DoBuilding` 简化构建流程 +- **灵活配置管理** - 支持 menuconfig 图形配置和 attachconfig 快速配置方案 +- **智能工具链检测** - 自动从 `~/.env` 或项目配置检测 GCC 工具链路径 +- **组件化构建** - 支持 `DefineGroup` 和 `package.json` 两种组件组织方式 +- **多 IDE 支持** - 可导出 VS Code、CMake、Keil MDK 工程文件 +- **条件编译** - 基于配置项的依赖检查和条件编译支持 + +## 快速开始 + +### 基本使用 + +在项目根目录的 `SConstruct` 中: + +```python +from ebuild import PrepareBuilding, DoBuilding + +# 准备构建环境 +env = Environment() +build = PrepareBuilding(env, project_root='.', proj_config=proj_config) + +# 注册组件(在 SConscript 中) +# env.DefineGroup('my_component', ['src/*.c'], depend=['CONFIG_MY_FEATURE']) + +# 执行构建 +DoBuilding(env, 'target.elf') +``` + +### 配置项目 + +```bash +# 打开图形配置界面 +scons --menuconfig + +# 查看可用的 attachconfig 配置方案 +scons --attach=? + +# 应用某个配置方案 +scons --attach=stm32f103-basic +``` + +### 构建项目 + +```bash +# 默认构建 +scons + +# 清理构建 +scons -c + +# 详细输出 +scons --verbose +``` + +### 导出工程 + +```bash +# 导出 VS Code 工程 +scons --target=vscode + +# 导出 CMake 工程 +scons --target=cmake + +# 导出 Keil MDK 工程 +scons --target=mdk5 +``` + +## 核心概念 + +### 配置文件 + +- **proj_config.py** - 项目配置,定义工具链、MCU 系列、工程名称等 +- **proj_config.h** - 由 menuconfig 生成的 C 头文件,包含所有配置宏 +- **Kconfig** - menuconfig 配置描述文件 + +### 组件注册 + +EBuild 提供两种组件组织方式: + +#### 1. DefineGroup + +```python +# 在 SConscript 中 +src = ['init.c', 'driver.c'] +depend = ['CONFIG_USE_DRIVER'] +CPPPATH = ['./include'] +CPPDEFINES = ['DEBUG_MODE=1'] + +env.DefineGroup('my_component', src, depend=depend, CPPPATH=CPPPATH, CPPDEFINES=CPPDEFINES) +``` + +#### 2. package.json + +```json +{ + "type": "rt-thread-component", + "name": "my_component", + "dependencies": ["CONFIG_USE_DRIVER"], + "defines": ["DEBUG_MODE=1"], + "sources": [ + { + "dependencies": [], + "includes": ["./include"], + "files": ["*.c"] + } + ] +} +``` + +### 工具链配置 + +EBuild 支持多种工具链配置方式: + +1. **自动检测** - 从 `~/.env/tools/scripts/packages` 自动检测 +2. **proj_config.py** - 设置 `EXEC_PATH` 和 `CC_PREFIX` +3. **命令行参数** - 使用 `--cross-compile`、`--cpu` 等 + +```python +# proj_config.py 示例 +TOOLCHAIN_CONFIG = { + 'CC_PREFIX': 'arm-none-eabi-', + 'EXEC_PATH': '/opt/gcc-arm-none-eabi/bin', + 'MCU_SERIES': { + 'CONFIG_STM32F103': { + 'cpu': 'cortex-m3', + 'link_script': 'linkstm32f103xe.ld', + 'bin': 'build/stm32f103.bin' + } + }, + 'BUILD': 'release' # or 'debug' +} +``` + +## 命令行选项 + +| 选项 | 说明 | +|------|------| +| `--target=TYPE` | 导出工程:mdk4/mdk5/cmake/vscode | +| `--menuconfig` | 打开配置菜单 | +| `--attach=NAME` | 应用 attachconfig 方案(`?` 查看列表,`default` 恢复) | +| `--verbose` | 显示完整编译命令 | +| `--cross-compile=PREFIX` | 交叉编译器前缀 | +| `--cpu=CPU` | 目标 CPU 类型 | +| `--fpu=FPU` | FPU 类型 | +| `--float-abi=ABI` | 浮点 ABI | + +## 支持的工具链 + +| 架构 | 配置模块 | +|------|----------| +| ARM | `ebuild.configs.arm_gcc` | +| AArch64 | `ebuild.configs.aarch64_gcc` | +| RISC-V | `ebuild.configs.riscv_gcc` | +| Linux | `ebuild.configs.linux_gcc` | + +## 项目结构 + +``` +project/ +├── SConstruct # 主构建脚本 +├── proj_config.py # 项目配置 +├── Kconfig # 配置菜单定义 +├── proj_config.h # 生成的配置头文件 +├── .config # menuconfig 配置输出 +├── SConscript # 组件注册脚本 +├── .vscode/ # VS Code 配置(导出后) +├── CMakeLists.txt # CMake 配置(导出后) +└── *.uvprojx # Keil 工程(导出后) +``` + +## 高级功能 + +### AttachConfig 快速配置 + +AttachConfig 允许预定义常用配置方案,快速切换: + +```bash +# 查看可用方案 +scons --attach=? + +# 应用方案 +scons --attach=nrf52832-peripheral + +# 恢复默认配置 +scons --attach=default +``` + +### 条件编译 + +```python +# 仅当依赖满足时才编译该组件 +env.DefineGroup('feature_x', ['feature_x.c'], depend=['CONFIG_FEATURE_X']) +``` + +### 包构建 + +```python +# 扫描并构建当前目录下的 package.json +env.BuildPackage('.') + +# 构建指定路径的包 +env.BuildPackage('path/to/package') +``` + +### 桥接模式 + +```python +# 自动扫描子目录并执行其中的 SConscript +groups = env.Bridge() +``` + +## API 参考 + +### 核心函数 + +- `PrepareBuilding(env, project_root, proj_config)` - 初始化构建系统 +- `DoBuilding(env, target, objs)` - 执行构建或导出 + +### SCons 环境方法 + +- `env.DefineGroup(name, src, depend, **kwargs)` - 注册组件组 +- `env.BuildPackage(path)` - 构建 package.json 组件 +- `env.Bridge()` - 桥接子目录组件 +- `env.GetDepend(dep)` - 检查配置依赖 +- `env.GlobFiles(pattern)` - 获取文件列表 +- `env.SrcRemove(src, remove)` - 从源文件列表中移除 +- `env.DoBuilding(target, objs)` - 执行构建 + +## 安装依赖 + +```bash +# 安装 SCons +pip install scons + +# 安装 kconfiglib(用于 menuconfig) +pip install kconfiglib + +# 安装 PyYAML(用于 attachconfig) +pip install pyyaml +``` + +## 许可证 + +本项目为内部构建工具,遵循项目许可证。 diff --git a/ebuild/__init__.py b/ebuild/__init__.py new file mode 100644 index 0000000..5b5a679 --- /dev/null +++ b/ebuild/__init__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +"""Ebuild package entrypoint.""" + +from .system import BuildSystem, prepare + + +def PrepareBuilding(env, project_root=None, proj_config=None): + return prepare(env, project_root, config_module=proj_config) + + +def DoBuilding(env, target, objs=None): + build = getattr(env, '_BuildSystem', None) or BuildSystem.current() + if not build: + raise RuntimeError("BuildSystem not initialized.") + return build.do_building(target, objs) + + +__all__ = ['BuildSystem', 'prepare', 'PrepareBuilding', 'DoBuilding'] diff --git a/ebuild/attach.py b/ebuild/attach.py new file mode 100644 index 0000000..98cb292 --- /dev/null +++ b/ebuild/attach.py @@ -0,0 +1,166 @@ +# -*- coding: utf-8 -*- +"""Attachconfig support for SCons --attach.""" + +from __future__ import annotations + +import os +import shutil +from dataclasses import dataclass +from typing import Dict, List, Optional + +from SCons.Script import GetOption + +from . import config +from .kconfig import defconfig + + +@dataclass +class AttachConfigPaths: + project_root: str + config_header: str = config.CONFIG_HEADER + + @property + def config_file(self) -> str: + return os.path.join(self.project_root, '.config') + + @property + def config_backup(self) -> str: + return self.config_file + '.origin' + + @property + def config_header_file(self) -> str: + return os.path.join(self.project_root, self.config_header) + + @property + def config_header_backup(self) -> str: + return self.config_header_file + '.origin' + + @property + def attach_dir(self) -> str: + return os.path.join(self.project_root, '.ci', 'attachconfig') + + +class AttachConfigRepository: + def __init__(self, attach_dir: str) -> None: + self.attach_dir = attach_dir + self.projects: Dict[str, Dict] = {} + + def load(self) -> None: + if not os.path.isdir(self.attach_dir): + return + for root, _, files in os.walk(self.attach_dir): + for filename in files: + if not filename.endswith('attachconfig.yml'): + continue + path = os.path.join(root, filename) + content = self._load_yaml(path) + if isinstance(content, dict): + self.projects.update(content) + + def list_names(self) -> List[str]: + return sorted([name for name, details in self.projects.items() if details and details.get('kconfig')]) + + def collect_kconfig(self, name: str) -> List[str]: + return self._collect_kconfig([name]) + + def _collect_kconfig(self, names: List[str], seen: Optional[set] = None) -> List[str]: + if seen is None: + seen = set() + lines: List[str] = [] + for item in names: + if item in seen: + print(f"::error::There are some problems with attachconfig depend: {item}") + continue + seen.add(item) + detail = self.projects.get(item) + if not detail: + continue + deps = detail.get('depends') or [] + if deps: + lines.extend(self._collect_kconfig(deps, seen)) + if detail.get('kconfig'): + lines.extend(detail.get('kconfig')) + return lines + + @staticmethod + def _load_yaml(path: str): + try: + import yaml + except ImportError as exc: + raise RuntimeError("Attachconfig requires PyYAML, please install pyyaml.") from exc + + with open(path, 'r', encoding='utf-8') as handle: + return yaml.safe_load(handle) + + +class AttachConfigManager: + def __init__(self, project_root: str) -> None: + self.paths = AttachConfigPaths(os.path.abspath(project_root)) + self.repo = AttachConfigRepository(self.paths.attach_dir) + + def run(self) -> None: + option = GetOption('attach') + if not option: + return + self.repo.load() + + if option == '?': + self._print_available() + return + if option == 'default': + self._restore_default() + return + self._apply(option) + + def _print_available(self) -> None: + names = self.repo.list_names() + if not names: + print("AttachConfig empty.") + return + print("\033[32m✅ AttachConfig has: \033[0m") + prefix = names[0].split('.', 1)[0] + for name in names: + section = name.split('.', 1)[0] + if section != prefix: + print("\033[42m \033[30m------" + section + "------\033[0m") + prefix = section + print(name) + + def _restore_default(self) -> None: + restored = False + if os.path.exists(self.paths.config_backup): + shutil.copyfile(self.paths.config_backup, self.paths.config_file) + os.remove(self.paths.config_backup) + restored = True + if os.path.exists(self.paths.config_header_backup): + shutil.copyfile(self.paths.config_header_backup, self.paths.config_header_file) + os.remove(self.paths.config_header_backup) + restored = True + if restored: + print(f"\033[32m✅ Default .config and {self.paths.config_header} recovery success!\033[0m") + else: + print("AttachConfig: no backup files to restore.") + + def _apply(self, name: str) -> None: + lines = self.repo.collect_kconfig(name) + if not lines: + print("❌\033[31m Without this AttachConfig:" + name + "\033[0m") + return + + if not os.path.exists(self.paths.config_backup): + shutil.copyfile(self.paths.config_file, self.paths.config_backup) + if not os.path.exists(self.paths.config_header_backup): + if os.path.exists(self.paths.config_header_file): + shutil.copyfile(self.paths.config_header_file, self.paths.config_header_backup) + + with open(self.paths.config_file, 'a', encoding='utf-8') as destination: + for line in lines: + destination.write(line + '\n') + + defconfig(self.paths.project_root) + print("\033[32m✅ AttachConfig add success!\033[0m") + + +def GenAttachConfigProject(project_root: Optional[str] = None) -> None: + root = project_root or os.getcwd() + AttachConfigManager(root).run() diff --git a/ebuild/config.py b/ebuild/config.py new file mode 100644 index 0000000..6b43c08 --- /dev/null +++ b/ebuild/config.py @@ -0,0 +1,253 @@ +# -*- coding: utf-8 -*- +"""Shared defaults for the build scripts.""" + +import os +from dataclasses import dataclass +from enum import Enum +from typing import Any, Dict, List, Optional, Union + +from SCons import cpp as scons_cpp +DEFAULT_PROJECT_NAME = "project" +PROJECT_NAME = DEFAULT_PROJECT_NAME + +DEFAULT_TARGET_NAME = f"{PROJECT_NAME}.elf" +TARGET_NAME = DEFAULT_TARGET_NAME + +CONFIG_HEADER = "proj_config.h" + + +def resolve_project_root(explicit_root=None): + root = explicit_root or os.getcwd() + return os.path.abspath(root) + + +class ConfigType(Enum): + """Configuration value types.""" + BOOLEAN = "boolean" + INTEGER = "integer" + STRING = "string" + UNDEFINED = "undefined" + + +@dataclass +class ConfigOption: + """Configuration option with metadata.""" + name: str + value: Any + type: ConfigType + line_number: int = 0 + comment: str = "" + + def as_bool(self) -> bool: + """Get value as boolean.""" + if self.type == ConfigType.BOOLEAN: + return bool(self.value) + if self.type == ConfigType.INTEGER: + return self.value != 0 + if self.type == ConfigType.STRING: + return bool(self.value) + return False + + def as_int(self) -> int: + """Get value as integer.""" + if self.type == ConfigType.INTEGER: + return int(self.value) + if self.type == ConfigType.BOOLEAN: + return 1 if self.value else 0 + if self.type == ConfigType.STRING: + try: + return int(self.value) + except ValueError: + return 0 + return 0 + + def as_str(self) -> str: + """Get value as string.""" + if self.type == ConfigType.STRING: + return self.value + return str(self.value) + + +class ConfigParser: + """Parser for config header files.""" + + def __init__(self): + self.options: Dict[str, ConfigOption] = {} + + def parse_file(self, filepath: str) -> Dict[str, ConfigOption]: + if not os.path.exists(filepath): + raise FileNotFoundError(f"Configuration file not found: {filepath}") + + with open(filepath, 'r', encoding='utf-8') as handle: + content = handle.read() + + return self.parse_content(content) + + def parse_content(self, content: str) -> Dict[str, ConfigOption]: + clean_content = self._strip_comments(content) + preprocessor = _ConfigPreProcessor(self) + preprocessor.process_contents(clean_content) + return self.options + + def _parse_value(self, value: str) -> tuple: + if not value or value == '1': + return (True, ConfigType.BOOLEAN) + + try: + return (int(value, 0), ConfigType.INTEGER) + except ValueError: + pass + + if value.startswith('"') and value.endswith('"'): + return (value[1:-1], ConfigType.STRING) + + return (value, ConfigType.STRING) + + @staticmethod + def _strip_comments(content: str) -> str: + result: List[str] = [] + index = 0 + length = len(content) + in_block = False + in_line = False + in_string = False + + while index < length: + char = content[index] + next_char = content[index + 1] if index + 1 < length else '' + + if in_line: + if char == '\n': + in_line = False + result.append(char) + index += 1 + continue + + if in_block: + if char == '*' and next_char == '/': + in_block = False + index += 2 + continue + if char == '\n': + result.append(char) + index += 1 + continue + + if in_string: + result.append(char) + if char == '\\' and next_char: + result.append(next_char) + index += 2 + continue + if char == '"': + in_string = False + index += 1 + continue + + if char == '"': + in_string = True + result.append(char) + index += 1 + continue + + if char == '/' and next_char == '/': + in_line = True + index += 2 + continue + + if char == '/' and next_char == '*': + in_block = True + index += 2 + continue + + result.append(char) + index += 1 + + return ''.join(result) + + +class _ConfigPreProcessor(scons_cpp.PreProcessor): + def __init__(self, parser: ConfigParser) -> None: + super().__init__(depth=0) + self._parser = parser + + def do_define(self, t) -> None: + _, name, args, expansion = t + if args: + super().do_define(t) + self._parser.options[name] = ConfigOption( + name=name, + value=None, + type=ConfigType.UNDEFINED, + ) + return + + raw_value = (expansion or '').strip() + parsed_value, value_type = self._parser._parse_value(raw_value) + self.cpp_namespace[name] = parsed_value + self._parser.options[name] = ConfigOption( + name=name, + value=parsed_value, + type=value_type, + ) + + def do_undef(self, t) -> None: + name = t[1] + self._parser.options.pop(name, None) + self.cpp_namespace.pop(name, None) + + def do_include(self, t) -> None: + return + + do_include_next = do_include + do_import = do_include + + +class ConfigManager: + """Configuration manager for build system.""" + + def __init__(self): + self.parser = ConfigParser() + self.options: Dict[str, ConfigOption] = {} + self.cache: Dict[str, bool] = {} + + def load_from_file(self, filepath: str) -> None: + self.options = self.parser.parse_file(filepath) + self.cache.clear() + + def get_option(self, name: str) -> Optional[ConfigOption]: + return self.options.get(name) + + def get_all_options(self) -> Dict[str, Any]: + return {name: opt.value for name, opt in self.options.items()} + + def get_value(self, name: str, default: Any = None) -> Any: + option = self.options.get(name) + if option: + return option.value + return default + + def get_dependency(self, depend: Union[str, List[str]]) -> bool: + if not depend: + return True + + if isinstance(depend, str): + depend = [depend] + + cache_key = ','.join(sorted(depend)) + if cache_key in self.cache: + return self.cache[cache_key] + + result = all(self._check_single_dependency(item) for item in depend) + self.cache[cache_key] = result + return result + + def _check_single_dependency(self, name: str) -> bool: + option = self.options.get(name) + if not option: + return False + + if option.type == ConfigType.INTEGER: + return option.value != 0 + + return option.as_bool() diff --git a/ebuild/configs/aarch64_gcc.py b/ebuild/configs/aarch64_gcc.py new file mode 100644 index 0000000..b0a9274 --- /dev/null +++ b/ebuild/configs/aarch64_gcc.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +"""Default config template for aarch64-none-eabi-gcc (data only).""" + +CROSS_TOOL = "gcc" +EXEC_PATH = "" +CC_PREFIX = "aarch64-none-eabi-" + +TOOLCHAIN_COMMANDS = { + "CC": "gcc", + "CXX": "g++", + "AS": "gcc", + "AR": "ar", + "LINK": "gcc", + "SIZE": "size", + "OBJDUMP": "objdump", + "OBJCOPY": "objcopy", +} + +BASE_CFLAGS = ["-std=c99"] +BASE_CXXFLAGS = ["-std=c99"] +BASE_ASFLAGS = ["-c", "-x", "assembler-with-cpp"] +BASE_LINKFLAGS = ["--specs=nosys.specs"] +BASE_DEFINES = ["gcc"] + +# Architecture profiles (march/mcpu). +CPU_PROFILES = { + "armv8-a": ["-march=armv8-a"], + "cortex-a53": ["-mcpu=cortex-a53"], + "cortex-a55": ["-mcpu=cortex-a55"], + "cortex-a57": ["-mcpu=cortex-a57"], +} + +ABI_PROFILES = { + "lp64": [], + "ilp32": ["-mabi=ilp32"], +} + +DEFAULT_CPU = "armv8-a" +DEFAULT_ABI = "lp64" diff --git a/ebuild/configs/arm_gcc.py b/ebuild/configs/arm_gcc.py new file mode 100644 index 0000000..95bd8a9 --- /dev/null +++ b/ebuild/configs/arm_gcc.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +"""Default config template for arm-none-eabi-gcc (data only).""" + +CROSS_TOOL = "gcc" +EXEC_PATH = "" +CC_PREFIX = "arm-none-eabi-" + +TOOLCHAIN_COMMANDS = { + "CC": "gcc", + "CXX": "g++", + "AS": "gcc", + "AR": "ar", + "LINK": "gcc", + "SIZE": "size", + "OBJDUMP": "objdump", + "OBJCOPY": "objcopy", +} + +BASE_CFLAGS = ["-std=c99"] +BASE_CXXFLAGS = ["-std=c99"] +BASE_ASFLAGS = ["-c", "-x", "assembler-with-cpp"] +BASE_LINKFLAGS = ["--specs=nano.specs"] +BASE_DEFINES = ["gcc"] + +# Core profiles (cpu + instruction set). +CPU_PROFILES = { + "cortex-m0": ["-mcpu=cortex-m0", "-mthumb"], + "cortex-m3": ["-mcpu=cortex-m3", "-mthumb"], + "cortex-m4": ["-mcpu=cortex-m4", "-mthumb"], + "cortex-m7": ["-mcpu=cortex-m7", "-mthumb"], + "cortex-m33": ["-mcpu=cortex-m33", "-mthumb"], + "cortex-m85": ["-mcpu=cortex-m85", "-mthumb"], + "cortex-r5": ["-mcpu=cortex-r5", "-marm"], + "cortex-r5f": ["-mcpu=cortex-r5", "-marm"], + "cortex-r52": ["-mcpu=cortex-r52", "-marm"], + "cortex-a5": ["-mcpu=cortex-a5", "-marm"], + "cortex-a7": ["-mcpu=cortex-a7", "-marm"], + "cortex-a9": ["-mcpu=cortex-a9", "-marm"], +} + +FPU_PROFILES = { + "none": [], + "fpv4-sp-d16": ["-mfpu=fpv4-sp-d16"], + "fpv4-d16": ["-mfpu=fpv4-d16"], + "fpv5-sp-d16": ["-mfpu=fpv5-sp-d16"], + "fpv5-d16": ["-mfpu=fpv5-d16"], + "neon-vfpv4": ["-mfpu=neon-vfpv4"], + "neon-fp-armv8": ["-mfpu=neon-fp-armv8"], +} + +ABI_PROFILES = { + "soft": ["-mfloat-abi=soft"], + "softfp": ["-mfloat-abi=softfp"], + "hard": ["-mfloat-abi=hard"], +} + +DEFAULT_CPU = "cortex-m4" +DEFAULT_FPU = "none" +DEFAULT_ABI = "soft" diff --git a/ebuild/configs/linux_gcc.py b/ebuild/configs/linux_gcc.py new file mode 100644 index 0000000..4e67beb --- /dev/null +++ b/ebuild/configs/linux_gcc.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +"""Default config template for Linux native GCC (data only).""" + +CROSS_TOOL = "gcc" +EXEC_PATH = "" +CC_PREFIX = "" + +TOOLCHAIN_COMMANDS = { + "CC": "gcc", + "CXX": "g++", + "AS": "gcc", + "AR": "ar", + "LINK": "gcc", + "SIZE": "size", + "OBJDUMP": "objdump", + "OBJCOPY": "objcopy", +} + +BASE_CFLAGS = ["-std=c99"] +BASE_CXXFLAGS = ["-std=c99"] +BASE_ASFLAGS = ["-c", "-x", "assembler-with-cpp"] +BASE_LINKFLAGS = [] +BASE_DEFINES = ["gcc"] diff --git a/ebuild/configs/riscv_gcc.py b/ebuild/configs/riscv_gcc.py new file mode 100644 index 0000000..bcec230 --- /dev/null +++ b/ebuild/configs/riscv_gcc.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +"""Default config template for riscv-none-eabi-gcc (data only).""" + +CROSS_TOOL = "gcc" +EXEC_PATH = "" +CC_PREFIX = "riscv-none-eabi-" + +TOOLCHAIN_COMMANDS = { + "CC": "gcc", + "CXX": "g++", + "AS": "gcc", + "AR": "ar", + "LINK": "gcc", + "SIZE": "size", + "OBJDUMP": "objdump", + "OBJCOPY": "objcopy", +} + +BASE_CFLAGS = ["-std=c99"] +BASE_CXXFLAGS = ["-std=c99"] +BASE_ASFLAGS = ["-c", "-x", "assembler-with-cpp"] +BASE_LINKFLAGS = ["--specs=nosys.specs"] +BASE_DEFINES = ["gcc"] + +CPU_PROFILES = { + "rv32imac": ["-march=rv32imac"], + "rv32imafc": ["-march=rv32imafc"], + "rv64imac": ["-march=rv64imac"], + "rv64gc": ["-march=rv64gc"], +} + +ABI_PROFILES = { + "ilp32": ["-mabi=ilp32"], + "ilp32f": ["-mabi=ilp32f"], + "ilp32d": ["-mabi=ilp32d"], + "lp64": ["-mabi=lp64"], + "lp64f": ["-mabi=lp64f"], + "lp64d": ["-mabi=lp64d"], +} + +DEFAULT_CPU = "rv32imac" +DEFAULT_ABI = "ilp32" +DEFAULT_CODE_MODEL = ["-mcmodel=medany"] diff --git a/ebuild/exporter.py b/ebuild/exporter.py new file mode 100644 index 0000000..a2aa155 --- /dev/null +++ b/ebuild/exporter.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +"""Project exporter implementations.""" + +from __future__ import annotations + +import json +import os +from typing import Optional, TYPE_CHECKING + +from . import config + +if TYPE_CHECKING: + from .system import BuildSystem + + +class ProjectExporter: + def __init__(self, build: 'BuildSystem') -> None: + self.build = build + + def export(self, target: str, output_dir: Optional[str] = None) -> bool: + name = (target or '').lower() + output_dir = output_dir or self.build.project_root + + if name in ('vscode', 'vsc'): + return self._export_vscode(output_dir) + if name == 'cmake': + return self._export_cmake(output_dir) + if name in ('mdk', 'mdk4', 'mdk5', 'keil'): + return self._export_keil(name) + raise ValueError(f"Unknown project target: {target}") + + def _export_vscode(self, output_dir: str) -> bool: + info = self.build.registry.project_info() + vscode_dir = os.path.join(output_dir, '.vscode') + os.makedirs(vscode_dir, exist_ok=True) + + compiler = self.build.env.get('CC', '') if self.build.env else '' + vscode_config = { + "configurations": [ + { + "name": "Project", + "includePath": ["${workspaceFolder}/**"] + info['includes'], + "defines": info['defines'], + "compilerPath": compiler, + "cStandard": "c99", + "cppStandard": "c++11" + } + ], + "version": 4 + } + with open(os.path.join(vscode_dir, 'c_cpp_properties.json'), 'w', encoding='utf-8') as handle: + json.dump(vscode_config, handle, indent=4) + + tasks = { + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "type": "shell", + "command": "scons", + "problemMatcher": "$gcc", + "group": {"kind": "build", "isDefault": True} + }, + {"label": "clean", "type": "shell", "command": "scons -c", "problemMatcher": "$gcc"} + ] + } + with open(os.path.join(vscode_dir, 'tasks.json'), 'w', encoding='utf-8') as handle: + json.dump(tasks, handle, indent=4) + + launch = { + "version": "0.2.0", + "configurations": [ + { + "name": "Cortex Debug", + "type": "cortex-debug", + "request": "launch", + "cwd": "${workspaceRoot}", + "executable": "${workspaceRoot}/" + config.TARGET_NAME, + "servertype": "openocd", + "device": "STM32F103C8" + } + ] + } + with open(os.path.join(vscode_dir, 'launch.json'), 'w', encoding='utf-8') as handle: + json.dump(launch, handle, indent=4) + + settings = { + "files.associations": { + "*.h": "c", + "*.c": "c", + "*.cpp": "cpp", + "*.cc": "cpp", + "*.cxx": "cpp" + } + } + with open(os.path.join(vscode_dir, 'settings.json'), 'w', encoding='utf-8') as handle: + json.dump(settings, handle, indent=4) + + return True + + def _export_cmake(self, output_dir: str) -> bool: + info = self.build.registry.project_info() + lines = [ + "cmake_minimum_required(VERSION 3.10)", + "", + f"project({config.PROJECT_NAME} C CXX ASM)", + "set(CMAKE_C_STANDARD 99)", + "set(CMAKE_CXX_STANDARD 11)", + "", + "set(SOURCES" + ] + for src in info['sources']: + lines.append(f" {src}") + lines.extend([")", ""]) + + lines.append("add_executable(${PROJECT_NAME} ${SOURCES})") + lines.append("") + + if info['includes']: + lines.append("target_include_directories(${PROJECT_NAME} PRIVATE") + for inc in info['includes']: + lines.append(f" {inc}") + lines.extend([")", ""]) + + if info['defines']: + lines.append("target_compile_definitions(${PROJECT_NAME} PRIVATE") + for define in info['defines']: + lines.append(f" {define}") + lines.extend([")", ""]) + + if info['libs']: + lines.append("target_link_libraries(${PROJECT_NAME}") + for lib in info['libs']: + lines.append(f" {lib}") + lines.append(")") + + with open(os.path.join(output_dir, 'CMakeLists.txt'), 'w', encoding='utf-8') as handle: + handle.write('\n'.join(lines)) + + return True + + def _export_keil(self, target: str) -> bool: + from .targets.keil import KeilProjectGenerator + + groups = self.build.registry.project_info()['groups'] + generator = KeilProjectGenerator(self.build.env, config.PROJECT_NAME) + generator.generate(target, groups) + return True diff --git a/ebuild/groups.py b/ebuild/groups.py new file mode 100644 index 0000000..9798065 --- /dev/null +++ b/ebuild/groups.py @@ -0,0 +1,198 @@ +# -*- coding: utf-8 -*- +"""Group model and registry.""" + +from __future__ import annotations + +import os +from dataclasses import dataclass, field +from typing import Any, Dict, List + +from .helpers import defines_to_list, source_to_path + + +@dataclass +class Group: + name: str + sources: List[Any] + dependencies: List[str] = field(default_factory=list) + path: str = "" + + include_paths: List[str] = field(default_factory=list) + defines: Dict[str, str] = field(default_factory=dict) + libs: List[str] = field(default_factory=list) + lib_paths: List[str] = field(default_factory=list) + + cflags: List[str] = field(default_factory=list) + cxxflags: List[str] = field(default_factory=list) + asflags: List[str] = field(default_factory=list) + ldflags: List[str] = field(default_factory=list) + + local_include_paths: List[str] = field(default_factory=list) + local_defines: Dict[str, str] = field(default_factory=dict) + local_cflags: List[str] = field(default_factory=list) + local_cxxflags: List[str] = field(default_factory=list) + local_asflags: List[str] = field(default_factory=list) + + objects: List[Any] = field(default_factory=list) + + def build(self, env) -> List[Any]: + if not self.sources: + return [] + + build_env = env.Clone() if self._has_local_options() else env + self._apply_options(build_env) + + objects: List[Any] = [] + build_root = self._build_root(build_env) + for src in self.sources: + source_node = self._resolve_source_node(build_env, src) + if self._is_object_node(source_node, build_env): + objects.append(source_node) + continue + + obj_target = self._object_target(source_node, build_env, build_root) + obj = build_env.Object(target=obj_target, source=source_node) + objects.extend(obj if isinstance(obj, list) else [obj]) + + self.objects = objects + return objects + + def info(self) -> Dict[str, Any]: + def norm_paths(paths: List[str]) -> List[str]: + return [os.path.abspath(p) for p in paths] + + return { + 'name': self.name, + 'src': [self._source_path(src) for src in self.sources], + 'CPPPATH': norm_paths(self.include_paths), + 'CPPDEFINES': defines_to_list(self.defines), + 'LOCAL_CPPPATH': norm_paths(self.local_include_paths), + 'LOCAL_CPPDEFINES': defines_to_list(self.local_defines), + 'CFLAGS': ' '.join(self.cflags), + 'CCFLAGS': ' '.join(self.cflags), + 'CXXFLAGS': ' '.join(self.cxxflags), + 'ASFLAGS': ' '.join(self.asflags), + 'LINKFLAGS': ' '.join(self.ldflags), + 'LIBS': self.libs, + 'LIBPATH': self.lib_paths, + } + + def _has_local_options(self) -> bool: + return bool( + self.local_include_paths + or self.local_defines + or self.local_cflags + or self.local_cxxflags + or self.local_asflags + ) + + def _apply_options(self, env) -> None: + if self.include_paths: + env.AppendUnique(CPPPATH=[os.path.abspath(p) for p in self.include_paths]) + if self.defines: + env.AppendUnique(CPPDEFINES=self.defines) + if self.cflags: + env.AppendUnique(CFLAGS=self.cflags) + if self.cxxflags: + env.AppendUnique(CXXFLAGS=self.cxxflags) + if self.asflags: + env.AppendUnique(ASFLAGS=self.asflags) + if self.ldflags: + env.AppendUnique(LINKFLAGS=self.ldflags) + if self.libs: + env.AppendUnique(LIBS=self.libs) + if self.lib_paths: + env.AppendUnique(LIBPATH=[os.path.abspath(p) for p in self.lib_paths]) + + if self.local_include_paths: + env.AppendUnique(CPPPATH=[os.path.abspath(p) for p in self.local_include_paths]) + if self.local_defines: + env.AppendUnique(CPPDEFINES=self.local_defines) + if self.local_cflags: + env.AppendUnique(CFLAGS=self.local_cflags) + if self.local_cxxflags: + env.AppendUnique(CXXFLAGS=self.local_cxxflags) + if self.local_asflags: + env.AppendUnique(ASFLAGS=self.local_asflags) + + def _source_path(self, src: Any) -> str: + return source_to_path(src) + + @staticmethod + def _project_root(env) -> str: + if hasattr(env, 'GetProjectRoot'): + return os.path.abspath(env.GetProjectRoot()) + return os.path.abspath(os.getcwd()) + + def _build_root(self, env) -> str: + return os.path.join(self._project_root(env), 'build') + + def _resolve_source_node(self, env, src: Any) -> Any: + if isinstance(src, str): + if os.path.isabs(src): + return env.File(src) + base_dir = self.path or os.getcwd() + return env.File(os.path.join(base_dir, src)) + return src + + @staticmethod + def _is_object_node(src: Any, env) -> bool: + suffix = env.get('OBJSUFFIX', '.o') + path = source_to_path(src) + return path.endswith(suffix) + + def _object_target(self, src: Any, env, build_root: str) -> str: + src_path = source_to_path(src) + project_root = self._project_root(env) + rel_path = os.path.relpath(src_path, project_root) + if rel_path.startswith('..'): + rel_path = os.path.join('_external', os.path.basename(src_path)) + rel_path = os.path.normpath(rel_path) + rel_base, _ = os.path.splitext(rel_path) + suffix = env.get('OBJSUFFIX', '.o') + return os.path.join(build_root, rel_base + suffix) + + +class GroupRegistry: + def __init__(self) -> None: + self.groups: List[Group] = [] + + def add(self, group: Group) -> None: + self.groups.append(group) + + def merge_objects(self) -> List[Any]: + objects: List[Any] = [] + for group in self.groups: + objects.extend(group.objects) + return objects + + def project_info(self) -> Dict[str, Any]: + sources: List[str] = [] + includes: List[str] = [] + defines: List[str] = [] + libs: List[str] = [] + lib_paths: List[str] = [] + + def append_unique(items: List[str], values: List[str]) -> None: + for item in values: + if item not in items: + items.append(item) + + for group in self.groups: + info = group.info() + sources.extend(info['src']) + append_unique(includes, info['CPPPATH']) + append_unique(includes, info['LOCAL_CPPPATH']) + append_unique(defines, info['CPPDEFINES']) + append_unique(defines, info['LOCAL_CPPDEFINES']) + append_unique(libs, info['LIBS']) + append_unique(lib_paths, info['LIBPATH']) + + return { + 'groups': [group.info() for group in self.groups], + 'sources': sources, + 'includes': includes, + 'defines': defines, + 'libs': libs, + 'lib_paths': lib_paths, + } diff --git a/ebuild/helpers.py b/ebuild/helpers.py new file mode 100644 index 0000000..97695ca --- /dev/null +++ b/ebuild/helpers.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +"""Shared helpers for the build system.""" + +from __future__ import annotations + +import os +from typing import Any, Dict, List + + +def normalize_list(value: Any) -> List[Any]: + if value is None: + return [] + if isinstance(value, list): + return value + if isinstance(value, tuple): + return list(value) + return [value] + + +def split_flags(value: Any) -> List[str]: + if not value: + return [] + if isinstance(value, (list, tuple)): + return [str(item) for item in value if item] + if isinstance(value, str): + return [item for item in value.split() if item] + return [str(value)] + + +def merge_flags(*values: Any) -> List[str]: + flags: List[str] = [] + for value in values: + flags.extend(split_flags(value)) + return flags + + +def normalize_defines(value: Any) -> Dict[str, str]: + if value is None: + return {} + if isinstance(value, dict): + return {str(k): str(v) for k, v in value.items()} + if isinstance(value, tuple): + value = list(value) + if isinstance(value, list): + defines: Dict[str, str] = {} + for item in value: + if not item: + continue + text = str(item) + if '=' in text: + key, val = text.split('=', 1) + defines[key] = val + else: + defines[text] = '1' + return defines + text = str(value) + if '=' in text: + key, val = text.split('=', 1) + return {key: val} + return {text: '1'} + + +def defines_to_list(defines: Dict[str, str]) -> List[str]: + items = [] + for key, val in defines.items(): + if val == '1': + items.append(key) + else: + items.append(f"{key}={val}") + return items + + +def source_to_path(source: Any) -> str: + if hasattr(source, 'rfile'): + return source.rfile().abspath + if hasattr(source, 'abspath'): + return source.abspath + return str(source) diff --git a/ebuild/kconfig.py b/ebuild/kconfig.py new file mode 100644 index 0000000..c5ea3e8 --- /dev/null +++ b/ebuild/kconfig.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- +"""Kconfig helpers for menuconfig/defconfig.""" + +from __future__ import annotations + +import hashlib +import os +import re +import sys +from dataclasses import dataclass +from typing import Optional + +from . import config + + +@dataclass +class KconfigPaths: + project_root: str + config_header: str = config.CONFIG_HEADER + + @property + def config_file(self) -> str: + return os.path.join(self.project_root, '.config') + + @property + def config_old(self) -> str: + return os.path.join(self.project_root, '.config.old') + + def resolve_pkg_dir(self) -> Optional[str]: + pkg_dir = os.path.join(self.project_root, 'packages') + return pkg_dir if os.path.exists(pkg_dir) else None + + +class KconfigManager: + def __init__(self, project_root: str, config_header: str = config.CONFIG_HEADER) -> None: + self.paths = KconfigPaths(os.path.abspath(project_root), config_header) + + def menuconfig(self) -> None: + self._check_kconfiglib() + self._exclude_utestcases() + + import menuconfig + import curses + + sys.argv = ['menuconfig', 'Kconfig'] + self._fix_locale() + + try: + menuconfig._main() + except curses.error as exc: + if not os.path.isfile(self.paths.config_file): + raise + if 'nocbreak()' not in str(exc): + print("警告: menuconfig 退出异常: %s" % exc) + except Exception as exc: + if not os.path.isfile(self.paths.config_file): + raise + print("警告: menuconfig 退出异常: %s" % exc) + + self._sync_proj_config() + + def defconfig(self) -> None: + self._check_kconfiglib() + self._exclude_utestcases() + + import defconfig + + sys.argv = ['defconfig', '--kconfig', 'Kconfig', '.config'] + defconfig.main() + self._mk_proj_config(self.paths.config_file) + + def _sync_proj_config(self) -> None: + if not os.path.isfile(self.paths.config_file): + raise SystemExit(-1) + + if os.path.isfile(self.paths.config_old): + diff_eq = self._file_md5(self.paths.config_file) == self._file_md5(self.paths.config_old) + else: + diff_eq = False + + if not diff_eq: + self._copy_file(self.paths.config_file, self.paths.config_old) + self._mk_proj_config(self.paths.config_file) + elif not os.path.isfile(self.paths.config_header): + self._mk_proj_config(self.paths.config_file) + + def _mk_proj_config(self, filename: str) -> None: + if not os.path.isfile(filename): + print('open config:%s failed' % filename) + return + + with open(filename, 'r', encoding='utf-8') as config_file: + lines = config_file.readlines() + + with open(self.paths.config_header, 'w', encoding='utf-8') as config_header: + config_header.write('#ifndef PROJ_CONFIG_H__\n') + config_header.write('#define PROJ_CONFIG_H__\n\n') + + empty_line = True + for line in lines: + line = line.lstrip(' ').replace('\n', '').replace('\r', '') + if not line: + continue + + if line.startswith('#'): + if len(line) == 1: + if empty_line: + continue + config_header.write('\n') + empty_line = True + continue + if line.startswith('# CONFIG_'): + line = ' ' + line[9:] + else: + config_header.write('/*%s */\n' % line[1:]) + empty_line = False + continue + + empty_line = False + setting = line.split('=') + if len(setting) < 2: + continue + key = setting[0] + if key.startswith('CONFIG_'): + key = key[7:] + if self._is_pkg_special_config(key): + continue + if setting[1] == 'y': + config_header.write('#define %s\n' % key) + else: + value = re.findall(r"^.*?=(.*)$", line)[0] + config_header.write('#define %s %s\n' % (key, value)) + + config_header.write('\n') + config_header.write('#endif\n') + + def _exclude_utestcases(self) -> None: + kconfig_path = os.path.join(self.paths.project_root, 'Kconfig') + if os.path.isfile(os.path.join(self.paths.project_root, 'Kconfig.utestcases')): + return + if not os.path.isfile(kconfig_path): + return + + with open(kconfig_path, 'r', encoding='utf-8') as handle: + data = handle.readlines() + with open(kconfig_path, 'w', encoding='utf-8') as handle: + for line in data: + if 'Kconfig.utestcases' not in line: + handle.write(line) + + def _check_kconfiglib(self) -> None: + try: + import kconfiglib # noqa: F401 + except ImportError as exc: + print("\033[1;31m**ERROR**: Failed to import kconfiglib, " + str(exc)) + print("") + print("You may need to install it using:") + print(" pip install kconfiglib\033[0m") + print("") + sys.exit(1) + + pkg_dir = self.paths.resolve_pkg_dir() + if pkg_dir and os.path.exists(pkg_dir): + os.environ['PKGS_DIR'] = pkg_dir + else: + print("\033[1;33m**WARNING**: PKGS_DIR not found, please install ENV tools\033[0m") + + @staticmethod + def _fix_locale() -> None: + import locale + + try: + locale.setlocale(locale.LC_ALL, '') + except locale.Error: + locale.setlocale(locale.LC_ALL, 'C') + + @staticmethod + def _file_md5(file_path: str) -> str: + md5 = hashlib.new('md5') + with open(file_path, 'r', encoding='utf-8') as handle: + md5.update(handle.read().encode('utf8')) + return md5.hexdigest() + + @staticmethod + def _copy_file(src: str, dst: str) -> None: + with open(src, 'r', encoding='utf-8') as handle: + content = handle.read() + with open(dst, 'w', encoding='utf-8') as handle: + handle.write(content) + + @staticmethod + def _is_pkg_special_config(config_str: str) -> bool: + return config_str.startswith("PKG_") and (config_str.endswith('_PATH') or config_str.endswith('_VER')) + + +def menuconfig(project_root: str) -> None: + KconfigManager(project_root).menuconfig() + + +def defconfig(project_root: str) -> None: + KconfigManager(project_root).defconfig() diff --git a/ebuild/options.py b/ebuild/options.py new file mode 100644 index 0000000..b4e666d --- /dev/null +++ b/ebuild/options.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +"""SCons option registry for the build system.""" + +from dataclasses import dataclass +from typing import List, Optional + +from SCons.Script import AddOption + + +@dataclass(frozen=True) +class OptionSpec: + flags: List[str] + dest: str + help: str + action: Optional[str] = None + opt_type: Optional[str] = None + default: Optional[object] = None + + def apply(self) -> None: + kwargs = { + 'dest': self.dest, + 'help': self.help, + } + if self.action: + kwargs['action'] = self.action + if self.opt_type: + kwargs['type'] = self.opt_type + if self.default is not None: + kwargs['default'] = self.default + AddOption(*self.flags, **kwargs) + + +class OptionRegistry: + def __init__(self, specs: List[OptionSpec]) -> None: + self.specs = specs + self._registered = False + + def register(self) -> None: + if self._registered: + return + for spec in self.specs: + spec.apply() + self._registered = True + + +_DEFAULT_SPECS = [ + OptionSpec(['--target'], 'target', 'set target project: mdk/mdk4/mdk5/cmake/vscode', opt_type='string'), + OptionSpec(['--menuconfig'], 'menuconfig', 'open menuconfig for the project', action='store_true', default=False), + OptionSpec(['--attach'], 'attach', 'view attachconfig or apply attach item', opt_type='string'), + OptionSpec(['--verbose'], 'verbose', 'show full command lines', action='store_true', default=False), + OptionSpec(['--cross-compile'], 'cross-compile', 'set CROSS_COMPILE prefix', opt_type='string'), + OptionSpec(['--cpu'], 'cpu', 'set target CPU', opt_type='string'), + OptionSpec(['--fpu'], 'fpu', 'set target FPU', opt_type='string'), + OptionSpec(['--float-abi'], 'float-abi', 'set float ABI', opt_type='string'), +] + +_REGISTRY = OptionRegistry(_DEFAULT_SPECS) + + +def AddOptions() -> None: + _REGISTRY.register() diff --git a/ebuild/packages.py b/ebuild/packages.py new file mode 100644 index 0000000..7ed7313 --- /dev/null +++ b/ebuild/packages.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +"""Package.json based component builder.""" + +from __future__ import annotations + +import json +import os +from typing import Any, Dict, List, Optional, TYPE_CHECKING + +from .helpers import normalize_list + +if TYPE_CHECKING: + from .system import BuildSystem + + +class PackageBuilder: + def __init__(self, build: 'BuildSystem') -> None: + self.build = build + + def build_package(self, package_path: Optional[str] = None) -> List[Any]: + env = self.build.env + if env is None: + raise RuntimeError('BuildPackage requires an initialized SCons environment.') + + if package_path is None: + package_path = os.path.join(self.build.get_current_dir(), 'package.json') + elif os.path.isdir(package_path): + package_path = os.path.join(package_path, 'package.json') + + if not os.path.isfile(package_path): + return [] + + with open(package_path, 'r', encoding='utf-8') as handle: + package = json.load(handle) + + if package.get('type') != 'rt-thread-component' or 'name' not in package: + return [] + + dependencies = normalize_list(package.get('dependencies')) + if dependencies and not self._any_dependency_enabled(dependencies): + return [] + + defines = normalize_list(package.get('defines')) + sources, includes = self._collect_sources(package_path, package.get('sources', [])) + return self.build.define_group( + package['name'], + sources, + depend=dependencies, + CPPPATH=includes, + CPPDEFINES=defines, + ) + + def _any_dependency_enabled(self, deps: List[str]) -> bool: + for dep in deps: + if self.build.get_depend(dep): + return True + return False + + def _collect_sources(self, package_path: str, items: List[Dict[str, Any]]) -> tuple: + base_dir = os.path.dirname(os.path.abspath(package_path)) + sources: List[str] = [] + includes: List[str] = [] + + for item in items: + item_deps = normalize_list(item.get('dependencies')) + if item_deps and not self._any_dependency_enabled(item_deps): + continue + + for inc in normalize_list(item.get('includes')): + if os.path.isabs(inc) and os.path.isdir(inc): + includes.append(inc) + else: + includes.append(os.path.abspath(os.path.join(base_dir, inc))) + + for pattern in normalize_list(item.get('files')): + pattern_path = pattern if os.path.isabs(pattern) else os.path.join(base_dir, pattern) + sources.extend(self.build.glob_files(pattern_path)) + + return sources, includes diff --git a/ebuild/system.py b/ebuild/system.py new file mode 100644 index 0000000..d00ca6f --- /dev/null +++ b/ebuild/system.py @@ -0,0 +1,363 @@ +# -*- coding: utf-8 -*- +"""SCons build system core.""" + +from __future__ import annotations + +import os +import sys +from dataclasses import dataclass +from typing import Any, Dict, List, Optional + +from . import config +from .config import ConfigManager +from .exporter import ProjectExporter +from .groups import Group, GroupRegistry +from .helpers import merge_flags, normalize_defines, normalize_list +from .options import AddOptions +from .packages import PackageBuilder + + +@dataclass +class ToolchainSettings: + cc_prefix: str = "" + build_program: bool = True + + +class BuildSystem: + _current: Optional['BuildSystem'] = None + + def __init__(self, env, workspace_root: Optional[str] = None, project_root: Optional[str] = None) -> None: + if workspace_root is None: + workspace_root = project_root + self.env = env + self.workspace_root = os.path.abspath(workspace_root or config.resolve_project_root()) + self.project_root = os.path.abspath(os.getcwd()) + self.config_header = config.CONFIG_HEADER + + self.config = ConfigManager() + self.build_options: Dict[str, Any] = {} + self.registry = GroupRegistry() + self.packages = PackageBuilder(self) + self.exporter = ProjectExporter(self) + + BuildSystem._current = self + + @classmethod + def current(cls) -> Optional['BuildSystem']: + return cls._current + + def setup(self) -> 'BuildSystem': + AddOptions() + self._prepare_environment() + self._handle_attach() + self._handle_menuconfig() + self._load_config() + self._apply_command_output() + self.install_env() + return self + + def install_env(self) -> None: + env = self.env + env['_BuildSystem'] = self + + def define_group(env, name: str, src: List[Any], depend: Any = None, **kwargs) -> List[Any]: + return self.define_group(name, src, depend, **kwargs) + + def build_package(env, package_path: Optional[str] = None) -> List[Any]: + return self.build_package(package_path) + + def bridge(env) -> List[Any]: + return self.bridge() + + def glob(env, pattern: str): + from SCons.Script import Glob + + try: + return Glob(pattern) + except Exception: + return [] + + def get_cc(env) -> str: + return str(env.get('CC', '')) + + env.AddMethod(define_group, 'DefineGroup') + env.AddMethod(build_package, 'BuildPackage') + env.AddMethod(bridge, 'Bridge') + env.AddMethod(lambda env, dep: self.get_depend(dep), 'GetDepend') + env.AddMethod(glob, 'Glob') + env.AddMethod(lambda env, pattern: self.glob_files(pattern), 'GlobFiles') + env.AddMethod(lambda env, src, remove: self.src_remove(src, remove), 'SrcRemove') + env.AddMethod(lambda env: self.get_current_dir(), 'GetCurrentDir') + env.AddMethod(lambda env, target, objs=None: self.do_building(target, objs), 'DoBuilding') + env.AddMethod(lambda env: self.build_options, 'GetBuildOptions') + env.AddMethod(lambda env: self.workspace_root, 'GetProjectRoot') + env.AddMethod(lambda env: self.project_root, 'GetBSPRoot') + env.AddMethod(lambda env: self.workspace_root, 'GetRTTRoot') + env.AddMethod(get_cc, 'GetCC') + env.AddMethod(lambda env: self, 'GetContext') + + def define_group(self, name: str, src: List[Any], depend: Any = None, **kwargs) -> List[Any]: + dependencies = [item for item in normalize_list(depend) if item] + if not self.get_dependency(dependencies): + return [] + + src_list = self._normalize_sources(src) + + group = Group( + name=name, + sources=src_list, + dependencies=dependencies, + path=self.get_current_dir(), + include_paths=normalize_list(kwargs.get('CPPPATH')), + defines=normalize_defines(kwargs.get('CPPDEFINES')), + libs=normalize_list(kwargs.get('LIBS')), + lib_paths=normalize_list(kwargs.get('LIBPATH')), + cflags=merge_flags(kwargs.get('CFLAGS'), kwargs.get('CCFLAGS')), + cxxflags=merge_flags(kwargs.get('CXXFLAGS'), kwargs.get('CCFLAGS')), + asflags=merge_flags(kwargs.get('ASFLAGS')), + ldflags=merge_flags(kwargs.get('LINKFLAGS')), + local_cflags=merge_flags(kwargs.get('LOCAL_CFLAGS'), kwargs.get('LOCAL_CCFLAGS')), + local_cxxflags=merge_flags(kwargs.get('LOCAL_CXXFLAGS'), kwargs.get('LOCAL_CCFLAGS')), + local_asflags=merge_flags(kwargs.get('LOCAL_ASFLAGS')), + local_include_paths=normalize_list(kwargs.get('LOCAL_CPPPATH')), + local_defines=normalize_defines(kwargs.get('LOCAL_CPPDEFINES')), + ) + + self.registry.add(group) + + objects = group.build(self.env) + if kwargs.get('LIBRARY'): + return self.env.Library(name, objects) + return objects + + def build_package(self, package_path: Optional[str] = None) -> List[Any]: + return self.packages.build_package(package_path) + + def bridge(self) -> List[Any]: + from SCons.Script import SConscript + + base_dir = self.get_current_dir() + if not base_dir or not os.path.isdir(base_dir): + return [] + + env = self.env + groups: List[Any] = [] + for name in sorted(os.listdir(base_dir)): + child_dir = os.path.join(base_dir, name) + if not os.path.isdir(child_dir): + continue + script = os.path.join(child_dir, 'SConscript') + if not os.path.isfile(script): + continue + group = SConscript(script) + if not group: + continue + if isinstance(group, list): + groups.extend(group) + else: + groups.append(group) + + return groups + + def export_project(self, target: str, output_dir: Optional[str] = None) -> bool: + return self.exporter.export(target, output_dir) + + def prepare_building(self) -> List[Any]: + return self.merge_groups() + + def do_building(self, target: Optional[str], objs: Optional[List[Any]] = None) -> Optional[Any]: + from SCons.Script import Default, GetOption + + if objs is None: + objs = self.prepare_building() + + export_target = GetOption('target') + if export_target: + self.export_project(export_target) + raise SystemExit(0) + + if not target: + Default(objs) + return None + + program = self.env.Program(target, objs) + targets = [program] + + bin_name = self.env.get('RTBOOT_BIN') + if bin_name: + bin_file = self.env.Command(bin_name, program, "$OBJCOPY -O binary $SOURCE $TARGET") + targets.append(bin_file) + + if self.env.get('SIZE'): + self.env.AddPostAction(program, "$SIZE $TARGET") + Default(targets) + return program + + def get_dependency(self, depend: Any) -> bool: + return self.config.get_dependency(depend) + + def get_depend(self, depend: Any) -> bool: + return self.get_dependency(depend) + + def get_current_dir(self) -> str: + from SCons.Script import Dir, File + + conscript = File('SConscript') + if conscript.exists(): + return os.path.dirname(conscript.rfile().abspath) + return Dir('.').abspath + + def glob_files(self, pattern: str) -> List[str]: + from SCons.Script import Glob + + try: + return sorted(Glob(pattern, strings=True)) + except Exception: + return [] + + def src_remove(self, src: List[str], remove: List[str]) -> None: + if not src: + return + if not isinstance(remove, list): + remove = [remove] + + import fnmatch + + for item in remove: + matches = [s for s in src if fnmatch.fnmatch(str(s), item)] + for match in matches: + src.remove(match) + + def merge_groups(self) -> List[Any]: + return self.registry.merge_objects() + + def apply_toolchain_options(self, base_cflags: Optional[List[str]] = None) -> ToolchainSettings: + from SCons.Script import GetOption + + cflags = list(base_cflags) if base_cflags else [] + + def option_value(name: str, default: str = "") -> str: + value = GetOption(name) + return value or default + + cc_prefix = option_value('cross-compile', '') + if cc_prefix: + self.env['CC'] = cc_prefix + 'gcc' + self.env['AR'] = cc_prefix + 'ar' + self.env['AS'] = cc_prefix + 'gcc' + self.env['RANLIB'] = cc_prefix + 'ranlib' + cflags.append('-ffreestanding') + + cpu = option_value('cpu', '') + if cpu: + cflags.append(f'-mcpu={cpu}') + + fpu = option_value('fpu', '') + if fpu: + cflags.append(f'-mfpu={fpu}') + + float_abi = option_value('float-abi', '') + if float_abi: + cflags.append(f'-mfloat-abi={float_abi}') + + if cflags: + self.env.Append(CFLAGS=cflags) + + build_program = not cc_prefix + return ToolchainSettings(cc_prefix=cc_prefix, build_program=build_program) + + def _prepare_environment(self) -> None: + self.env['PROJECT_ROOT'] = self.workspace_root + self.env.setdefault('RTT_ROOT', self.workspace_root) + self.env['BSP_ROOT'] = self.project_root + + tools_path = os.path.join(self.workspace_root, 'tools') + if tools_path not in sys.path: + sys.path.insert(0, tools_path) + + def _apply_command_output(self) -> None: + from SCons.Script import GetOption + + if GetOption('verbose'): + return + + self.env.Replace( + ARCOMSTR="AR $TARGET", + ASCOMSTR="AS $TARGET", + ASPPCOMSTR="AS $TARGET", + CCCOMSTR="CC $TARGET", + CXXCOMSTR="CXX $TARGET", + LINKCOMSTR="LINK $TARGET", + RANLIBCOMSTR="RANLIB $TARGET", + ) + + def _handle_menuconfig(self) -> None: + from SCons.Script import GetOption + + if GetOption('menuconfig'): + from .kconfig import menuconfig + + menuconfig(self.workspace_root) + raise SystemExit(0) + + def _handle_attach(self) -> None: + from SCons.Script import GetOption + + if GetOption('attach'): + from .attach import GenAttachConfigProject + + GenAttachConfigProject(self.workspace_root) + raise SystemExit(0) + + def _load_config(self) -> None: + config_path = os.path.join(self.project_root, self.config_header) + if not os.path.exists(config_path): + print(f"{self.config_header} not found, run: scons --menuconfig") + raise SystemExit(1) + self.config.load_from_file(config_path) + self.build_options = self.config.get_all_options() + + def _normalize_sources(self, src: Any) -> List[Any]: + if src is None: + return [] + src_list = src if isinstance(src, list) else [src] + result: List[Any] = [] + seen = set() + + for item in src_list: + if item is None: + continue + node = self.env.File(item) if isinstance(item, str) else item + key = getattr(node, 'abspath', None) or str(node) + if key in seen: + continue + seen.add(key) + result.append(node) + + return result + + +def prepare(env, workspace_root: Optional[str] = None, project_root: Optional[str] = None, config_module=None) -> BuildSystem: + if workspace_root is None: + workspace_root = project_root + build = BuildSystem(env, workspace_root).setup() + from SCons.Script import Export + Export(env=env) + if config_module is not None: + env['config'] = config_module + project_name = getattr(config_module, 'PROJECT_NAME', None) + if project_name: + config.PROJECT_NAME = str(project_name) + target_name = getattr(config_module, 'TARGET_NAME', None) + if target_name: + config.TARGET_NAME = str(target_name) + else: + if project_name: + config.TARGET_NAME = f"{config.PROJECT_NAME}.elf" + if hasattr(config_module, 'setup_project'): + config_module.setup_project(env, build.project_root) + elif hasattr(config_module, 'MCU_SERIES'): + from .toolchain import setup_project + + setup_project(env, build.project_root, config_module) + return build diff --git a/ebuild/targets/__init__.py b/ebuild/targets/__init__.py new file mode 100644 index 0000000..13be6b6 --- /dev/null +++ b/ebuild/targets/__init__.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# +# File : __init__.py +# This file is part of RT-Thread RTOS +# COPYRIGHT (C) 2006 - 2015, RT-Thread Development Team +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Change Logs: +# Date Author Notes +# 2025-01-XX Bernard Create targets module for IDE project generators + +# Import supported target generators +from . import keil + +__all__ = [ + 'keil', +] diff --git a/ebuild/targets/keil.py b/ebuild/targets/keil.py new file mode 100644 index 0000000..1b7e98e --- /dev/null +++ b/ebuild/targets/keil.py @@ -0,0 +1,544 @@ +# +# File : keil.py +# This file is part of RT-Thread RTOS +# COPYRIGHT (C) 2006 - 2015, RT-Thread Development Team +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Change Logs: +# Date Author Notes +# 2015-01-20 Bernard Add copyright information +# + +import os +import sys +import string +import shutil + +import xml.etree.ElementTree as etree +from xml.etree.ElementTree import SubElement +from .utils import _make_path_relative +from .utils import xml_indent + +fs_encoding = sys.getfilesystemencoding() + +def _get_filetype(fn): + if fn.rfind('.cpp') != -1 or fn.rfind('.cxx') != -1: + return 8 + + if fn.rfind('.c') != -1 or fn.rfind('.C') != -1: + return 1 + + # assemble file type + if fn.rfind('.s') != -1 or fn.rfind('.S') != -1: + return 2 + + # header type + if fn.rfind('.h') != -1: + return 5 + + if fn.rfind('.lib') != -1: + return 4 + + if fn.rfind('.o') != -1: + return 3 + + # other filetype + return 5 + +def MDK4AddGroupForFN(ProjectFiles, parent, name, filename, project_path): + group = SubElement(parent, 'Group') + group_name = SubElement(group, 'GroupName') + group_name.text = name + + name = os.path.basename(filename) + path = os.path.dirname (filename) + + basename = os.path.basename(path) + path = _make_path_relative(project_path, path) + path = os.path.join(path, name) + files = SubElement(group, 'Files') + file = SubElement(files, 'File') + file_name = SubElement(file, 'FileName') + name = os.path.basename(path) + + if name.find('.cpp') != -1: + obj_name = name.replace('.cpp', '.o') + elif name.find('.c') != -1: + obj_name = name.replace('.c', '.o') + elif name.find('.s') != -1: + obj_name = name.replace('.s', '.o') + elif name.find('.S') != -1: + obj_name = name.replace('.s', '.o') + else: + obj_name = name + + if ProjectFiles.count(obj_name): + name = basename + '_' + name + ProjectFiles.append(obj_name) + try: # python 2 + file_name.text = name.decode(fs_encoding) + except: # python 3 + file_name.text = name + file_type = SubElement(file, 'FileType') + file_type.text = '%d' % _get_filetype(name) + file_path = SubElement(file, 'FilePath') + try: # python 2 + file_path.text = path.decode(fs_encoding) + except: # python 3 + file_path.text = path + + + return group + +def MDK4AddLibToGroup(ProjectFiles, group, name, filename, project_path): + name = os.path.basename(filename) + path = os.path.dirname (filename) + + basename = os.path.basename(path) + path = _make_path_relative(project_path, path) + path = os.path.join(path, name) + files = SubElement(group, 'Files') + file = SubElement(files, 'File') + file_name = SubElement(file, 'FileName') + name = os.path.basename(path) + + if name.find('.cpp') != -1: + obj_name = name.replace('.cpp', '.o') + elif name.find('.c') != -1: + obj_name = name.replace('.c', '.o') + elif name.find('.s') != -1: + obj_name = name.replace('.s', '.o') + elif name.find('.S') != -1: + obj_name = name.replace('.s', '.o') + else: + obj_name = name + + if ProjectFiles.count(obj_name): + name = basename + '_' + name + ProjectFiles.append(obj_name) + try: + file_name.text = name.decode(fs_encoding) + except: + file_name.text = name + file_type = SubElement(file, 'FileType') + file_type.text = '%d' % _get_filetype(name) + file_path = SubElement(file, 'FilePath') + + try: + file_path.text = path.decode(fs_encoding) + except: + file_path.text = path + + return group + +def MDK4AddGroup(ProjectFiles, parent, name, files, project_path, group_scons): + # don't add an empty group + if len(files) == 0: + return + + group = SubElement(parent, 'Group') + group_name = SubElement(group, 'GroupName') + group_name.text = name + + for f in files: + fn = f.rfile() + name = fn.name + path = os.path.dirname(fn.abspath) + + basename = os.path.basename(path) + path = _make_path_relative(project_path, path) + path = os.path.join(path, name) + + files = SubElement(group, 'Files') + file = SubElement(files, 'File') + file_name = SubElement(file, 'FileName') + name = os.path.basename(path) + + if name.find('.cpp') != -1: + obj_name = name.replace('.cpp', '.o') + elif name.find('.c') != -1: + obj_name = name.replace('.c', '.o') + elif name.find('.s') != -1: + obj_name = name.replace('.s', '.o') + elif name.find('.S') != -1: + obj_name = name.replace('.s', '.o') + + if ProjectFiles.count(obj_name): + name = basename + '_' + name + ProjectFiles.append(obj_name) + file_name.text = name # name.decode(fs_encoding) + file_type = SubElement(file, 'FileType') + file_type.text = '%d' % _get_filetype(name) + file_path = SubElement(file, 'FilePath') + file_path.text = path # path.decode(fs_encoding) + + # for local LOCAL_CFLAGS/LOCAL_CXXFLAGS/LOCAL_CCFLAGS/LOCAL_CPPPATH/LOCAL_CPPDEFINES + MiscControls_text = ' ' + if file_type.text == '1' and 'LOCAL_CFLAGS' in group_scons: + MiscControls_text = MiscControls_text + group_scons['LOCAL_CFLAGS'] + elif file_type.text == '8' and 'LOCAL_CXXFLAGS' in group_scons: + MiscControls_text = MiscControls_text + group_scons['LOCAL_CXXFLAGS'] + if 'LOCAL_CCFLAGS' in group_scons: + MiscControls_text = MiscControls_text + group_scons['LOCAL_CCFLAGS'] + if MiscControls_text != ' ' or ('LOCAL_CPPDEFINES' in group_scons): + FileOption = SubElement(file, 'FileOption') + FileArmAds = SubElement(FileOption, 'FileArmAds') + Cads = SubElement(FileArmAds, 'Cads') + VariousControls = SubElement(Cads, 'VariousControls') + MiscControls = SubElement(VariousControls, 'MiscControls') + MiscControls.text = MiscControls_text + Define = SubElement(VariousControls, 'Define') + if 'LOCAL_CPPDEFINES' in group_scons: + Define.text = ', '.join(set(group_scons['LOCAL_CPPDEFINES'])) + else: + Define.text = ' ' + Undefine = SubElement(VariousControls, 'Undefine') + Undefine.text = ' ' + IncludePath = SubElement(VariousControls, 'IncludePath') + if 'LOCAL_CPPPATH' in group_scons: + IncludePath.text = ';'.join([_make_path_relative(project_path, os.path.normpath(i)) for i in group_scons['LOCAL_CPPPATH']]) + else: + IncludePath.text = ' ' + + return group + +# The common part of making MDK4/5 project +def MDK45Project(env, tree, target, script): + project_path = os.path.dirname(os.path.abspath(target)) + + root = tree.getroot() + out = open(target, 'w') + out.write('\n') + + CPPPATH = [] + CPPDEFINES = env.get('CPPDEFINES', []) + LINKFLAGS = '' + CXXFLAGS = '' + CCFLAGS = '' + CFLAGS = '' + ProjectFiles = [] + + # add group + groups = tree.find('Targets/Target/Groups') + if groups is None: + groups = SubElement(tree.find('Targets/Target'), 'Groups') + groups.clear() # clean old groups + for group in script: + group_tree = MDK4AddGroup(ProjectFiles, groups, group['name'], group['src'], project_path, group) + + # get each include path + if 'CPPPATH' in group and group['CPPPATH']: + if CPPPATH: + CPPPATH += group['CPPPATH'] + else: + CPPPATH += group['CPPPATH'] + + # get each group's link flags + if 'LINKFLAGS' in group and group['LINKFLAGS']: + if LINKFLAGS: + LINKFLAGS += ' ' + group['LINKFLAGS'] + else: + LINKFLAGS += group['LINKFLAGS'] + + # get each group's CXXFLAGS flags + if 'CXXFLAGS' in group and group['CXXFLAGS']: + if CXXFLAGS: + CXXFLAGS += ' ' + group['CXXFLAGS'] + else: + CXXFLAGS += group['CXXFLAGS'] + + # get each group's CCFLAGS flags + if 'CCFLAGS' in group and group['CCFLAGS']: + if CCFLAGS: + CCFLAGS += ' ' + group['CCFLAGS'] + else: + CCFLAGS += group['CCFLAGS'] + + # get each group's CFLAGS flags + if 'CFLAGS' in group and group['CFLAGS']: + if CFLAGS: + CFLAGS += ' ' + group['CFLAGS'] + else: + CFLAGS += group['CFLAGS'] + + # get each group's LIBS flags + if 'LIBS' in group and group['LIBS']: + for item in group['LIBPATH']: + full_path = os.path.join(item, group['name'] + '.lib') + if os.path.isfile(full_path): # has this library + if group_tree != None: + MDK4AddLibToGroup(ProjectFiles, group_tree, group['name'], full_path, project_path) + else: + group_tree = MDK4AddGroupForFN(ProjectFiles, groups, group['name'], full_path, project_path) + + # write include path, definitions and link flags + IncludePath = tree.find('Targets/Target/TargetOption/TargetArmAds/Cads/VariousControls/IncludePath') + IncludePath.text = ';'.join([_make_path_relative(project_path, os.path.normpath(i)) for i in set(CPPPATH)]) + + Define = tree.find('Targets/Target/TargetOption/TargetArmAds/Cads/VariousControls/Define') + Define.text = ', '.join(set(CPPDEFINES)) + + if 'c99' in CXXFLAGS or 'c99' in CCFLAGS or 'c99' in CFLAGS: + uC99 = tree.find('Targets/Target/TargetOption/TargetArmAds/Cads/uC99') + uC99.text = '1' + + if 'gnu' in CXXFLAGS or 'gnu' in CCFLAGS or 'gnu' in CFLAGS: + uGnu = tree.find('Targets/Target/TargetOption/TargetArmAds/Cads/uGnu') + uGnu.text = '1' + + Misc = tree.find('Targets/Target/TargetOption/TargetArmAds/LDads/Misc') + Misc.text = LINKFLAGS + + xml_indent(root) + out.write(etree.tostring(root, encoding='utf-8').decode()) + out.close() + +def MDK4Project(env, target, script): + + if os.path.isfile('template.uvproj') is False: + print ('Warning: The template project file [template.uvproj] not found!') + return + + template_tree = etree.parse('template.uvproj') + + MDK45Project(env, template_tree, target, script) + + # remove project.uvopt file + project_uvopt = os.path.abspath(target).replace('uvproj', 'uvopt') + if os.path.isfile(project_uvopt): + os.unlink(project_uvopt) + + # copy uvopt file + if os.path.exists('template.uvopt'): + import shutil + shutil.copy2('template.uvopt', '{}.uvopt'.format(os.path.splitext(target)[0])) +import threading +import time +def monitor_log_file(log_file_path): + if not os.path.exists(log_file_path): + open(log_file_path, 'w').close() + empty_line_count = 0 + with open(log_file_path, 'r') as log_file: + while True: + line = log_file.readline() + if line: + print(line.strip()) + if 'Build Time Elapsed' in line: + break + empty_line_count = 0 + else: + empty_line_count += 1 + time.sleep(1) + if empty_line_count > 30: + print("Timeout reached or too many empty lines, exiting log monitoring thread.") + break +def MDK5Project(env, target, script): + + if os.path.isfile('template.uvprojx') is False: + print ('Warning: The template project file [template.uvprojx] not found!') + return + + template_tree = etree.parse('template.uvprojx') + + MDK45Project(env, template_tree, target, script) + + # remove project.uvopt file + project_uvopt = os.path.abspath(target).replace('uvprojx', 'uvoptx') + if os.path.isfile(project_uvopt): + os.unlink(project_uvopt) + # copy uvopt file + if os.path.exists('template.uvoptx'): + import shutil + shutil.copy2('template.uvoptx', '{}.uvoptx'.format(os.path.splitext(target)[0])) + # build with UV4.exe + + if shutil.which('UV4.exe') is not None: + target_name = template_tree.find('Targets/Target/TargetName') + print('target_name:', target_name.text) + log_file_path = 'keil.log' + if os.path.exists(log_file_path): + os.remove(log_file_path) + log_thread = threading.Thread(target=monitor_log_file, args=(log_file_path,)) + log_thread.start() + cmd = 'UV4.exe -b project.uvprojx -q -j0 -t '+ target_name.text +' -o '+log_file_path + print('Start to build keil project') + print(cmd) + os.system(cmd) + else: + print('UV4.exe is not available, please check your keil installation') + +def MDK2Project(env, target, script): + template = open(os.path.join(os.path.dirname(__file__), 'template.Uv2'), 'r') + lines = template.readlines() + + project = open(target, "w") + project_path = os.path.dirname(os.path.abspath(target)) + + line_index = 5 + # write group + for group in script: + lines.insert(line_index, 'Group (%s)\r\n' % group['name']) + line_index += 1 + + lines.insert(line_index, '\r\n') + line_index += 1 + + # write file + + ProjectFiles = [] + CPPPATH = [] + CPPDEFINES = env.get('CPPDEFINES', []) + LINKFLAGS = '' + CFLAGS = '' + + # number of groups + group_index = 1 + for group in script: + # print group['name'] + + # get each include path + if 'CPPPATH' in group and group['CPPPATH']: + if CPPPATH: + CPPPATH += group['CPPPATH'] + else: + CPPPATH += group['CPPPATH'] + + # get each group's link flags + if 'LINKFLAGS' in group and group['LINKFLAGS']: + if LINKFLAGS: + LINKFLAGS += ' ' + group['LINKFLAGS'] + else: + LINKFLAGS += group['LINKFLAGS'] + + # generate file items + for node in group['src']: + fn = node.rfile() + name = fn.name + path = os.path.dirname(fn.abspath) + basename = os.path.basename(path) + path = _make_path_relative(project_path, path) + path = os.path.join(path, name) + if ProjectFiles.count(name): + name = basename + '_' + name + ProjectFiles.append(name) + lines.insert(line_index, 'File %d,%d,<%s><%s>\r\n' + % (group_index, _get_filetype(name), path, name)) + line_index += 1 + + group_index = group_index + 1 + + lines.insert(line_index, '\r\n') + line_index += 1 + + # remove repeat path + paths = set() + for path in CPPPATH: + inc = _make_path_relative(project_path, os.path.normpath(path)) + paths.add(inc) #.replace('\\', '/') + + paths = [i for i in paths] + CPPPATH = string.join(paths, ';') + + definitions = [i for i in set(CPPDEFINES)] + CPPDEFINES = string.join(definitions, ', ') + + while line_index < len(lines): + if lines[line_index].startswith(' ADSCINCD '): + lines[line_index] = ' ADSCINCD (' + CPPPATH + ')\r\n' + + if lines[line_index].startswith(' ADSLDMC ('): + lines[line_index] = ' ADSLDMC (' + LINKFLAGS + ')\r\n' + + if lines[line_index].startswith(' ADSCDEFN ('): + lines[line_index] = ' ADSCDEFN (' + CPPDEFINES + ')\r\n' + + line_index += 1 + + # write project + for line in lines: + project.write(line) + + project.close() + +def ARMCC_Version(): + from proj_config import CONFIG as toolchain_config + import subprocess + import re + + config = getattr(toolchain_config, "TOOLCHAIN_CONFIG", None) + if isinstance(config, dict): + path = config.get("EXEC_PATH", "") + platform = config.get("PLATFORM", getattr(toolchain_config, "PLATFORM", "")) + else: + path = getattr(toolchain_config, "EXEC_PATH", "") + platform = getattr(toolchain_config, "PLATFORM", "") + + if(platform == 'armcc'): + path = os.path.join(path, 'armcc.exe') + elif(platform == 'armclang'): + path = os.path.join(path, 'armlink.exe') + + if os.path.exists(path): + cmd = path + else: + return "0.0" + + child = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + stdout, stderr = child.communicate() + + ''' + example stdout: + Product: MDK Plus 5.24 + Component: ARM Compiler 5.06 update 5 (build 528) + Tool: armcc [4d3621] + + return version: MDK Plus 5.24/ARM Compiler 5.06 update 5 (build 528)/armcc [4d3621] + ''' + if not isinstance(stdout, str): + stdout = str(stdout, 'utf8') # Patch for Python 3 + version_Product = re.search(r'Product: (.+)', stdout).group(1) + version_Product = version_Product[:-1] + version_Component = re.search(r'Component: (.*)', stdout).group(1) + version_Component = version_Component[:-1] + version_Tool = re.search(r'Tool: (.*)', stdout).group(1) + version_Tool = version_Tool[:-1] + version_str_format = '%s/%s/%s' + version_str = version_str_format % (version_Product, version_Component, version_Tool) + return version_str + + +class KeilProjectGenerator: + """Keil project generator wrapper.""" + + def __init__(self, env, project_name: str) -> None: + self.env = env + self.project_name = project_name + + def generate(self, target_name: str, groups: list) -> None: + if os.path.isfile("template.uvprojx") and target_name not in ["mdk4"]: + MDK5Project(self.env, self.project_name + ".uvprojx", groups) + print("Keil5 project is generating...") + elif os.path.isfile("template.uvproj") and target_name not in ["mdk5"]: + MDK4Project(self.env, self.project_name + ".uvproj", groups) + print("Keil4 project is generating...") + elif os.path.isfile("template.Uv2") and target_name not in ["mdk4", "mdk5"]: + MDK2Project(self.env, self.project_name + ".Uv2", groups) + print("Keil2 project is generating...") + else: + raise RuntimeError("Keil template missing.") + + print("Keil Version: " + ARMCC_Version()) + print("Keil-MDK project has generated successfully!") diff --git a/ebuild/targets/utils.py b/ebuild/targets/utils.py new file mode 100644 index 0000000..403142e --- /dev/null +++ b/ebuild/targets/utils.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +"""Minimal utilities for project generation.""" + +import os +from dataclasses import dataclass + + +@dataclass(frozen=True) +class PathUtils: + @staticmethod + def make_relative(origin: str, dest: str) -> str: + origin = os.path.abspath(origin).replace('\\', '/') + dest = os.path.abspath(dest).replace('\\', '/') + + origin_parts = PathUtils._split_all(os.path.normcase(origin)) + dest_parts = PathUtils._split_all(dest) + + if origin_parts[0] != os.path.normcase(dest_parts[0]): + return dest + + index = 0 + for origin_seg, dest_seg in zip(origin_parts, dest_parts): + if origin_seg != os.path.normcase(dest_seg): + break + index += 1 + + segments = [os.pardir] * (len(origin_parts) - index) + segments += dest_parts[index:] + if not segments: + return os.curdir + return os.path.join(*segments) + + @staticmethod + def _split_all(path: str) -> list: + parts = [] + while path not in (os.curdir, os.pardir): + prev = path + path, child = os.path.split(prev) + if path == prev: + break + parts.append(child) + parts.append(path) + parts.reverse() + return parts + + +@dataclass(frozen=True) +class XmlUtils: + @staticmethod + def indent(elem, level: int = 0) -> None: + indent_str = "\n" + level * " " + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = indent_str + " " + if not elem.tail or not elem.tail.strip(): + elem.tail = indent_str + for child in elem: + XmlUtils.indent(child, level + 1) + if not elem.tail or not elem.tail.strip(): + elem.tail = indent_str + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = indent_str + + +def _make_path_relative(origin: str, dest: str) -> str: + return PathUtils.make_relative(origin, dest) + + +def xml_indent(elem, level: int = 0) -> None: + XmlUtils.indent(elem, level) diff --git a/ebuild/toolchain.py b/ebuild/toolchain.py new file mode 100644 index 0000000..667487b --- /dev/null +++ b/ebuild/toolchain.py @@ -0,0 +1,260 @@ +# -*- coding: utf-8 -*- +"""Toolchain helpers for SCons projects.""" + +from __future__ import annotations + +import json +import os +from typing import Any, Dict, List, Optional + + +DEFAULT_COMMANDS = { + "CC": "gcc", + "CXX": "g++", + "AS": "gcc", + "AR": "ar", + "LINK": "gcc", + "SIZE": "size", + "OBJDUMP": "objdump", + "OBJCOPY": "objcopy", +} + + +def _as_list(value: Any) -> List[str]: + if not value: + return [] + if isinstance(value, (list, tuple)): + return list(value) + return str(value).split() + + +def _get_config_dict(config_module: Optional[Any]) -> Optional[Dict[str, Any]]: + if config_module is None: + return None + config_dict = getattr(config_module, "TOOLCHAIN_CONFIG", None) + if isinstance(config_dict, dict): + return config_dict + return None + + +def _get_attr(config_module: Optional[Any], name: str, default: Any) -> Any: + if config_module is None: + return default + config_dict = _get_config_dict(config_module) + if config_dict and name in config_dict: + return config_dict[name] + if hasattr(config_module, name): + return getattr(config_module, name) + return default + + +def _log_debug(config_module: Optional[Any], message: str) -> None: + level = _get_attr(config_module, "LOG_LEVEL", "") + if isinstance(level, str) and level.lower() in ("debug", "trace"): + print(f"[ebuild][debug] {message}") + + +def _is_verbose() -> bool: + try: + from SCons.Script import GetOption + except Exception: + return False + try: + return bool(GetOption("verbose")) + except Exception: + return False + + +def _log_verbose(message: str) -> None: + if _is_verbose(): + print(f"\033[32m[ebuild]: {message}\033[0m") + + +def _toolchain_bins(config_module: Optional[Any]) -> Dict[str, str]: + prefix = _get_attr(config_module, "CC_PREFIX", "") + commands = _get_attr(config_module, "TOOLCHAIN_COMMANDS", DEFAULT_COMMANDS) + if not isinstance(commands, dict): + commands = DEFAULT_COMMANDS + return {key: f"{prefix}{value}" for key, value in commands.items()} + + +def _cc_executable_name(config_module: Optional[Any]) -> str: + prefix = _get_attr(config_module, "CC_PREFIX", "") + commands = _get_attr(config_module, "TOOLCHAIN_COMMANDS", DEFAULT_COMMANDS) + if not isinstance(commands, dict): + commands = DEFAULT_COMMANDS + cc = commands.get("CC", DEFAULT_COMMANDS["CC"]) + return f"{prefix}{cc}" + + +def _is_exec_path_valid(exec_path: str, cc_bin: str) -> bool: + if not exec_path: + return False + cc_path = os.path.join(exec_path, cc_bin) + if os.path.isfile(cc_path): + return True + if os.name == "nt" and os.path.isfile(cc_path + ".exe"): + return True + return False + + +def _load_json(path: str) -> Optional[Any]: + try: + with open(path, "r", encoding="utf-8") as handle: + return json.load(handle) + except (OSError, json.JSONDecodeError): + return None + + +def _env_root() -> str: + return os.path.expanduser("~/.env") + + +def _detect_sdk_path_from_env(cc_bin: str) -> Optional[str]: + env_root = _env_root() + sdk_root = os.path.join(env_root, "tools", "scripts") + sdk_pkgs = os.path.join(sdk_root, "packages") + if not os.path.isdir(sdk_pkgs): + return None + + sdk_cfg_path = os.path.join(sdk_root, "sdk_cfg.json") + sdk_cfg = _load_json(sdk_cfg_path) + if isinstance(sdk_cfg, list): + for item in sdk_cfg: + if item.get("name") != cc_bin: + continue + candidate = os.path.join(sdk_pkgs, item.get("path", "")) + if _is_exec_path_valid(candidate, cc_bin): + return candidate + if "gcc" in cc_bin: + candidate_bin = os.path.join(candidate, "bin") + if _is_exec_path_valid(candidate_bin, cc_bin): + return candidate_bin + + pkgs_path = os.path.join(sdk_pkgs, "pkgs.json") + pkgs = _load_json(pkgs_path) + if not isinstance(pkgs, list): + return None + + for item in pkgs: + package_path = os.path.join(env_root, "packages", item.get("path", ""), "package.json") + package = _load_json(package_path) + if not isinstance(package, dict): + continue + if package.get("name") != cc_bin: + continue + version = item.get("ver", "") + if not version: + continue + candidate = os.path.join(sdk_pkgs, f"{package['name']}-{version}") + if _is_exec_path_valid(candidate, cc_bin): + return candidate + if "gcc" in cc_bin: + candidate_bin = os.path.join(candidate, "bin") + if _is_exec_path_valid(candidate_bin, cc_bin): + return candidate_bin + + return None + + +def _resolve_exec_path(config_module: Optional[Any]) -> str: + exec_path = _get_attr(config_module, "EXEC_PATH", "") + cc_bin = _cc_executable_name(config_module) + prefix = _get_attr(config_module, "CC_PREFIX", "") + + if _is_exec_path_valid(exec_path, cc_bin): + return exec_path + + if not exec_path and not prefix: + return exec_path + + _log_debug( + config_module, + f"toolchain path invalid, start env detect (EXEC_PATH={exec_path or ''}, CC={cc_bin})", + ) + candidate = _detect_sdk_path_from_env(cc_bin) + if candidate: + _log_verbose(f"CC={cc_bin}, EXEC_PATH={candidate}") + _log_debug(config_module, f"toolchain detected at {candidate}") + config_dict = _get_config_dict(config_module) + if config_dict is not None: + config_dict["EXEC_PATH"] = candidate + return candidate + + _log_debug(config_module, "toolchain detect failed in ~/.env") + env_root = _env_root() + raise SystemExit( + "Toolchain not found.\n" + f" EXEC_PATH: {exec_path or ''}\n" + f" CC: {cc_bin}\n" + f" env root: {env_root}\n" + " hint: install toolchain into ~/.env or update proj_config.py" + ) + + +def apply_toolchain(env, config_module: Optional[Any] = None) -> None: + cross_tool = _get_attr(config_module, "CROSS_TOOL", "gcc") + if cross_tool != "gcc": + raise SystemExit("Only gcc toolchain is supported.") + + exec_path = _resolve_exec_path(config_module) + if exec_path: + env.PrependENVPath("PATH", exec_path) + + env.Replace(**_toolchain_bins(config_module)) + env["ARFLAGS"] = "-rc" + env["ASCOM"] = env["ASPPCOM"] + env.AppendUnique(CPPDEFINES=_as_list(_get_attr(config_module, "BASE_DEFINES", []))) + + +def _resolve_mcu(env, mcu_series: Dict[str, Dict[str, str]]) -> tuple: + for macro, entry in mcu_series.items(): + if env.GetDepend(macro): + return (entry.get("cpu"), entry.get("link_script"), entry.get("bin")) + raise SystemExit("MCU series not configured, run: scons --menuconfig") + + +def apply_device_flags(env, project_root: str, config_module: Optional[Any] = None) -> Optional[str]: + mcu_series = _get_attr(config_module, "MCU_SERIES", {}) + cpu, link_script, bin_name = _resolve_mcu(env, mcu_series) + if not cpu or not link_script: + raise SystemExit("MCU series not configured, run: scons --menuconfig") + + device_flags = [ + f"-mcpu={cpu}", + "-mthumb", + "-ffunction-sections", + "-fdata-sections", + ] + + cflags = device_flags + _as_list(_get_attr(config_module, "BASE_CFLAGS", [])) + cxxflags = device_flags + _as_list(_get_attr(config_module, "BASE_CXXFLAGS", [])) + asflags = _as_list(_get_attr(config_module, "BASE_ASFLAGS", [])) + device_flags + linkflags = device_flags + _as_list(_get_attr(config_module, "BASE_LINKFLAGS", [])) + ["-T", link_script] + + build = _get_attr(config_module, "BUILD", "release") + if build == "debug": + cflags += ["-O0", "-gdwarf-2", "-g"] + cxxflags += ["-O0", "-gdwarf-2", "-g"] + asflags += ["-gdwarf-2"] + else: + cflags += ["-Os"] + cxxflags += ["-Os"] + + env.AppendUnique(CFLAGS=cflags, CXXFLAGS=cxxflags, ASFLAGS=asflags, LINKFLAGS=linkflags) + env.AppendUnique(CPPPATH=[project_root]) + return bin_name + + +def setup_project(env, project_root: str, config_module: Optional[Any] = None) -> Optional[str]: + apply_toolchain(env, config_module) + bin_name = apply_device_flags(env, project_root, config_module) + env["RTBOOT_BIN"] = bin_name + if bin_name: + output_dir = os.path.dirname(bin_name) + if output_dir: + os.makedirs(output_dir, exist_ok=True) + return bin_name + + +__all__ = ["apply_toolchain", "apply_device_flags", "setup_project"] From 8675ba22b07eb8a06219c61c0edbe8eb34642030 Mon Sep 17 00:00:00 2001 From: bernard Date: Thu, 25 Dec 2025 15:02:57 +0800 Subject: [PATCH 2/2] [ebuild] refine project roots and post actions --- ebuild/README.md | 12 ++++++--- ebuild/__init__.py | 4 +-- ebuild/config.py | 12 +++++++-- ebuild/groups.py | 2 ++ ebuild/system.py | 61 ++++++++++++++++++++++++++++++++------------- ebuild/toolchain.py | 17 ++++--------- 6 files changed, 70 insertions(+), 38 deletions(-) diff --git a/ebuild/README.md b/ebuild/README.md index a9b3bf2..ad8b61c 100644 --- a/ebuild/README.md +++ b/ebuild/README.md @@ -22,7 +22,7 @@ from ebuild import PrepareBuilding, DoBuilding # 准备构建环境 env = Environment() -build = PrepareBuilding(env, project_root='.', proj_config=proj_config) +build = PrepareBuilding(env, proj_config=proj_config) # 注册组件(在 SConscript 中) # env.DefineGroup('my_component', ['src/*.c'], depend=['CONFIG_MY_FEATURE']) @@ -78,6 +78,8 @@ scons --target=mdk5 - **proj_config.h** - 由 menuconfig 生成的 C 头文件,包含所有配置宏 - **Kconfig** - menuconfig 配置描述文件 +ProjectRoot 指 SCons 的执行根目录(`scons` 或 `scons -C dir` 的 `dir`),上述配置文件与 `.config`、`.ci/attachconfig` 都应位于该目录下。 + ### 组件注册 EBuild 提供两种组件组织方式: @@ -128,12 +130,14 @@ TOOLCHAIN_CONFIG = { 'MCU_SERIES': { 'CONFIG_STM32F103': { 'cpu': 'cortex-m3', - 'link_script': 'linkstm32f103xe.ld', - 'bin': 'build/stm32f103.bin' + 'link_script': 'linkstm32f103xe.ld' } }, 'BUILD': 'release' # or 'debug' } + +# 可选:构建结束后执行的动作(如生成 bin) +POST_ACTION = "$OBJCOPY -O binary $TARGET build/stm32f103.bin" ``` ## 命令行选项 @@ -218,7 +222,7 @@ groups = env.Bridge() ### 核心函数 -- `PrepareBuilding(env, project_root, proj_config)` - 初始化构建系统 +- `PrepareBuilding(env, proj_config)` - 初始化构建系统 - `DoBuilding(env, target, objs)` - 执行构建或导出 ### SCons 环境方法 diff --git a/ebuild/__init__.py b/ebuild/__init__.py index 5b5a679..ef5eea6 100644 --- a/ebuild/__init__.py +++ b/ebuild/__init__.py @@ -4,8 +4,8 @@ from .system import BuildSystem, prepare -def PrepareBuilding(env, project_root=None, proj_config=None): - return prepare(env, project_root, config_module=proj_config) +def PrepareBuilding(env, proj_config=None): + return prepare(env, config_module=proj_config) def DoBuilding(env, target, objs=None): diff --git a/ebuild/config.py b/ebuild/config.py index 6b43c08..d3cf5d5 100644 --- a/ebuild/config.py +++ b/ebuild/config.py @@ -17,8 +17,16 @@ def resolve_project_root(explicit_root=None): - root = explicit_root or os.getcwd() - return os.path.abspath(root) + if explicit_root: + return os.path.abspath(explicit_root) + try: + from SCons.Script import Dir + except Exception: + return os.path.abspath(os.getcwd()) + try: + return Dir('#').abspath + except Exception: + return os.path.abspath(os.getcwd()) class ConfigType(Enum): diff --git a/ebuild/groups.py b/ebuild/groups.py index 9798065..03e5df1 100644 --- a/ebuild/groups.py +++ b/ebuild/groups.py @@ -122,6 +122,8 @@ def _source_path(self, src: Any) -> str: def _project_root(env) -> str: if hasattr(env, 'GetProjectRoot'): return os.path.abspath(env.GetProjectRoot()) + if hasattr(env, 'GetWorkspaceRoot'): + return os.path.abspath(env.GetWorkspaceRoot()) return os.path.abspath(os.getcwd()) def _build_root(self, env) -> str: diff --git a/ebuild/system.py b/ebuild/system.py index d00ca6f..5739524 100644 --- a/ebuild/system.py +++ b/ebuild/system.py @@ -27,11 +27,11 @@ class BuildSystem: _current: Optional['BuildSystem'] = None def __init__(self, env, workspace_root: Optional[str] = None, project_root: Optional[str] = None) -> None: - if workspace_root is None: - workspace_root = project_root self.env = env + self.project_root = self._resolve_project_root(env, project_root) + if workspace_root is None: + workspace_root = self.project_root self.workspace_root = os.path.abspath(workspace_root or config.resolve_project_root()) - self.project_root = os.path.abspath(os.getcwd()) self.config_header = config.CONFIG_HEADER self.config = ConfigManager() @@ -42,6 +42,26 @@ def __init__(self, env, workspace_root: Optional[str] = None, project_root: Opti BuildSystem._current = self + @staticmethod + def _resolve_project_root(env, explicit_root: Optional[str]) -> str: + if explicit_root: + return os.path.abspath(explicit_root) + try: + from SCons.Script import Dir + except Exception: + Dir = None + if Dir is not None: + try: + return Dir('#').abspath + except Exception: + pass + if env is not None: + try: + return env.Dir('#').abspath + except Exception: + pass + return os.path.abspath(os.getcwd()) + @classmethod def current(cls) -> Optional['BuildSystem']: return cls._current @@ -90,9 +110,8 @@ def get_cc(env) -> str: env.AddMethod(lambda env: self.get_current_dir(), 'GetCurrentDir') env.AddMethod(lambda env, target, objs=None: self.do_building(target, objs), 'DoBuilding') env.AddMethod(lambda env: self.build_options, 'GetBuildOptions') - env.AddMethod(lambda env: self.workspace_root, 'GetProjectRoot') - env.AddMethod(lambda env: self.project_root, 'GetBSPRoot') - env.AddMethod(lambda env: self.workspace_root, 'GetRTTRoot') + env.AddMethod(lambda env: self.workspace_root, 'GetWorkspaceRoot') + env.AddMethod(lambda env: self.project_root, 'GetProjectRoot') env.AddMethod(get_cc, 'GetCC') env.AddMethod(lambda env: self, 'GetContext') @@ -165,6 +184,20 @@ def export_project(self, target: str, output_dir: Optional[str] = None) -> bool: def prepare_building(self) -> List[Any]: return self.merge_groups() + def _apply_post_action(self, target: Any) -> None: + config_module = self.env.get('config') + if config_module is None: + return + post_action = getattr(config_module, 'POST_ACTION', None) + if not post_action: + return + if isinstance(post_action, (list, tuple)): + for action in post_action: + if action: + self.env.AddPostAction(target, action) + else: + self.env.AddPostAction(target, post_action) + def do_building(self, target: Optional[str], objs: Optional[List[Any]] = None) -> Optional[Any]: from SCons.Script import Default, GetOption @@ -183,13 +216,9 @@ def do_building(self, target: Optional[str], objs: Optional[List[Any]] = None) - program = self.env.Program(target, objs) targets = [program] - bin_name = self.env.get('RTBOOT_BIN') - if bin_name: - bin_file = self.env.Command(bin_name, program, "$OBJCOPY -O binary $SOURCE $TARGET") - targets.append(bin_file) - if self.env.get('SIZE'): self.env.AddPostAction(program, "$SIZE $TARGET") + self._apply_post_action(program) Default(targets) return program @@ -267,10 +296,6 @@ def option_value(name: str, default: str = "") -> str: return ToolchainSettings(cc_prefix=cc_prefix, build_program=build_program) def _prepare_environment(self) -> None: - self.env['PROJECT_ROOT'] = self.workspace_root - self.env.setdefault('RTT_ROOT', self.workspace_root) - self.env['BSP_ROOT'] = self.project_root - tools_path = os.path.join(self.workspace_root, 'tools') if tools_path not in sys.path: sys.path.insert(0, tools_path) @@ -297,7 +322,7 @@ def _handle_menuconfig(self) -> None: if GetOption('menuconfig'): from .kconfig import menuconfig - menuconfig(self.workspace_root) + menuconfig(self.project_root) raise SystemExit(0) def _handle_attach(self) -> None: @@ -306,7 +331,7 @@ def _handle_attach(self) -> None: if GetOption('attach'): from .attach import GenAttachConfigProject - GenAttachConfigProject(self.workspace_root) + GenAttachConfigProject(self.project_root) raise SystemExit(0) def _load_config(self) -> None: @@ -340,7 +365,7 @@ def _normalize_sources(self, src: Any) -> List[Any]: def prepare(env, workspace_root: Optional[str] = None, project_root: Optional[str] = None, config_module=None) -> BuildSystem: if workspace_root is None: workspace_root = project_root - build = BuildSystem(env, workspace_root).setup() + build = BuildSystem(env, workspace_root, project_root).setup() from SCons.Script import Export Export(env=env) if config_module is not None: diff --git a/ebuild/toolchain.py b/ebuild/toolchain.py index 667487b..271e2a5 100644 --- a/ebuild/toolchain.py +++ b/ebuild/toolchain.py @@ -210,13 +210,13 @@ def apply_toolchain(env, config_module: Optional[Any] = None) -> None: def _resolve_mcu(env, mcu_series: Dict[str, Dict[str, str]]) -> tuple: for macro, entry in mcu_series.items(): if env.GetDepend(macro): - return (entry.get("cpu"), entry.get("link_script"), entry.get("bin")) + return (entry.get("cpu"), entry.get("link_script")) raise SystemExit("MCU series not configured, run: scons --menuconfig") -def apply_device_flags(env, project_root: str, config_module: Optional[Any] = None) -> Optional[str]: +def apply_device_flags(env, project_root: str, config_module: Optional[Any] = None) -> None: mcu_series = _get_attr(config_module, "MCU_SERIES", {}) - cpu, link_script, bin_name = _resolve_mcu(env, mcu_series) + cpu, link_script = _resolve_mcu(env, mcu_series) if not cpu or not link_script: raise SystemExit("MCU series not configured, run: scons --menuconfig") @@ -243,18 +243,11 @@ def apply_device_flags(env, project_root: str, config_module: Optional[Any] = No env.AppendUnique(CFLAGS=cflags, CXXFLAGS=cxxflags, ASFLAGS=asflags, LINKFLAGS=linkflags) env.AppendUnique(CPPPATH=[project_root]) - return bin_name -def setup_project(env, project_root: str, config_module: Optional[Any] = None) -> Optional[str]: +def setup_project(env, project_root: str, config_module: Optional[Any] = None) -> None: apply_toolchain(env, config_module) - bin_name = apply_device_flags(env, project_root, config_module) - env["RTBOOT_BIN"] = bin_name - if bin_name: - output_dir = os.path.dirname(bin_name) - if output_dir: - os.makedirs(output_dir, exist_ok=True) - return bin_name + apply_device_flags(env, project_root, config_module) __all__ = ["apply_toolchain", "apply_device_flags", "setup_project"]