前几天有个做嵌入式的朋友问我:鸿蒙系统源码下载到底该怎么弄,是不是去某个页面填个表就能拿到一个大压缩包?我当时就笑了——这个误解太普遍了。鸿蒙系统的源码不是一个压缩包,而是一棵由几十个甚至上百个 Git 仓库拼起来的大树,靠一份 manifest 清单文件把你需要的那几百个仓库在某个时间点的提交号钉死,再用 repo 这个工具统一拉下来。你要做的不是“下载”,而是“同步一套版本快照”。
这篇东西写给三类人:想读内核和框架源码但被仓库结构劝退的开发者、想在自己板子上跑通一次完整编译的嵌入式玩家、以及需要评估移植或二次开发成本的团队技术负责人。我会把“鸿蒙系统源码下载”这件事从头到尾拆开讲:源码分成哪几层、环境怎么盘、命令怎么敲、参数为什么这么设、坑在哪里。命令我会给可复制的形态,但仓库地址一律用占位符,因为具体地址以官方文档公布的为准,而且版本迭代快,写死了反而害你。
1. 先搞清楚“鸿蒙源码”到底有几层,别一上来就找压缩包
我见过太多人卡在第一步,不是因为技术难,而是因为目标没定义清楚。说“我要鸿蒙源码”,可能指的是四种完全不同的东西,对应四条完全不同的获取路径。搞混了,你会发现下了几十个 G,打开一看却没有你想要的那部分代码。
1.1 社区开源版、商用发行版、设备侧工具包、三方移植仓
第一层是社区开源版。这是真正意义上公开、可自由下载的那部分,社区里通常叫 OpenHarmony。它包含内核适配层、系统服务框架、应用框架、图形、多媒体、编译构建系统等,是最适合学习和研究的对象。你听到的“鸿蒙系统源码下载”,九成指的是这一层。
第二层是商用发行版。厂商基于开源版做裁剪、加固、增加自有服务和驱动适配,形成可以装到量产设备上的系统。这一层的完整源码一般不会整体公开,对外提供的是 SDK、HDK、开发文档和部分头文件。如果你是做产品集成,拿的是这一层的东西,别指望能 clone 到全部代码。
第三层是设备侧工具包与预编译产物。包括交叉编译工具链、系统镜像、签名工具、烧录工具。它们不是源码,但很多新手会把“下载了一个 SDK 包”当成“下载了源码”,然后在里面翻 .c 文件翻不到,白折腾半天。
第四层是芯片与板卡厂商的移植仓。这些仓库通常挂在开源版组织之外,或者在 vendor、device 目录下以独立仓库的形式存在。你要在特定开发板上跑,就离不开这一层,而这一层往往是文档最少、坑最多的地方。
我的建议是:先明确你要的是“读代码”还是“跑编译”。只读代码,第一层足够;要跑编译,第一层加第三层;要上板,四层全要。
1.2 为什么官方更愿意让你用 repo,而不是给你一个 zip
很多人第一反应是:给我一个 zip 多省事。但从项目方的角度看,一个大 zip 是灾难。原因有三个,都很实在。
第一,解耦与按需。这套系统由几百个仓库组成,图形、媒体、通信、内核适配各管各的,各有各的维护者和提交节奏。做成一个大仓库,任何人改一行代码都要跟所有人的提交打架,评审和合入会直接瘫痪。拆成多仓库之后,某个子系统单独发版、单独回退都很轻松。
第二,可复现。manifest 文件里记录的是“每个仓库对应哪个 commit”,相当于一张精确的配料表。今天拉下来的代码和三个月后拉下来的代码,只要 manifest 一致,编译出来的东西就是一致的。这对定位问题太重要了——线上出现一个诡异 bug,你能精确回到当时那个代码状态。
第三,传输效率。一个大 zip 意味着每次更新都要重新下一遍全量。而 Git 只传增量对象,你昨天同步过,今天再同步可能只下几十兆。
打个比方:大 zip 像是把整栋楼的所有房间钥匙熔成一把万能钥匙,丢了就得重铸;repo 加 manifest 像是给你一串编号清晰的钥匙,哪把丢了配哪把,还能记录“今天这串钥匙开哪几扇门”。
提示:不要用“下载一个 release 压缩包然后改代码”的方式做开发。没有 .git 目录,你既没法看提交历史,也没法做 diff 和回退,出了问题只能靠肉眼比对。
2. 下载前的环境盘点:磁盘、内存、网络三笔账
真正开始敲命令之前,先把账算清楚。我见过最惨的一次,是某位同学在 128G 的笔记本上跑到一半磁盘满了,repo 报了一堆莫名其妙的错误,他以为是网络问题,折腾了一整天。
2.1 磁盘空间到底要留多少,我给你算笔明细
先说结论:准备 200GB 空闲空间,用 ext4 或 xfs,别用 NTFS 或 exFAT。下面是拆解。
源码树本身,如果只拉当前分支(-c参数),工作区文件大概十几到二十几 GB;加上每个仓库的 .git 对象库,总量会到三十 GB 上下。如果拉全量历史,翻倍很正常。
编译产物 out 目录,取决于你编译的范围。全量编译一个中等规模的产品配置,产物加中间文件二十到四十 GB 是常态。你要是同时编多个产品配置,out 目录会成倍增长。
ccache 缓存,如果你开了编译缓存(强烈建议开),第一次编译完可能占十几 GB,之后增长会放缓。
再加上工具链、预编译二进制、日志,零零碎碎又是几个 GB。所以:
| 使用场景 | 源码 | 编译产物 | 缓存 | 建议预留 |
|---|---|---|---|---|
| 只读代码、不编译 | 15-30 GB | 0 | 0 | 50 GB |
| 单产品配置编译 | 20-35 GB | 20-40 GB | 10-20 GB | 120 GB |
| 多产品配置 + 反复编译 | 30-50 GB | 40-80 GB | 20-40 GB | 250 GB |
还有一个容易被忽略的点:inode。这套源码小文件极多,几十万个文件很正常。有些云主机默认 inode 数量少,磁盘还有空间但df -i已经 100%,写入会直接失败。所以我习惯在同步前先跑一遍df -h和df -i双检查。
内存方面,8GB 能跑起来但会很憋屈,16GB 是比较舒服的起点,32GB 在跑并行编译时优势明显。如果内存不够又想编译,就把并行度降下来,别硬顶,OOM Killer 会在你编译到 80% 的时候毫不留情地干掉进程。
2.2 系统与工具链依赖清单
| 依赖项 | 版本建议 | 作用 | 踩坑提示 |
|---|---|---|---|
| Linux 发行版 | Ubuntu 20.04/22.04 等主流 LTS | 编译宿主环境 | 内核版本别太老 |
| git | 2.20 以上 | 版本控制 | 太老不支持 partial clone |
| python3 | 3.8-3.10 | repo 脚本运行环境 | 版本过高可能报语法错 |
| repo | 最新版脚本 | 多仓库管理 | 用包管理器装的常偏旧 |
| gn / ninja | 随源码预置或自装 | 构建系统 | 注意 PATH 顺序 |
| clang | 源码内预置 | 编译器 | 别用系统自带版本覆盖 |
| nodejs | 14-18 | 部分构建脚本依赖 | 版本漂移易报错 |
| ccache | 任意较新版 | 编译加速 | 单独挂盘更省心 |
为什么必须在 Linux 上做这件事?不是矫情,是硬约束。第一,大小写敏感。这套代码里存在路径只差大小写的文件,在不区分大小写的文件系统上会直接互相覆盖,编译必然失败。第二,符号链接。构建过程大量依赖软链接,Windows 文件系统对软链接的支持很别扭。第三,文件权限。一堆构建脚本需要可执行位,权限位丢失就跑不起来。第四,路径长度。深层嵌套目录很容易突破 Windows 的路径长度限制。
那 Windows 用户怎么办?三条路:WSL2、虚拟机、双系统。WSL2 最省事,但要注意把代码放在 WSL 自己的文件系统里(比如/home/xxx/),千万别放在/mnt/c/下——跨文件系统访问会让同步和编译慢到怀疑人生,我实测过,差距能有五到十倍。虚拟机隔离性好但吃资源。双系统性能最好但切换麻烦。
2.3 网络不顺畅时的四条替代路径
同步卡住是常态,尤其仓库多、单个仓库大的时候。我不谈任何绕过网络限制的手段,只说正当的技术优化,这些手段本身就够用了。
路径一:用就近的代码托管镜像。同一份代码在多个平台都有官方同步的镜像仓库,选物理距离近、响应快的那个,manifest 里的 remote 地址可以替换。这一步能解决大部分“慢”的问题。
路径二:浅克隆。--depth=1只拉最新一次提交,不下历史。对只读代码和跑编译的场景完全够用,体积能砍掉一大半。代价是看不到历史提交,也不能直接切老版本。
路径三:单仓库按需拉取。不跑全量repo sync,先repo sync指定几个你关心的路径,把最核心的拉下来,其余的以后再说。这个策略在磁盘紧张或者只想看某个子系统时特别有效。
路径四:用官方归档包。有些版本会提供打包好的源码归档,适合做离线存档、教学演示、代码审计。缺点是更新不及时,且没有版本控制信息。
注意:无论用哪条路径,都不要在同步过程中手动去删 .repo 目录下的文件。那个目录是 repo 的状态机,删了之后它会认为从没同步过,你得从头再来。
3. 实操:从零到源码落地的完整流程
前面铺垫这么多,是因为我发现跳过环境检查直接敲命令的人,八成会在中途返工。下面是我自己用的一套流程,按顺序走基本不会翻车。
3.1 装 repo 的两种方式,我推荐手动放脚本
方式一:包管理器安装。一行命令搞定,省事。问题是发行版仓库里的版本经常滞后,遇到 manifest 用了新特性就可能报错,而且报错信息往往很隐晦,让人误以为是仓库地址错了。
方式二:手动下载脚本放到 PATH 里。这是我推荐的做法,可控性强。
mkdir -p ~/bin curl -o ~/bin/repo https://<代码托管平台>/<组织>/repo/raw/<分支>/repo chmod a+x ~/bin/repo export PATH=$HOME/bin:$PATH echo 'export PATH=$HOME/bin:$PATH' >> ~/.bashrc几个细节值得说。~/bin是约定俗成的用户级可执行目录,不需要 sudo,避免污染系统环境。chmod a+x千万别忘,忘了他会报“permission denied”,很多人第一反应是去加 sudo,越搞越乱。写完 PATH 记得source ~/.bashrc,并且新开一个终端验证一下which repo指向的是不是你刚放的那个。
装完立刻验证:
repo --version python3 --version两行都出结果,才算环境就绪。如果repo --version报语法错,多半是 python 版本不匹配,用update-alternatives或者虚拟环境切一下。
3.2 初始化:分支和标签选错,后面全白干
初始化的核心是搞清楚-b和-m这两个参数到底在指定什么。
mkdir -p ~/oh_workspace cd ~/oh_workspace repo init -u https://<代码托管平台>/<组织>/manifest.git -b <manifest分支名> -m default.xml-u指向的是manifest 仓库,注意,不是代码仓库,这是个极高频的错误。很多人把某个子系统的仓库地址填进去,repo 会告诉你找不到 manifest 文件。
-b指定的是manifest 仓库的分支,不是代码的分支。这一点极其反直觉。你选-b的那个分支,决定了 manifest 文件里记录的是哪一套仓库组合和提交号。
-m指定用哪个 xml 文件。默认是default.xml,有些版本会提供default.xml之外的组合文件,比如精简版、带特定硬件适配的版本。
关于选主干还是选发布标签,我的经验是:学习和研究用发布标签,跟进新特性用主干,做产品基线必须锁标签。理由很直接——主干每天都在变,今天能编过的代码明天可能就编不过;而标签和发布版本是一一对应的,可复现,出了问题能查。
初始化成功后,目录下会多出一个.repo目录。这时候别急着 sync,先看一眼清单:
ls .repo/manifests/ cat .repo/manifest.xml | head -50你会看到一堆<project>标签,每个对应一个仓库和它的路径、revision。这就是你的“配料表”,后面所有同步行为都由它决定。
3.3 同步:并发数不是越大越好,断点续传是免费的
repo sync -c --no-tags --force-sync -j8逐个参数解释。-c表示只拉当前分支,能省下大量历史对象,是最划算的一个优化。--no-tags不拉标签,进一步减体积。--force-sync在本地有改动冲突时强制覆盖——这个参数要谨慎,它会覆盖你在工作区的未提交修改,只在确认本地无改动时用。-j8是并发数,同时拉 8 个仓库。
关于-j,我踩过坑。设成 32,结果网络跑满了但同步速度反而更慢,因为每个连接分到的带宽太薄,加上服务端可能有并发限制,大量请求排队超时,repo 不断重试,看起来就是“卡住不动”。后来我固定用 4 到 8,视带宽和 CPU 而定,反而最快。经验值:百兆带宽用 -j4,千兆带宽用 -j8,别超过 CPU 核数太多。
同步中断了怎么办?直接重新执行同一条命令即可。repo 会检查每个仓库的本地状态,已经完整的跳过,没完成的续上,这是它比手工 clone 强的地方。所以千万别因为断了就rm -rf重来,那是纯浪费时间。
如果全量同步太慢,用分批策略:
repo sync -c build repo sync -c foundation/ability repo sync -c kernel先把你最关心的几个路径拉下来,能打开 IDE 看代码了,剩下的慢慢补。这个策略对只想读某个子系统源码的人特别友好,可能一两个小时就能开始干活。
3.4 校验完整性:花五分钟省一天
同步跑完,很多人直接就开编了。我建议加一步校验,五分钟的事,能省掉后面一天的排查。
repo status repo forall -c 'git status --porcelain' | head -50 repo manifest -r -o snapshot_$(date +%Y%m%d).xml第一条看整体状态。第二条逐个仓库检查工作区是否干净——如果同步完就有未跟踪文件或修改,那多半是同步过程中断了,某个仓库没拉全。第三条最重要:-r会把当前每个仓库实际所在的 commit 号固化进一个新的 xml 文件。
这个快照文件的价值在于:三个月后你想回到今天这个状态,只要用这个 xml 重新 init 再 sync,就能一字不差地复现。做产品的团队一定要把这个文件纳入版本管理,它是你构建基线的凭证。
再补一步,检查有没有仓库处于异常状态:
repo forall -c 'git rev-parse --verify HEAD > /dev/null || echo "BROKEN: $REPO_PATH"'输出为空说明全部正常。有 BROKEN 的,单独对该路径执行一次 sync 就行。
4. 只要某个模块?按需拉取的三种姿势
不是所有人都需要整棵树。我经常遇到这种情况:只想看看某个子系统的实现,或者只想给某个仓库提个补丁,结果为了这一个仓库要同步几十 G,纯属浪费。下面三种姿势按需选。
4.1 单仓库 clone 配合稀疏检出
如果你只关心某一个仓库里的某个子目录,这是最省的方式。
git clone --filter=blob:none --no-checkout https://<代码托管平台>/<组织>/<仓库名>.git cd <仓库名> git sparse-checkout init --cone git sparse-checkout set <子目录A> <子目录B> git checkout <分支名>--filter=blob:none是部分克隆,只拉提交树和元数据,文件内容按需取。--no-checkout先不检出工作区。然后用稀疏检出指定你真正要的目录。这套组合下来,一个几 GB 的仓库可能只占几百兆。
代价是每次访问没拉过的文件时会触发按需请求,离线状态下不可用。所以如果你的目标是“读完就删”,这种方式很合适;如果打算长期在上面开发,还是老实全量。
4.2 归档包与发行版本
有些场景就是想要一份静态快照:给新人做培训材料、做代码审计存档、放在离线环境里查阅。这时候归档包反而更顺手。
下载后务必校验摘要:
sha256sum <归档包名>把结果和官方公布的值比对。别跳过这步,大文件传输损坏的概率不低,解压时才发现就晚了。解压建议用tar -xzf保留权限位,不要用图形界面的解压工具,很容易丢可执行位和软链接。
归档包的局限要说清楚:没有 .git,不能 diff、不能回退、不能看 blame。所以它只适合“读”和“存”,不适合“改”。
4.3 版本快照与每日构建产物
持续集成系统每天会跑构建,产出编译好的镜像和部分源码快照。这类产物的定位是验证和试用,不是开发基线。原因很简单:它对应的是某一次随机的提交状态,没有经过发布流程的稳定性验证,今天能跑明天可能就挂。
那它有什么用?两个场景很有价值。一是你怀疑某个问题是不是主干已经修了,拿最新快照验证一下,比从头同步快得多。二是给非技术同事演示功能,他们不需要源码,只要一个能跑起来的镜像。
如果你确实要基于快照做开发,记得用 3.4 节说的方法,把当时的 manifest 快照保存下来,否则以后没法复现。
5. 常见问题与排查实录
这部分是我这些年帮人看问题攒下来的。很多现象看着吓人,其实原因很无聊。
5.1 同步阶段的四类故障速查
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
| 卡在某个仓库长时间无输出 | 单仓体积大或链路抖动 | 单独repo sync <路径>,降低-j |
| 报连接重置、early EOF | 并发过高或链路不稳 | -j4重试,或换就近镜像 |
| 提示 No space left on device | 磁盘满或 inode 耗尽 | df -h加df -i双查 |
| 报文件名非法、无法创建 | 文件系统不支持或大小写冲突 | 换 ext4,检查挂载参数 |
| 反复提示某仓库有本地修改 | 上次同步中断留下脏状态 | 确认无改动后--force-sync |
第一条我遇到最多。现象是终端停在Fetching project xxx不动,一停半小时。绝大多数情况不是死锁,是那个仓库真的很大,加上网络在慢慢爬。判断方法是另开一个终端看.repo/projects/<路径>.git目录的大小有没有在涨,涨就是在下,别动它。如果十分钟一点没涨,那才是真卡了,Ctrl+C 后单独同步这个路径。
第三条特别隐蔽。磁盘还有 20G,但写入报错,八成是 inode 用完了。df -i一看 IUse% 100%,删点小文件或者换个大 inode 的文件系统。
5.2 大小写敏感与换行符:两个安静的杀手
大小写问题。这套代码里存在仅大小写不同的路径。在 macOS 默认的 APFS(大小写不敏感模式)或者 Windows 的 NTFS 上,这两个路径会被认为是同一个,后写入的覆盖先写入的,编译时报“找不到文件”,而你ls看明明在。解决办法只有两个:在 macOS 上新建一个大小写敏感的 APFS 卷,或者干脆在 Linux 上做。我建议后者,省心。
换行符问题。Git 有个core.autocrlf配置,Windows 上默认是 true,检出时把 LF 转成 CRLF。对于 C 代码无所谓,但对于 shell 脚本是致命的——带 CRLF 的脚本执行时会报bad interpreter: /bin/bash^M,那个^M就是回车符。处理方式:
git config --global core.autocrlf input在 Linux 上设成input或者false,让 Git 原样保存。如果已经搞脏了,可以用dos2unix批量转换,或者重新检出。
5.3 编译前就报错的三类“假故障”
代码下完了,一编译就报错,很多人第一反应是“源码有问题”。我统计过,九成是环境问题。
第一类,python 版本不对。构建脚本里用了某些语法特性,python 3.11 可能能跑,3.7 就报错。反过来也有。我一般用python3 --version确认在官方建议的区间内,不在就用虚拟环境隔离,不要动系统 python。
第二类,gn 和 ninja 不在 PATH 里。有些版本的构建系统自带预编译的 gn/ninja 在prebuilts/目录下,需要先把它们加进 PATH,或者直接用项目提供的构建入口脚本(比如hb工具),它会自动处理。手动调 ninja 而没设好环境,报错信息会非常莫名其妙。
第三类,ccache 目录权限或已满。开了 ccache 但目录不可写,编译会直接失败,报错还很不直观。我习惯把 ccache 目录单独指到一块大容量盘上:
export CCACHE_DIR=/data/ccache ccache -M 30G ccache -s-M限制最大容量,-s看命中率。第一次编译命中率接近零,第二次开始能到七成以上,提速非常明显。
6. 源码到手之后:目录结构速览与第一次构建
代码躺在磁盘上了,接下来得知道它长什么样,不然打开就是一脸茫然。
6.1 目录地图:先认路再读代码
| 目录 | 作用 | 阅读优先级 |
|---|---|---|
| build | 构建框架、编译脚本、产品配置 | 高,改配置先看这里 |
| foundation | 系统基础能力,含通信、分布式、 ability 框架 | 高 |
| base | 基础库、启动、安全等底层组件 | 中 |
| kernel | 内核适配层与轻量内核实现 | 高,做移植必看 |
| drivers | 外设驱动框架与适配 | 中 |
| device | 板级配置、产品定义 | 高,上板必看 |
| vendor | 厂商定制代码 | 视板卡而定 |
| third_party | 三方开源组件 | 低,除非要改依赖 |
| prebuilts | 预编译工具链与二进制 | 低,但要会用 |
| out | 编译产物 | 低,可以整个删掉重编 |
| utils、test | 工具与测试用例 | 中,写测试看这里 |
我的阅读顺序建议是:先看build里的产品配置,搞清楚一个产品是怎么被定义的;再看device里对应板卡的目录,看它拉了哪些组件;然后进foundation看系统服务怎么启动;最后才下潜到kernel。倒过来读会很痛苦,因为你不知道一个函数是被谁调起来的。
6.2 第一次构建的最小闭环
构建入口一般有两种:项目提供的封装工具(如hb),或者直接调build目录下的脚本。我用封装工具多一些,参数少,不容易错。
# 进入源码根目录 cd ~/oh_workspace # 选择产品配置 ./build.sh --product-name <产品名> # 或者用封装工具 hb set hb build -fhb set是交互式的,会让你选当前目录对应哪个产品,选完会生成一个配置,之后hb build就按这个产品编。-f表示全量编译,不加则是增量。
第一次编译建议先跑全量,哪怕慢。原因是从零到有的过程能把所有依赖问题暴露出来,而增量编译可能因为某个模块没参与而漏掉问题。
产物一般在out/<产品名>/packages/phone/images/之类的路径下,具体以版本为准。编译成功会打印出各个镜像文件的大小。
首次编译常见失败点,按出现频率排:工具链没找到(PATH 问题)、python 依赖包缺失(缺 requests、pycryptodome 之类,按报错装就行)、磁盘空间不足(编到一半挂掉,报错信息还很不明显)、内存不足被 OOM Killer 干掉(表现为进程无声退出,看dmesg能看到记录)。
我个人在实践中形成的习惯是:第一次同步之前先跑一遍磁盘和 inode 检查,同步完立刻保存 manifest 快照,编译前先单独把 ccache 配好,这三件事花不了十分钟,但能挡掉后面绝大多数的糟心事。另外提醒一句,out目录和.repo目录千万别手贱去动,前者大不了重编,后者一动就得重新同步,那才是真的痛。
如果你只是想读代码,其实不用等全量同步完,用第 4 节说的单仓 clone 加稀疏检出,一两个小时就能开始看核心子系统的实现了。剩下那些仓库,等真正需要编译或者上板的时候再补,完全来得及。