构建独立的 Source SDK 消费工程¶
从经过复核的模板创建真实外部 C11 应用,再构建并运行 Native 模型。此路径需要 Linux、Python 3.11+、CMake 3.31+、Ninja、C 编译器与 Git。产品拥有装配和应用源码;Nexus 拥有可复用机制。本教程不烧录 MCU。
创建并运行应用¶
取得经过验证的 Source SDK 和外部 nexus-examples checkout。该仓库是私有仓库,需要单独的 GitHub 访问权限;公开 Nexus 或 SDK 的权限不包含模板权限。下方独立 SDK 验证不需要该私有仓库。将 NEXUS_SDK_PREFIX 设置为已安装 SDK prefix,将 NEXUS_EXAMPLES_DIR 设置为该 checkout,将 NEXUS_EXAMPLES_REVISION 设置为所选平台交付关联的精确 40 位示例 commit。将 NEXUS_EXPECTED_REVISION 设置为该已验证交付中的精确 40 位平台 commit。这些是独立身份:模板预期的平台 revision 不证明实际用了哪个 SDK。从 $NEXUS_SDK_PREFIX/share/nexus/src 执行以下命令,在两个输入 checkout 外选择新的产品目录:
: "${NEXUS_EXPECTED_REVISION:?select the reviewed platform commit}"
python cmake/package/package_source_sdk.py \
--verify "$NEXUS_SDK_PREFIX/share/nexus/src"
python tools/dev/dev.py create --templates "$NEXUS_EXAMPLES_DIR" \
--revision "$NEXUS_EXAMPLES_REVISION" --template native \
--output /tmp/my-nexus-product
cmake -S /tmp/my-nexus-product -B /tmp/my-nexus-product-build -G Ninja \
-DNexus_DIR="$NEXUS_SDK_PREFIX/lib/cmake/Nexus" \
-DNEXUS_EXPECTED_SOURCE_REVISION="$NEXUS_EXPECTED_REVISION"
cmake --build /tmp/my-nexus-product-build --parallel 2
ctest --test-dir /tmp/my-nexus-product-build \
--output-on-failure --no-tests=error
创建过程读取已提交的模板 blob 与其 SHA-256 manifest,不访问网络。不同 Git HEAD、变化的模板字节、symlink 或已有目标目录都会被拒绝。成功前先解析复制后的装配;失败只删除其新创建的目标。检查 .nexus-template.json:expected_platform 是模板源码锁中的预期;validation 记录实际检查的解析器与输入字节。
生成的产品包含 CMakeLists.txt、assembly.toml、main.c 与 README.md。Native 测试成功会输出 Nexus starter: transfer settled; platform stopped 并以零退出。Native UART 是软件模型,不是 host 串口。示例使用 strong factory 引用,提供请求存储,观察结清并停止平台。timeout 是错误,不能授权释放未结清存储。
使用开发 checkout 时,将两个已安装 SDK 的 CMake 参数替换为 -DNEXUS_SOURCE=/absolute/path/to/nexus。这仅验证该 checkout,不建立 SDK 资格。已维护的 MCU 模板选择 qiming、sky 或 liangshan。SDK 消费方增加 -DCMAKE_TOOLCHAIN_FILE="$NEXUS_SDK_PREFIX/share/nexus/src/cmake/toolchains/arm-gcc.cmake";源码 checkout 消费方则选择 -DCMAKE_TOOLCHAIN_FILE=/absolute/path/to/nexus/cmake/toolchains/arm-gcc.cmake。使用锁定的 ARM 编译器。MCU 链接属于软件证据;板卡启动和电气验证仍单独进行。
创建因配置被拒绝时,阅读报告中的文件、key 与约束。先检查所选 SDK/接口和固定模板版本。模板修正需要新的已复核提交,不能修改固定模板字节。成功创建后,编辑产品 authored 装配并重新配置;通过配置与所有权指南检查已复核 binding。不要修补生成文件或压制拒绝。保留产品源码;保存必要日志后只删除自有构建输出。
升级现有产品¶
选择新完成资格验证的 Source SDK 及其已复核的 40 位平台 commit。将 NEXUS_NEXT_SDK_PREFIX 和 NEXUS_NEXT_REVISION 设置为这两个输入;在产品和 SDK 目录之外使用新的构建目录:
: "${NEXUS_NEXT_SDK_PREFIX:?select the next verified SDK prefix}"
: "${NEXUS_NEXT_REVISION:?select the reviewed next platform commit}"
python "$NEXUS_NEXT_SDK_PREFIX/share/nexus/src/cmake/package/package_source_sdk.py" \
--verify "$NEXUS_NEXT_SDK_PREFIX/share/nexus/src"
cmake -S /tmp/my-nexus-product -B /tmp/my-nexus-product-upgrade-build -G Ninja \
-DNexus_DIR="$NEXUS_NEXT_SDK_PREFIX/lib/cmake/Nexus" \
-DNEXUS_EXPECTED_SOURCE_REVISION="$NEXUS_NEXT_REVISION"
cmake --build /tmp/my-nexus-product-upgrade-build --parallel 2
ctest --test-dir /tmp/my-nexus-product-upgrade-build \
--output-on-failure --no-tests=error
保留产品的 authored 源码和 .nexus-template.json;该文件记录模板来源,不表示当前选择的 SDK。不同 SDK prefix 之间不要复用旧的 CMake cache。两个源码 revision 可能声明相同的语义版本;显式源码 pin 会拒绝不同的 revision。pin 不匹配属于配置错误:保留诊断并选择已复核的输入。复核产品的 find_package() 版本要求及变化的装配约束;不要自动放宽该版本要求。接受升级前,重新执行产品要求的 Native 测试和 MCU 链接。旧 SDK 的结果不能建立新 SDK 的资格;链接成功也不能建立物理硬件资格。
维护者:验证完整 SDK 消费¶
验证示例仓库本身时,使用固定示例版本中的维护入口 scripts/local_ci.py 和双语 docs/local-ci.md。默认 scope 从新输出目录执行完整映射的本地工作流;--scope native、--scope arm 和 preset 选择仍是局部范围。显式 --nexus-source 必须匹配示例锁定版本;可选 --installed-sdk 也必须匹配同一版本。该工作流不会把受阻的托管 CI 改为通过,也不建立物理 HIL 资格。
以下各节是平台维护任务。先取得固定源码与递归依赖。从该检出执行,不使用任意默认分支。以下已维护合同检查独立 C/C++ 消费、搬移与包身份。
从仓库根目录执行,先初始化锁定的递归依赖。输出置于源码 checkout 外。下面这个确切测试调用生产打包器和维护中的消费者模板,将前缀搬移到含空格的路径,配置独立工程,构建两种语言并运行 Native 可执行文件:
python tests/contracts/test_source_sdk.py SourceSDKTests.test_relocated_c_and_cpp_consumer_build_and_run_without_original_source
成功要求非空 unittest 执行以 OK 结束,退出码为零。测试自动清理临时目录。失败保留实际命令诊断;编译器缺失不能记作消费者通过。夹具明确打包开发快照并显式允许消费;这验证消费流程,不建立可发布软件候选。
完整消费者由生产验证器维护,本页不复制实现。以下真实输入就是该测试执行的模板:
def prepare_consumer(prefix, directory, name, run_checked, *, fixture=False,
arm=False, chip="stm32", expected_revision=None,
version=None, contracts=False):
"""One public consumer template shared by qualification and boundary tests."""
prefix = Path(prefix)
sdk = prefix / "share/nexus/src"
if version is None:
version = load_policy(sdk)["semantic_version"]
source = directory / (name + " source")
build = directory / (name + " build")
source.mkdir()
# A parent Git identity must never become the SDK source identity.
run_checked(["git", "init", "--quiet", str(source)])
(source / "CMakeLists.txt").write_text(
'cmake_minimum_required(VERSION 3.31)\n'
'project(installed_source_consumer LANGUAGES C CXX ASM)\n'
'set(CMAKE_EXPORT_COMPILE_COMMANDS ON)\n'
f'find_package(Nexus {version} EXACT CONFIG REQUIRED)\n'
'find_package(Nexus CONFIG REQUIRED)\n'
'add_executable(parent_plain parent.c)\n'
'nexus_add_firmware(installed_c SOURCES main.c)\n'
'nexus_add_firmware(installed_cpp SOURCES main.cpp LIBRARIES '
'Nexus::Log Nexus::BMP280 Nexus::Storage Nexus::MappedStorage Nexus::AMPIPC Nexus::I3CController Nexus::UserIoBroker'
+ (' Nexus::STM32F407Handoff' if arm and chip == "stm32" else '') + ')\n'
'target_compile_options(installed_cpp PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:-fno-exceptions;-fno-rtti>")\n'
'get_target_property(sdk_root Nexus::Config NEXUS_SOURCE_DIR)\n'
'file(WRITE "${CMAKE_BINARY_DIR}/resolved-sdk.txt" "${sdk_root}")\n')
(source / "parent.c").write_text(
'#ifdef NEXUS_EFFECTIVE_CONFIG\n#error SDK leaked into unrelated parent target\n#endif\n'
'int main(void) { return 0; }\n')
code = ('#include "nexus_bindings.h"\n'
'#include "nexus_factory.h"\n'
'#include "nexus_config.h"\n'
'#ifndef NEXUS_PLATFORM_VERSION\n'
'#error "Missing source interface version"\n'
'#endif\n'
'#if NEXUS_FACTORY_ABI_EPOCH != 1U\n'
'#error "Unsupported factory interface epoch"\n'
'#endif\n'
'#if __has_include("stm32f4xx.h") || __has_include("gd32f4xx.h")\n'
'#error Vendor SDK leaked into product source\n#endif\n'
'#if __has_include("stm32f407_provider.h") || __has_include("gd32f470_provider.h")\n'
'#error Concrete provider leaked into product source\n#endif\n'
'int main(void) {\n'
' if (nx_factory_gpio(NX_GPIO_ID_COUNT) != 0) return 7;\n'
' nx_platform_start_result_t result = nx_platform_start();\n'
' if (result.primary != NX_SUCCESS) return 1;\n'
' return nx_platform_stop() == NX_SUCCESS ? 0 : 2;\n}\n')
(source / "main.c").write_text(code)
handoff = arm and chip == "stm32"
(source / "main.cpp").write_text(
'#include "nexus/components/log.h"\n'
'#include "nexus/components/bmp280.h"\n'
'#include "nexus/components/storage.h"\n'
'#include "nexus/components/mapped_storage.h"\n'
'#include "nexus/components/mapped_storage_owner.h"\n'
'#include "nexus/components/amp_ipc_storage.h"\n'
'#include "nexus/components/i3c_controller_storage.h"\n'
'#include "nexus/components/user_io_async_storage.h"\n'
+ ('#include "nexus/components/stm32f407_handoff.h"\n' if handoff else '')
+ '#include <cstring>\n' + code.replace(
' nx_platform_start_result_t result',
' nx_log_sink_port_t sink;\n std::memset(&sink, 0, sizeof(sink));\n'
' if (nx_log_init(nullptr, sink, NX_LOG_INFO) != NX_ERROR_INVALID) return 3;\n'
' if (nx_bmp280_compensate(nullptr, 0, 0, nullptr) != NX_ERROR_INVALID) return 4;\n'
' if (nx_mapped_storage_stop(nullptr) != NX_ERROR_INVALID) return 5;\n'
' if (nx_amp_ipc_construct(nullptr, nullptr) != NX_ERROR_INVALID) return 6;\n'
' if (nx_i3c_controller_construct(nullptr, nullptr) != NX_ERROR_INVALID) return 9;\n'
' if (nx_user_io_async_construct(nullptr, nullptr, 0U, nullptr) '
'!= NX_ERROR_INVALID) return 10;\n'
+ (' if (nx_stm32f407_handoff_initialize(0) != NX_ERROR_INVALID) return 8;\n'
if handoff else '') + ' nx_platform_start_result_t result'))
arguments = ["cmake", "-S", str(source), "-B", str(build), "-G", "Ninja",
"-DCMAKE_BUILD_TYPE=Release", f"-DNexus_DIR={prefix / 'lib/cmake/Nexus'}",
"-DNEXUS_BUILD_TESTS=OFF", "-DNEXUS_BUILD_WORKLOAD=OFF"]
if fixture:
arguments.append("-DNEXUS_ALLOW_SOURCE_SDK_FIXTURE=ON")
if contracts:
arguments.append("-DNEXUS_BUILD_WORKLOAD=ON")
if expected_revision:
arguments.append("-DNEXUS_EXPECTED_SOURCE_REVISION=" + expected_revision)
arm_assembly = "liangshan-baremetal.toml" if chip == "gd32" else "sky-baremetal.toml"
assembly = sdk / "tools/configure/assemblies" / (arm_assembly if arm else "native.toml")
arguments.append(f"-DNEXUS_ASSEMBLY_FILE={assembly}")
if arm:
arguments.append(f"-DCMAKE_TOOLCHAIN_FILE={sdk / 'cmake/toolchains/arm-gcc.cmake'}")
return source, build, arguments
维护者:准备可发布前缀¶
从已初始化、干净且已提交的 checkout 开始,选择自己拥有的新输出路径:
python cmake/package/package_source_sdk.py --source . --output /tmp/nexus-source-sdk
python cmake/package/package_source_sdk.py --verify /tmp/nexus-source-sdk/share/nexus/src
消费前搬移整个前缀,包括身份和许可。Source SDK 合同定义版本与接口 epoch 检查。内容文件缺失、额外、修改或符号链接文件会拒绝配置。使用 --development-fixture 生成的包保持不可发布,消费者必须显式设置 NEXUS_ALLOW_SOURCE_SDK_FIXTURE=ON。不得用该选项绕过发布资格。
SDK 保留锁定的官方 v6.4.0 来源中完整递归 CMSIS-Core Include 树及其 Apache-2.0 LICENSE。它不包含整个 CMSIS 仓库,也不自动启用 CMSIS-RTOS、CMSIS-Driver、DSP 或 NN。这些私有实现头与公共 Nexus::Core 中性机制不同。Device/startup 和 HAL/LL 仍分别锁定;GigaDevice 原包中的旧 Core 文件保留原始字节及通知,但不作为选中的共享 Core 来源。应验证移位后的完整 prefix,而不是手动复制的头文件子集。这项依赖更新不扩大芯片支持,也不替代当前源码的编译器与 consumer 执行证据。
正式安装前缀验证器除主机工具外,还要求锁定的 ARM 编译器位于 PATH。它构建 Native C/C++ 和维护中的 MCU 消费者:
python cmake/package/verify_consumers.py --prefix /tmp/nexus-source-sdk --output /tmp/nexus-source-sdk-verification
检查 verification.json、每个记录的退出码、Native 运行结果、MCU ELF/BIN/map 与资源报告。MCU 链接不执行硬件。验证器输出为专有目录;不相关的已有目录会拒绝替换。SDK 前缀与输出必须是没有符号链接别名的规范路径,且两者均不得包含另一方。路径被拒绝时,SDK 字节和既有报告保持不变;失败诊断写入 stderr。通过准入的专有输出会在下一次尝试前清空。仅在保留所需证据后删除自己创建的输出。
选择产品装配¶
产品在外部仓库维护 assembly.toml、main、执行任务和私有 Board 数据。通过 nexus_add_firmware() 消费 Nexus::Platform;该辅助函数不生成应用或启动调度器。使用装配参考选择复核绑定,通过factory 说明取得接口面。公开产品示例位于 nexus-examples。
FIG-10 表示当前软件消费流程。Runtime 与 MCU 链接证据和物理资格分别记录。
维护者:恢复交付且不移动已有 tag¶
发布维护者通常对 SDK 源码和交付工作流使用同一提交。源码建立标签后,若仅需修复交付工具,必须保持该标签不变。recover_tagged_source 输入默认为 false;只有显式选择恢复并设置 publish=true,才允许已标签源码与当前 main 工作流构建器使用不同提交。同源运行无需 builder_ci_run_id。
恢复前,打开该仓库的 Settings → Secrets and variables → Actions → New repository secret,创建 NEXUS_RELEASE_TOKEN。使用经批准且仅限本仓库的凭据,授予 Actions 读、Contents 写和 Workflows 写权限。凭据类型、有效期和批准要求见发布合同。仅在 GitHub 的 secret 输入框填写凭据值;不得通过聊天发送。将 GitHub 连接到助手并不会创建该 Actions secret。
恢复流程在 SDK 工作前拒绝缺少 secret 的运行。此检查仅证明 secret 已配置,不能证明其权限、批准状态或有效期符合要求。仅恢复流程的最终发布步骤使用该凭据;SDK 准备、下载与证明继续使用内置工作流凭据。源码与构建器使用同一提交的普通发布始终使用内置 token。
从仓库根目录操作,使用已认证且具备 Actions 写权限的 GitHub CLI。将 NEXUS_RELEASE_SOURCE 设为已标签的 40 字符源码提交,将 NEXUS_CI_RUN_ID 设为在 main 上针对该源码成功执行的完整 CI 运行编号,将 NEXUS_BUILDER_CI_RUN_ID 设为当前 main 构建器成功执行的完整 CI 运行编号。两次运行均须在过去 24 小时内创建;源码 CI 不能为不同的构建器建立资格。每次修复交付工具后,选择新构建器的 CI 运行,保持源码提交与标签不变。然后触发工作流:
gh workflow run release.yml --ref main \
-f expected_commit="$NEXUS_RELEASE_SOURCE" \
-f ci_run_id="$NEXUS_CI_RUN_ID" \
-f builder_ci_run_id="$NEXUS_BUILDER_CI_RUN_ID" \
-f publish=true -f recover_tagged_source=true
工作流根据实际 main 工作流与 checkout 确定构建器。恢复要求源码与构建器的版本策略字节完全相同、既有标签指向该源码、实时 main 引用指向该构建器。SDK、文档和源码 CI 保持已标签源码的身份,交付记录另行标明构建器。策略不一致时拒绝准入;资产上传前后均复核这两个引用。
修复打包限制时保留全部原始证据文件。不得为了获得成功而移动标签、删去证据或换用不同源码的 CI 运行。仅触发工作流不能证明已发布:必须检查完成的工作流、发布清单与资产,再独立下载并验证 SDK。恢复不建立物理 HIL 或生产资格。