欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/
欢迎在 PC 社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
如有项目源码,可上传至 AtomGit 仓库,可在博文内附上仓库链接。


鸿蒙 PC 高难度适配实战:GCC-Ada 9.5.0 从 Ada 自举到真机可用的 GNAT 工具链链

图1 PR !6116 已合入主线,页面显示已合并、审查人 9/1 已通过、测试人 6/1 已通过、评审人 1/1 已通过
在这里插入图片描述

这次适配不是“换个编译器再编一遍”,而是把 GCC 9.5.0 的 Ada/GNAT 编译器这一整套工程,从 Alpine musl/AArch64 GNAT 9.3 自举种子起步,在鸿蒙 PC 上从零完整跑通——鸿蒙 PC 生态第一次有了一套可签名、可运行、能编 Ada 程序的 GNAT 工具链。配方位于 archives/g/gcc-ada/9.5.0,对应 PR !6116,已于 2026-09-16 合入 OpenHarmonyPCDeveloper/build_in_harmonyos 主线。

1.背景

GCC Ada(GNAT)是 GCC 的 Ada 语言前端。Ada 这门语言在航空航天、轨道交通、安全关键系统这些领域被广泛采用——强类型、编译期检查、任务(tasking)和受保护对象(protected object)让它在高可靠软件里不可替代。

它对鸿蒙 PC 的意义“不是多了一个编译器”,而是让鸿蒙 PC 生态第一次有了 Ada/GNAT 工具链这个底座。很多依赖 Ada 的工业软件、教学项目,过去在 OpenHarmony 上根本无从编译,现在有了。

关键难点:
GCC 的 Ada 前端(GNAT)和其他前端不一样——它自己就是用 Ada 写的。

这有一个大的死结:想编出 gnat1(Ada 编译器),前提是得有一个能跑的 GNAT;而你想要的那个 GNAT,恰恰是要用 gnat1 去编的。这就是“先有鸡还是先有蛋”的 Ada 自举。

所以 GCC Ada 的难度不是“改了多少构建脚本”,而在于它有一个难题:必须先造出一个能用的 GNAT,才能开始造目标 GNAT。

本次适配的成果:

项目结果
安装后上游可执行子集41/41(30 GNAT 回归 + 11 ACATS)
AdaCGI 1.6 下游联动13/13 断言通过
真机消费者验证6/6 检查通过
自举依赖9 个 Alpine GNAT 9.3 seed APK(SHA-256 全部固定,仅构建期)
版本输出GNAT 9.5.0(aarch64-linux-ohos, musl)

2. 环境:这次适配需要准备什么

编译器类项目和普通库不一样,先把两边的环境说清楚,后面排查问题才容易区分“是配方的问题”还是“是环境的问题”。

构建机(跑 Conan 配方、产出制品):

  • Conan 2(本次 2.29.1)。配方还依赖 dejagnu/1.6.3.1、expect/5.45.4、tcl/8.6.14 三个工具,用来跑 GCC 上游的 Ada 测试套件;
  • HarmonyOS SDK,提供交叉工具链:clang/clang++、sysroot、llvm-ar/llvm-ranlib;
  • 宿主的 GNU as / ld。本包不含汇编器和链接器,汇编由宿主 binutils 完成——鸿蒙 PC 上它们在 /data/service/hnp/bin(GNU as 2.45),这个目录不在默认 PATH,不加会报 gcc.real: fatal error: cannot execute 'as': execvp: Permission denied;
  • profile 必须对齐:OHOS / armv8 / Release / clang 15 / C++17 / libc++。host 编译器的 C++ 标准或运行库不一致,Conan 会算出另一个 package ID,消费者就会“下载到 A 包、请求 B 包”。

真机(跑消费者验证):

  • 一台配置了社区适配环境的 HarmonyOS PC(本次用 HUAWEI MateBook Pro,HarmonyOS 6.1,aarch64);
  • 可写、且不在 HMDFS 分区的目录。包放在 /storage(HMDFS 挂载)时,签名后的 ELF 也无法执行,会报 Operation not permitted,必须先复制到非 HMDFS 分区;
  • binary-sign-tool:鸿蒙的代码签名工具,编译产物必须先自签名才能运行,strip 过的 ELF 同样会被拒绝。

顺带一个成本数据:本包一次完整构建(conan-build-test)实测耗时 55 分 21 秒——这是编译器类项目的正常量级,也解释了后面为什么要把日常门禁和全量测试分开跑。

3. 其他难点以及上游假设和鸿蒙 PC 的差异

GCC Ada 的构建假设来自常规 Linux 桌面环境,而鸿蒙 PC 在多个地方与它有很多实质差异。真机跑了第一轮构建后,实际问题大多都集中在下面几处——其中“自举”是排在第一位的。

差异点常规 Linux 假设鸿蒙 PC 实际情况
自举有现成 GNAT 可直接引导无任何 Ada 编译器,必须从 Alpine seed 起步
平台探测config.guess/config.gcc 认识目标平台不认识 OHOS,需显式传 triplet(aarch64-linux-ohos)
文件系统常规 POSIX 文件系统HMDFS(/storage 挂载)上 mmap(MAP_XPM) 报 EACCES、临时目录受限
依赖声明glibc 头文件声明齐全musl 缺 pthread_cancel 等三个接口,Ada 运行时编译报错
运行时异常自带 unwind-dw2 正常抛异常unwind-dw2 异常抛出时挂起,需换 SDK libunwind.a
二进制执行编译产物可直接执行未签名 ELF 无法执行(代码签名体系)

4.适配方案:从种子开始

4.1 第一步解决自举问题用 Alpine GNAT 9.3 做构建期种子

我通过使用 Alpine Linux 3.12 的 AArch64/musl GNAT 9.3.0 当“种子”来解决这一问题

  • 9.3 是比 9.5 要老的,这符合 GCC 自举建议
  • 它是 musl/aarch64 的,和鸿蒙 PC 的执行模型一致(seed 的 ELF 解释器是 /lib/ld-musl-aarch64.so.1)
  • gnat、gnatmake、gnatbind、gnatlink、gcc 只需 musl;gnat1 额外需要 libisl.so.15、libmpc.so.3、libmpfr.so.6、libgmp.so.10、libz.so.1,全部从同一 Alpine 仓库固定;
  • 9 个 APK 的下载地址和 SHA-256 全部固定在 conandata.yml 里
  • 这个只在构建时启用,不存在于最终制品

4.2 在鸿蒙 PC 上成功运行 Alpine 的 ELF:XPM loader + 签名代理

  1. XPM loader:鸿蒙的 musl 动态链接器需要支持一种可执行内存映射(MAP_XPM)。补丁 0001-musl-ohos-xpm-loader.patch 为 ldso/dynlink.c 增加 MAP_XPM 探测,并调整 LOAD 段映射(可写段改为匿名映射 + pread 填充),使签名后的 ELF 能被 seed 加载。
  2. 短 NEEDED 名重写:把 seed 依赖的动态库名改写成短名,配合 loader 定位,解决 musl 依赖链加载。
  3. 签名原生代理:鸿蒙 PC 执行不了 shell 脚本式的驱动。我用 /proc/self/exe 解析相邻的隐藏 wrapper,把 gcc、gnatmake、gnatbind、gnatlink 包成能签名、能执行的原生代理。

4.3 分段编译出完整的 GNAT 工具链

./configure \
  --prefix=/ --libdir=/lib --libexecdir=/libexec \
  --build=aarch64-linux-ohos --host=aarch64-linux-ohos --target=aarch64-linux-ohos \
  --enable-languages=c,ada \
  --enable-threads=posix \
  --enable-libada \
  --disable-bootstrap \
  --disable-lto --disable-plugin --disable-multilib --disable-nls --disable-shared \
  --disable-libatomic --disable-libitm --disable-libsanitizer \
  --disable-libgomp --disable-libquadmath --disable-libssp --disable-libvtv \
  --disable-werror \
  --without-isl \
  --with-sysroot=<OHOS sysroot> \
  --with-native-system-header-dir=/usr/include

# 分阶段:all-gcc -> target libgcc -> PIC target libada -> GNAT tools

不能一次性全部构建完成,必须拆开,每一步的产物都需要签名。
到这里,就解决了自举问题,编译出来目标 GCC 9.5.0 和整套 GNAT 工具链。

5. 除了自举外还有几个难点

5.1 pthread 三个接口缺失

Ada 运行时 tasking 相关的源文件,编译时经常报未定义。发现是鸿蒙的 musl 没有 pthread_cancel、pthread_mutexattr_setprioceiling、pthread_rwlockattr_setkind_np,而 GCC 9.5 的 Ada/POSIX 运行时默认它们都在。处理分两处,都固化在 patches/0002-gcc-ada-ohos-build-rules.patch 里。

第一处是 libgcc/gthr-posix.h,用 OHOS 守卫把 pthread_cancel 的弱引用排除掉:

--- a/libgcc/gthr-posix.h
+++ b/libgcc/gthr-posix.h
@@ -108,7 +108,7 @@
 __gthrw(pthread_equal)
 __gthrw(pthread_self)
 __gthrw(pthread_detach)
-#ifndef __BIONIC__
+#if !defined (__BIONIC__) && !defined (__OHOS__)
 __gthrw(pthread_cancel)
 #endif

Ada 运行时侧(gcc/ada/libgnarl/s-taprop__linux.adb),把两个缺失调用注释掉、结果直接置 0:

--- a/gcc/ada/libgnarl/s-taprop__linux.adb
+++ b/gcc/ada/libgnarl/s-taprop__linux.adb
-         Result := pthread_mutexattr_setprioceiling
-           (Mutex_Attr'Access, Prio_To_Linux_Prio (Prio));
+         --  pthread_mutexattr_setprioceiling is absent on OHOS.
+         Result := 0;
          pragma Assert (Result = 0);

-            Result := pthread_rwlockattr_setkind_np
-              (RWlock_Attr'Access,
-               PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);
+            --  pthread_rwlockattr_setkind_np is absent on OHOS.
+            Result := 0;
             pragma Assert (Result = 0);

这里可以总结出来一处经验:musl 上 pthread_* 报缺,可以先看看是不是 glibc 私有扩展,不用急着加其他库。

5.2 unwind-dw2 异常挂起 → 换 libunwind.a

发现 Ada 程序一 raise 异常,进程不是报错或者退出,而是直接挂在那里不动了。最后定位到 GCC 9 自带的 native unwind-dw2 在这平台上会在异常抛出路径最后卡死。

通过改用 OHOS SDK 的 AArch64 静态 libunwind.a,且只在链接阶段 whole-archive 注入,不伪装成普通运行时依赖。替换后异常程序正常退出 0。但是必须把边界说清楚:libunwind.a 是链接期注入,不动源码和运行时语义。

6.其他难点:HMDFS、lld 与平台探测

6.1 HMDFS 上 mmap(MAP_XPM) 报 EACCES

seed probe 阶段,对短名依赖 mmap(MAP_XPM) 返回 EACCES,发现是这种文件系统不支持该映射方式。通过把 bootstrap 目录固定到非 HMDFS 的 app 目录,并做写入探测即可解决。

# conanfile.py(节选)
@property
def _bootstrap_work_root(self):
    # The general cache root is writable but rejects signed executable maps.
    base = Path("/data/storage/el2/base/haps/entry/files")
    root = base / "gcc-ada-bootstrap-{}".format(os.getpid())
    probe = root / ".write-probe"
    try:
        root.mkdir(parents=True, exist_ok=True)
        probe.write_bytes(b"gcc-ada")
        probe.unlink()
    except OSError as error:
        ...
        raise ConanInvalidConfiguration(
            "XPM bootstrap directory is not writable: {} ({})".format(
                base, error
            )
        ) from error
    ...

6.2 lld 找不到配套 libxml2.so.16

bootstrap loader 构建时,SDK 的 lld 报 xmlFreeDoc 等符号缺失。发现是 lld 依赖 SDK 里同目录的 llvm/lib/libxml2.so.16,但运行库搜索路径没覆盖到。通过从已验证的 sysroot 反推当前 SDK 的 llvm/lib,前置到 bootstrap loader 子进程的 LD_LIBRARY_PATH,并把后续 GCC/GNAT 构建环境同步前置即可解决

7.测试统计

编译器类项目,验证方法不是库能不能被链接,而是能不能把大量源码变成目标产物。
我复用了 DejaGNU/Expect/Tcl,在构建树里跑 GCC 上游的完整 check-gnat + check-acats。

测试集合数量结果说明
上游全量 check-gnat/check-acats(CI #42755)35423542/3542一次性完整证据,失败 0、跳过 0
上游清单未形成独立记录的入口773不计为通过4315-3542,不虚报
鸿蒙特有测试2020/20OHOS 制品消费者 7 条断言 + AdaCGI 1.6 下游联动 13 条断言
安装后 GNAT 回归子集(门禁复跑)3030/30从 gnat.dg(共 2034 文件)中选取,{ dg-do run }
安装后 ACATS 子集(门禁复跑)1111/11从 ACATS(共 2595 文件)中选取
真机消费者检查66/6容器 / 异常 / task / 保护对象等

如实说清楚口径:完整 build-tree check-gnat/check-acats 跑过一次,CI #42755 得 3542/3542(100%);上游清单总量 N=4315,其中 773 项没有独立结果记录,不计为 PASS;日常门禁复跑的是安装后 41 项子集(30 GNAT + 11 ACATS);另有鸿蒙特有测试 20 条(OHOS 消费者 7 + AdaCGI 13)。主分母 3562 = 3542 + 20。

UPSTREAM_GNAT_DG_SUBSET=30/30
UPSTREAM_ACATS_SUBSET=11/11
UPSTREAM_ADA_SUBSET_TOTAL=41/41

跑这套子集时也踩过测试运行环境的问题,全出在“路径/签名/运行库”上:dg-extract-results.sh 写不进 mode-0700 目录(把 TMPDIR 挪到 XPM 文件系统)、ACATS 的 macrosub.adb 没生成(把 Alpine seed 的 gnatchop 也走 XPM loader 包一遍)、链接时 musl 符号解析失败(补 -Wl,-rpath-link,…)。总结:编译器类项目的测试,问题几乎都出在“运行环境”上,而不是代码本身。

8.消费者验证的重要性

除了完成基础测试,还必须从消费者角度验证是否能用 GCC-Ada 的消费者测试调用安装后的 gnatmake,真实编译一个 Ada 程序(覆盖泛型容器、异常处理),断言可执行产物生成、签名后运行输出精确匹配。

-- test_package/test_gcc_ada.adb(真实源码)
with Ada.Containers.Vectors;
with Ada.Strings.Fixed;
with Ada.Text_IO;

procedure Test_Gcc_Ada is
   Checks        : Natural := 0;
   Platform_Name : constant String := "HarmonyOS";

   procedure Check (Condition : Boolean; Name : String) is
   begin
      if not Condition then
         Ada.Text_IO.Put_Line ("FAILED: " & Name);
         raise Program_Error;
      end if;
      Checks := Checks + 1;
   end Check;

   protected Shared_Counter is
      procedure Increment;
      function Value return Natural;
   private
      Current : Natural := 0;
   end Shared_Counter;

   protected body Shared_Counter is
      procedure Increment is
      begin
         Current := Current + 1;
      end Increment;

      function Value return Natural is
      begin
         return Current;
      end Value;
   end Shared_Counter;

   task Worker is
      entry Run;
   end Worker;

   task body Worker is
   begin
      accept Run do
         Shared_Counter.Increment;
      end Run;
   end Worker;
begin
   Check (6 * 7 = 42, "integer arithmetic");
   Check (Platform_Name'Length = 9, "string attributes");
   Check
     (Ada.Strings.Fixed.Index (Platform_Name, "OS") = 8,
      "standard string runtime");

   declare
      package Integer_Vectors is new Ada.Containers.Vectors
        (Index_Type => Positive, Element_Type => Integer);
      Values : Integer_Vectors.Vector;
   begin
      Integer_Vectors.Append (Values, 21);
      Integer_Vectors.Append (Values, 21);
      Check
        (Integer_Vectors.Element (Values, 1)
         + Integer_Vectors.Element (Values, 2) = 42,
         "generic containers");
   end;

   declare
      Caught : Boolean := False;
   begin
      begin
         raise Constraint_Error;
      exception
         when Constraint_Error =>
            Caught := True;
      end;
      Check (Caught, "exception handling");
   end;

   Worker.Run;
   Check (Shared_Counter.Value = 1, "tasking runtime");

   if Checks /= 6 then
      raise Program_Error;
   end if;
   Ada.Text_IO.Put_Line ("gcc-ada checks passed:" & Natural'Image (Checks));
end Test_Gcc_Ada;

它的结果契约是:

full_package_exit_code=0
gcc-ada checks passed: 6
GCC_ADA_FULL_PACKAGE_DIAGNOSTIC=pass

另外还有一条真实的下游联动路径:AdaCGI 1.6 用打包后的 gnatmake/gnatbind/gnatlink 编译、绑定、链接、签名,跑一个带 13 条 fail-closed 断言的 CGI 请求,验证器输出 ADACGI_LINKAGE=pass assertions=13。这类测试证明的是“发布后的包能被上层组件正常调用”,与编译通过是不同维度的证据。

9.真机运行与复现

适配完成之后,在同一台设备上做到端到端复现。但是 GNAT 的一个特点:编译成功不一定打印输出,所以重点看产物和运行结果。

先确认制品仓和安装环境(图2):

在这里插入图片描述

版本输出(图3):

在这里插入图片描述

$ gnatmake --version
GNATMAKE 9.5.0
Copyright (C) 1995-2019, Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

完整的编译 → 签名 → 运行流程(图4):

在这里插入图片描述

$ gnatmake test_gcc_ada.adb -o test_gcc_ada
$ ./test_gcc_ada
sh: permission denied: ./test_gcc_ada          ← 未签名,被拒
$ /data/service/hnp/bin/binary-sign-tool sign -selfSign 1 \
    -inFile test_gcc_ada -outFile test_gcc_ada.signed -signAlg SHA256withECDSA
09-24 21:19:53.770 INFO  - add codesign section success
09-24 21:19:53.812 INFO  - write code sign data success
$ chmod +x test_gcc_ada.signed
$ ./test_gcc_ada.signed
gcc-ada checks passed: 6

这里还有两个 HMDFS/签名体系的细节:

  1. 测试时不能直接在包目录里跑编译器——包位于 /storage(HMDFS),签好名的二进制在 HMDFS 上执行会报 Operation not permitted。测试脚本先把整个包复制到非 HMDFS 分区,再在那里执行。
  2. 签名是硬门槛——编译出来的可执行文件必须先 binary-sign-tool 签名,才能在鸿蒙 PC 上运行;strip 过的 ELF 同样报 Operation not permitted。

本包不含 as/ld,汇编靠宿主 GNU as(/data/service/hnp/bin/as,Binutils 2.45),而该目录不在默认 PATH,不加会报 gcc.real: fatal error: cannot execute ‘as’: execvp: Permission denied。

如果你有配置了社区适配环境的鸿蒙 PC 设备,可以照下面完整步骤从零复现(装到能跑 gnatmake):

# 1. 加制品仓 remote
conan remote add ohpcd https://conan.cnb.cool/OpenHarmonyPCDeveloper/Conan/-/packages/

# 2. 确认 gcc-ada 包在制品仓
conan list "gcc-ada/*" -r=ohpcd

# 3. 安装(settings 必须对齐 CI 构建值,否则报 os.version 未定义)
conan install --requires=gcc-ada/9.5.0 -r=ohpcd \
  -s os.version=6.0 -s compiler.libcxx=libc++ -s compiler.cppstd=17 \
  -g VirtualRunEnv

# 4. 激活运行环境(让 gnatmake 进 PATH)
source conanrun.sh

# 5. 验证
which gnatmake
gnatmake --version

10.实战拆解:问题是怎么解决的

第一步:先解决自举这个死结。拿到一个在目标平台没有现成编译器的库,第一步应该是检查它的编译前提,而不是改构建参数。GCC-Ada 的关键前提不是某个头文件,而是“必须有一个 GNAT 才能编 GNAT”。只要 C-only 的 GCC 配方没有 Ada 前端,改 CC、CFLAGS 都无济于事。

第二步:选定构建期种子。把“推测平台没有 GNAT”变成可复现方案:用版本更老(9.3 < 9.5)、校验固定(9 个 APK 的 SHA-256)、且执行模型一致(musl/aarch64)的 Alpine GNAT 做 seed,并且只用于构建期。

第三步:使种子运行起来。用 seed 的 gnatmake -c 编一个 Ada 单元,如果 mmap(MAP_XPM) 在 HMDFS 上 EACCES,就把执行目录迁移到非 HMDFS 分区;如果短名依赖加载失败,就上 XPM loader + 短 NEEDED 重写。

第四步:分段编译 GNAT 工具链。不一次性编译完成,按 all-gcc → target libgcc → PIC target libada → GNAT tools 顺序,每阶段产物都签名。

第五步:处理 musl 运行时差异。pthread 三接口缺失用源转换替换;unwind-dw2 异常挂起换 SDK libunwind.a(链接期注入)。

第六步:跑全量 check-gnat + check-acats。 复用 DejaGNU/Expect/Tcl,用临时签名的 xgcc 代理跑完整套件,逐个解决 TMPDIR、gnatchop、rpath-link 三个问题

第七步:消费者验证。 从安装包调用 gnatmake 真实编译 Ada 程序,断言产物生成 + 签名后运行输出精确匹配

第八步:正确汇报统计 + 原子性审计。 测试分母必须写清 N=4315 / N’=3542 / 773 项未计通过;提交前 git diff --cached --name-only、git diff --check 核对只含 gcc-ada 文件,临时日志、缓存和其他库修改不进提交

11.FAQ

Q1:Alpine 的 GNAT 种子是不是一个“偷偷塞进来的”路径依赖?

不是。它是构建期输入,不是运行时依赖:

  • 9 个 APK 的下载地址和 SHA-256 全部固定在 conandata.yml 里,可复现、可审计;
  • 只用于构建期引导(9.3 < 9.5,符合 GCC 自举建议),编完即弃;
  • 不进最终制品,也不在 PATH 里“恰好存在”。消费 gcc-ada/9.5.0 时,运行依赖是 required: [](GMP/MPFR/MPC 都是源码内置)。

这和仓库既有的编译器类规范一致:可信二进制工具链是受校验的源码输入。

Q2:为什么完整的上游测试套件不放在每次门禁里跑?

因为跑不动。check-gnat + check-acats 全量执行会超过默认 3600 秒的 conan create 窗口(实测在 ACATS 阶段超时,见 CI #42817)。所以配方把全量路径保留为显式的证据复跑:设 GCC_ADA_FULL_UPSTREAM_TESTS=1 才会跑完整套件。日常门禁则跑“完整构建 + 安装后 41 项上游子集 + 20 条消费者断言”,在门禁窗口内完成。

需要说明的是:CI #42755 那次确实跑完过完整套件,3542/3542 就是它的结果。

Q3:为什么只做 Ada 前端,不做 C++ / Fortran?

这次的目标是把自举链闭合:Ada 前端是唯一“自己用自己写”、没有现成编译器就无法构建的前端,也是难度所在。C/C++ 前端可以由宿主 clang 交叉编译,不构成自举问题;Fortran(gfortran)可以作为后续独立任务。

把边界写清楚比“声称全都支持”更有用——本包实际启用的是 --enable-languages=c,ada。

Q4:链接期注入 libunwind.a,算不算改了运行时语义?

不算,但边界必须写死:

  • 注入只发生在链接阶段(whole-archive),不改源码、不改 Ada 运行时;
  • 它替换的是 GCC 自带的 native unwind-dw2 在鸿蒙上的异常展开路径(原路径会挂起);
  • 它不是 Conan 运行时依赖,不伪装成普通库依赖。

换句话说:修的是"异常展开在这平台上跑不通",不是"改写异常语义"。

Q5:上游清单 4315 项,为什么只执行了 3542 项?是不是少报了?

不是少报,是两个口径:

  • 4315 是上游测试入口的清单总量(作为正文模板里的"上游用例数");
  • 3542 是 CI #42755 里实际形成了独立结果记录的数量,3542/3542 全部通过;
  • 差额 773 项没有在 CI 里形成独立结果记录,不计为已执行,也不计为 PASS。

所以 3542/4315 = 82.1% 是清单覆盖率,不是通过率;通过率是 3542/3542 = 100%。这两个数在同一份报告里分开列,不合并。

12.小结

本次适配属于高难度,本质来自于 Ada 自举:要编出 Ada 编译器,得先有一个 Ada 编译器——于是从固定校验的 Alpine GNAT 9.3 种子出发,经 XPM loader、签名原生代理、分阶段编译,才“用 Ada 编出了 Ada”。自举之外,musl 的 pthread 能力差异和 unwind-dw2 异常挂起,又分别考验了 Ada 运行时的 tasking 和异常展开;HMDFS 与签名体系则是所有鸿蒙 PC 适配都要过的共性关。

最终成果:上游实际执行结果 3542/3542、安装后上游子集 41/41、OHOS/消费者断言 20/20 全部通过。gcc-ada 9.5.0 已能在鸿蒙 PC 完成完整 Conan 构建,并对主要产物做了真实功能检查。后续值得补的方向有两个:一是配合鸿蒙签名方案恢复 strip 以优化体积;二是用更复杂的 Ada 工程验证编译器输出等价性。

可复用的方法,都是这次拿真实踩坑换来的:

  • 自举种子要“更老 + 固定 + 只用一次”,三条缺一不可——Alpine GNAT 9.3 比目标 9.5 老(符合 GCC 自举建议),9 个 APK 的 SHA-256 全固定(可复现、可审计),构建完就丢(不混进运行时)。少一条:版本不老可能自举失败,校验不固定不可复现,混进运行时就成了 undeclared 依赖。

  • 先让最小一件事跑通,再上全量——先用 seed 的 gnatmake -c 编一个 seed_probe.o,确认整条 seed 链路通,才进入正式 GCC 构建。别一上来就跑全量,否则失败点全搅在一起,根本没法定位。

  • 编译器类项目,运行环境比代码更常出错——跑上游子集时踩的坎(TMPDIR 写不进、gnatchop 未生成、rpath-link 缺路径),全是"路径/签名/运行库/加载器"问题,没有一个是真正的代码 bug。

  • 链接期注入 ≠ 运行时依赖,这条边界要写死——libunwind.a 只在链接阶段 whole-archive 注入,不冒充运行时 Conan 依赖;seed 只用于构建期,不冒充 PATH 工具。

  • 测试口径不能混——安装后子集(41/41)、AdaCGI 下游(13/13)、真机消费者(6/6)、上游全量执行(3542/3542)分开统计;上游 gnat.dg 的 2034 个文件、ACATS 的 2595 个文件只是"文件数",不等于"执行通过数"。

编译器是生态的地基,而 Ada 编译器这块地基,比别的更难打——因为它得先自己把自己造出来。

参考文献

[1] GCC, the GNU Compiler Collection. https://gcc.gnu.org/

[2] GNAT User’s Guide / GCC 9.5.0 Ada 文档.

[3] OpenHarmonyPCDeveloper/build_in_harmonyos — gcc-ada 9.5.0 适配配方(archives/g/gcc-ada/9.5.0),PR !6116. https://atomgit.com/OpenHarmonyPCDeveloper/build_in_harmonyos

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐