欢迎加入开源鸿蒙PC社区:https://harmonypc.csdn.net/
欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper

项目源码(AtomGit 仓库):https://atomgit.com/OpenHarmonyPCDeveloper/build_in_harmonyos


把 8051 编译器搬到鸿蒙PC:SDCC 4.6.0 高难度适配复盘

原创

一句话概括:这次适配不是"换个编译器再编一遍",而是把一个内嵌了整棵裁剪版 GCC 12.1.0 构建树的编译器工程,在鸿蒙PC 的 HMDFS 文件系统上完整跑通——最终 75 个目标架构库全部编译通过。配方位于 archives/s/sdcc/4.6.0,对应 PR !5660,已于 2026-08-20 合入 OpenHarmonyPCDeveloper/build_in_harmonyos 主线。

1 背景

在这里插入述
图1 PR !5660 复审通过并合入主线(开源鸿蒙PC社区 build_in_harmonyos 仓库)
SDCC(Small Device C Compiler)是面向 8/16 位单片机的开源 C 编译器,支持 8051、Z80、STM8、mos6502、PIC、HC08、PDK 等 20 多个目标架构。在嵌入式开发里,它常被用来替代厂商的私有工具链——很多开源固件和教学项目直接依赖它产出的 .ihx 固件。

把它搬上鸿蒙PC 的意义,不在于"多了一个能跑的编译器",而在于嵌入式工具链在 OpenHarmony 生态里有了一个可复用的底座:这类编译器是固件开发链的入口,一旦它能在鸿蒙PC 上稳定构建与运行,后续的烧录、调试、仿真工具就有条件逐步接过来。

从工程难度看,SDCC 有两个特点让它区别于普通的 C/C++ 库:

  1. 它是一个编译器,验证方式不是"库能被链接",而是"能否真的把源码编译成目标固件";
  2. 它的发布源码里内嵌了一棵裁剪过的 GCC 12.1.0 构建树support/cpp,用于预处理)。这棵树在 sdist 打包时被裁掉了若干关键文件,只有真正把 make 跑到那一步才会暴露

本次适配的成果:

项目结果
全架构 device lib 编译75/75 通过(20+ 目标架构全部产出 .lib
消费者冒烟编译1/1 通过(8051 源码 → .ihx 固件)
依赖解析bison / boost / flex / zlib 全部从 ohpcd 制品仓解析
版本输出SDCC : ... TD- 4.6.0 #16555 (Linux)

2 上游假设与鸿蒙PC 环境的差异

SDCC 的构建假设来自常规 Linux 桌面环境,而鸿蒙PC 在四个层面与它有实质差异。源码树放进真机跑第一轮构建后,实际问题集中在下面几处:

差异点上游假设鸿蒙PC 实际情况
平台探测config.guess 认识目标平台2018 版 config.guess 不识别 OHOS,configure 直接失败
文件系统常规 POSIX 文件系统HMDFS/storage 挂载)上 getcwd 失败、临时目录权限受限
依赖声明glibc 头文件声明齐全musl 未声明 pthread_setcancelstate,boost 编译报错
可选组件gputils 可用(PIC 工具链)制品仓没有 gputils,PIC 端口无法构建
二进制可执行性strip 后可正常执行strip 后的 ELF Operation not permitted(代码签名体系)
源码完整性内嵌 GCC 树完整sdist 裁掉了 6 处关键文件/目录,编译到那步才报错

这些差异单个都不算太难,麻烦在于它们会在一次构建里同时出现,而且互相纠缠——尤其文件系统与源码完整性这两类,会分别把构建链在不同阶段打断。

3 适配流程

这次适配在 AtomCode 上完成,流程本身也是可复用的:先侦察确认源码地址与校验值 → 写 Conan 配方和平台补丁 → 真机跑构建 → 失败后诊断、修复、复核

回头看,这套流程里最关键的一条是 “先取证,再动手”

  • 每个问题必须能看到旧状态失败、新状态通过的对照才算闭环,不靠"看起来应该修好了";
  • 禁止吞错误、禁止放宽容差、禁止跳过用例;
  • 修之前先确认上游是否已有修复,避免重复造轮子或掩盖真实根因。

也正因为这条,适配中复用了社区知识库里的既有结论(如 E007 config.guess 绕过、E011 HMDFS 权限处理、以及针对 hmdfs 脚本执行与签名体系的既有条目),把精力集中在知识库尚未覆盖的部分——也就是下面两节的核心难点。

4 核心难点一:HMDFS 上的 getcwd 与权限

4.1 config.status 的临时目录被拒

首个拦路虎出现在 configure 阶段。autoconf 生成的 config.status 会这样创建临时目录来写中间文件:

tmp=`(umask 077 && mktemp -d "./confXXXXXX") 2>/dev/null`

在 HMDFS 上,用 umask 077 建出的目录后续写入被拒:

./config.status: can't create ./confXXXXXX/subs1.awk: Permission denied

更麻烦的是,SDCC 有多个递归 autotools 子工程support/cppsim/ucsim 等),每个子工程都有自己的 configure / config.status——只改顶层没有用。因此适配脚本采用递归遍历 + 精确替换,把临时目录改到 $TMPDIR 下、并把 umask 放宽:

# 递归 patch 每一个 configure:临时目录改到 $TMPDIR,umask 077 → 022
s2 = s.replace('mktemp -d "./confXXXXXX"',
               'mktemp -d "$TMPDIR/confXXXXXX"')
s2 = s.replace("tmp=$TMPDIR/conf$$-$RANDOM")
s2 = s.replace("(umask 077 && mktemp -d",
               "(umask 022 && mktemp -d")

4.2 真正的硬骨头:make 子进程 getcwd 失败

上面那步解决后 make 跑起来了,但在嵌套子项目处再次中断:

getcwd: cannot access parent directories: Not supported

原因是 SDCC 顶层 Makefile 用递归方式驱动子项目:

for lib in $(SDCC_LIBS); do $(MAKE) -C $$lib; done

$(MAKE) -C X 不会更新 PWD 环境变量,而 HMDFS 上子进程随后调用 getcwd 就会失败,导致子目录里的 configure 找不到。

这里有两个处理,缺一不可

① 整个构建迁移到非 HMDFS 分区。 实测 /data 分区上 pwd / cd / getcwd / chmod 全部正常。适配脚本先在候选目录中探测一个可写位置,再把源码整树复制过去构建:

work_root = None
for cand in ("/data/storage/el2/base/cache", "/data/cache",
             self.build_folder):
    try:
        os.makedirs(cand, exist_ok=True)
        probe = os.path.join(cand, ".sdcc-w-probe")
        with open(probe, "w") as _f:
            _f.write("x")
        os.remove(probe)
        work_root = cand
        break
    except OSError:
        continue
if not work_root:
    raise ConanException("no writable non-hmdfs work dir for sdcc")

work = os.path.join(work_root,
                    f"sdcc-{os.getpid()}-{int(time.time() * 1000)}")
shutil.copytree(self.source_folder, work, symlinks=True)

# copytree 保留了 HMDFS 源权限(含 other 无读权限的目录),
# 在 /data 上 chmod 生效,递归放开让 make 子进程能 stat 父目录链
for root, _dirs, _files in os.walk(work):
    try:
        os.chmod(root, 0o777)
    except OSError:
        pass

② 把 $(MAKE) -C X 改成子 shell 形式。 因为 bash cd 会更新 PWD 环境变量,子 make 继承到正确的 PWD,就不再触发 getcwd

("for lib in $(SDCC_LIBS); do $(MAKE) -C $$lib; done",
 "for lib in $(SDCC_LIBS); do (cd $$lib && $(MAKE)); done"),
("$(foreach as, $(SDCC_AS), $(MAKE) -C $(as) &&)",
 "$(foreach as, $(SDCC_AS), (cd $(as) && $(MAKE)) &&)"),

针对行尾无参形式,再用正则统一替换。这里有个容易踩的细节:行尾要用 (\s*)$ 锚定,否则会把 foreach / for 那两行一并误伤(它们同样含有 $(MAKE) -C):

# 行尾用 (\s*)$ 锚定,避免误伤 foreach/for 行
ms = re.sub(r'\$\(MAKE\) -C (\S+)(\s*)$',
            r'(cd \1 && $(MAKE))', ms, flags=re.M)

一个反直觉的教训:不要手动 export PWD。预设的 PWD 若与 make 实际工作目录不一致,bash 子进程反而会重新 getcwd,又触发 Permission denied。只保留 umask 022 + cd 即可。

5 核心难点二:内嵌 GCC 树的六处缺失

support/cpp 是 GCC 12.1.0 的裁剪版,SDCC 用它做预处理。sdist 打包时裁掉了一批文件,必须逐个补回——否则编译到那一步才报错。这次共处理了六处:

5.1 config/aarch64/ 目录整体缺失

该目录只有 dummy / i386 / riscv,host 若是 aarch64,cc1 编译必然引用 config/aarch64/*.def。补空桩文件即可(sdcpp 是预处理器,扩展选项内容无关紧要,只需文件存在):

aarch64_dir = os.path.join(gcc_dir, "config", "aarch64")
os.makedirs(aarch64_dir, exist_ok=True)
for _def in ("aarch64-arches.def", "aarch64-cores.def",
             "aarch64-option-extensions.def"):
    _p = os.path.join(aarch64_dir, _def)
    if not os.path.isfile(_p):
        open(_p, "w").write(
            f"/* minimal stub for {_def} — SDCC sdist trims "
            "aarch64 support */\n")

5.2 LIBIBERTY 路径指向不存在的目录

GCC Makefile 默认指向 ../libiberty/,而 support/cpp 下没有该目录。改为指向顶层已构建好的那份:

s2 = s.replace(
    "LIBIBERTY = ../libiberty/$(LIBIBERTY_PICDIR)/libiberty.a",
    "LIBIBERTY = ../../sdbinutils/libiberty/libiberty.a")

顺带纠正一个此前的误判:一度以为 libiberty 源码不在 sdist 里、打算 --disable-sdbinutils 绕开;实际核对后确认源码就在 sdist 中,于是保留 sdbinutils 构建,只把 GCC 子项目的 LIBIBERTY 指过去。误判必须在动手前纠正,否则会引入不必要的功能裁剪。

5.3 bversion.h 模板被裁掉

configure 不再生成它,而 gcc.o 编译依赖 GCC_VERSION 数字宏,需手写:

open(bversion, "w").write(
    "/* bversion.h — normally generated by configure;\n"
    "   SDCC sdist trims the template. */\n"
    "#define GCC_VERSION 120100\n"
    "#define GCC_VERSION_FULL \"12.1.0\"\n")

5.4 all-tree.def 需先生成

它是 make 目标,但被其他目标依赖,需先单独跑一次:

self.run(
    f"/data/service/hnp/bin/bash -c \"umask 022 && "
    f"cd '{gcc_dir}' && "
    f"make all-tree.def -j2 TMPDIR='{tmpdir}'\"",
    cwd=gcc_dir)

5.5 补丁顺序有依赖

gcc/Makefile 是由 configure-gcc 目标生成的。所以必须先触发 configure-gcc,再打 LIBIBERTY 补丁——顺序反了,补丁就打在空气上(文件还不存在)。这类"补丁依赖生成顺序"的问题在嵌套构建树里很常见。

5.6 libexec 必须随包

cc1 落在 libexec/sdcc/<target>/<ver>/cc1,SDCC 驱动运行期需要它。漏打包会直接报 cannot execute 'cc1'

# bin/share/lib + libexec(cc1 在 libexec 下,驱动运行期需要)
for entry in ("bin", "share", "lib", "libexec"):
    s = os.path.join(src, entry)
    if os.path.isdir(s):
        shutil.copytree(s, os.path.join(self.package_folder, entry))

6 其余差异

6.1 平台探测绕过

2018 年的 config.guess 不认识 OHOS。按知识库既有做法(E007),直接显式传 triplet 绕过探测:

bld = "--build=aarch64-unknown-linux-gnu --host=aarch64-unknown-linux-gnu"

6.2 boost 的 musl 声明缺失

OHOS musl 未声明 pthread_setcancelstate,而 boost 的 sp_thread_sleep.hpp 用了它。做法是复制该头文件到工作树内打补丁(补 extern 声明),并用 CPPFLAGS 让编译器优先找到它:

decl = ("\n// OHOS musl: pthread_setcancelstate not declared in "
        "<pthread.h> — declare it\n"
        "#if defined(BOOST_HAS_PTHREADS) && !defined(__ANDROID__)\n"
        "extern \"C\" int pthread_setcancelstate(int __state, "
        "int *__oldstate);\n#endif\n")
cppflags = f"-I{patch_dir} -I{os.path.join(boost_inc, 'include')} -D_GNU_SOURCE"

6.3 关闭缺依赖的可选端口

制品仓没有 gputils(PIC 工具链),PIC 端口无法构建,按需关闭:

opts = "--disable-pic14-port --disable-pic16-port --disable-nls " \
       f"--without-ccache --enable-host-shared --prefix={install_prefix}"

6.4 功能取舍:install 阶段禁用 strip

这是本次适配中一处明确的功能取舍,需要写清楚而不是含糊带过。

SDCC 的 make install 默认会 strip 安装的二进制。但在鸿蒙PC 上,strip 后的 ELF 执行会报:

Operation not permitted

这与鸿蒙的代码签名体系有关(未签名的、被改造过的 ELF 无法执行)。处理方式是在 install 时把 STRIP 指向一个空操作:

self.run(
    f"/data/service/hnp/bin/bash -c \"umask 022 && "
    f"cd '{work}' && "
    f"make install STRIP=/bin/true -j2 TMPDIR='{tmpdir}'\"",
    cwd=work)

代价要说清楚:产物体积会比常规发行版偏大(二进制未 strip)。这是为了优先保证"能构建、能运行";体积优化留待后续有签名方案时再处理。

7 真机运行与复现

适配完成后,在同一台设备上做了一次端到端复现。注意 SDCC 的一个特点:编译成功时不打印任何输出(Unix 惯例),所以重点看产物:

$ .../bin/sdcc --version
SDCC : mcs51/z80/z180/r2k/r2ka/r3ka/r4k/r5k/r6k/sm83/tlcs90/ez80/z80n/r800/ds390/
       TININative/ds400/hc08/s08/stm8/pdk13/pdk14/pdk15/mos6502/mos65c02/f8/f8l
       TD- 4.6.0 #16555 (Linux)
published under GNU General Public License (GPL)

$ .../bin/sdcc -mmcs51 t.c
(无输出即成功)

$ ls -l t.ihx
-rw-rw-r-- 1 ... 552  t.ihx

那一行 SDCC : mcs51/z80/z180/... 列出了它支持的全部目标架构——视觉上就能确认这是一个完整可用的跨架构编译器,而不是被裁剪过的残缺构建。

test_package 的消费者测试做的事是:把包复制到非 HMDFS 分区,再用打包后的编译器真实编译一个最小 8051 程序,并断言 Intel HEX 产物生成

src = ("/* minimal 8051 target program */\n"
       "void main(void) { volatile unsigned char i; "
       "for (i = 0; i < 10; i++) ; }\n")
r = subprocess.run([w_sdcc, "--std-c11", "-mmcs51", c_file],
                   capture_output=True, text=True,
                   cwd=work,   # sdcc 把 .ihx/.rel 输出到当前目录
                   env=dict(os.environ,
                            PATH=os.path.join(work, "bin") + os.pathsep
                            + os.environ.get("PATH", "")))
ihx = os.path.join(work, "t8051.ihx")
if not os.path.isfile(ihx):
    raise AssertionError(f"sdcc output .ihx missing: {ihx}")

测试输出:

OK: sdcc 4.6.0 real API ok (8051 compile -> .ihx)

这里还有一个 HMDFS 的细节值得记下来。 测试时不能直接在包目录里跑编译器——包位于 /storage(HMDFS),posix_spawn 打包好的二进制会报 Operation not permitted。所以测试脚本先把整个包复制到非 HMDFS 分区,再在那里执行。

另外 sdcppcc1 用的是编译期写死的 STANDARD_LIBEXEC_PREFIX 绝对路径,包被复制走后该路径失效。解法是把 cc1 一并复制到 bin/(与 sdcpp 同目录),借助 PATH 查找:

cc1_src = None
for root, _dirs, files in os.walk(os.path.join(work, "libexec")):
    if "cc1" in files:
        cc1_src = os.path.join(root, "cc1")
        break
if cc1_src:
    shutil.copy2(cc1_src, os.path.join(work, "bin", "cc1"))

在这里插入图片描述
图2 HUAWEI MateBook Pro(鸿蒙PC)真机运行 sdcc
如果你手上有配置了社区适配环境的鸿蒙PC 设备,可以直接验证:

conan remote add ohpcd https://conan.cnb.cool/OpenHarmonyPCDeveloper/Conan/-/packages/
conan list "sdcc/*" -r=ohpcd            # 查看可用版本与官方构建产物

本次验证的边界(如实声明):覆盖的是工程构建 + 全架构库编译(75/75)+ 8051 冒烟编译。编译器在更复杂工程上的等价性(例如大型 C 项目的完整编译产物比对)未作为本次验收项

8 小结

回头看,这次适配的麻烦不在"改了多少代码",而在于两个层面的问题互相纠缠

  • 文件系统层面:HMDFS 的 getcwd、权限、脚本执行限制,会把 autotools 的递归构建链在不同阶段打断——解法是迁移到非 HMDFS 分区 + 把 $(MAKE) -C 改为子 shell 形式
  • 源码完整性层面:内嵌 GCC 树被 sdist 裁剪,缺失文件要到构建那一步才暴露——解法是逐处补齐桩文件、修正路径、补生成目标,并注意补丁与生成的先后顺序

两类问题叠加,构成了这次"高难度"的实质。可复用的方法有几条:

  1. 先取证再动手——每个修复都要能看到旧状态失败、新状态通过的对照;
  2. 误判要在动手前纠正——本次 libiberty 一度被误判为"不在 sdist",核对后发现源码就在其中,避免了不必要的功能裁剪;
  3. 修复保持最小改动——桩文件只补必需项,不引入额外依赖;
  4. 功能取舍要写明代价——禁用 strip 是明确取舍,产物体积偏大这点必须如实说明,而不是含糊掩盖。

最终成果:全架构 75/75 编译通过,消费者冒烟 1/1 通过。SDCC 4.6.0 已能在鸿蒙PC 完成完整 Conan 构建,并对主要产物做了真实功能检查。

后续值得补的方向有两个:一是配合鸿蒙签名方案恢复 strip 以优化体积,二是用更复杂的目标工程验证编译器输出等价性。

编译器是嵌入式生态的地基。SDCC 在鸿蒙PC 上跑通全部 20+ 目标架构,意味着 8051 / Z80 / STM8 这条嵌入式工具链在 OpenHarmony 上有了可用的入口。

参考文献

[1] SDCC — Small Device C Compiler 官方网站. https://sdcc.sourceforge.net/

[2] SDCC User Guide / Manual. https://sdcc.sourceforge.net/doc/sdccman.pdf

[3] OpenHarmonyPCDeveloper/build_in_harmonyos — SDCC 4.6.0 适配配方(archives/s/sdcc/4.6.0),PR !5660. https://atomgit.com/OpenHarmonyPCDeveloper/build_in_harmonyos


欢迎加入开源鸿蒙PC社区:https://harmonypc.csdn.net/

欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper

项目源码(AtomGit 仓库):https://atomgit.com/OpenHarmonyPCDeveloper/build_in_harmonyos

Logo

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

更多推荐