ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

开源鸿蒙跨平台开发:AtomGit代码托管与可复现构建完整指南

开源鸿蒙跨平台开发:AtomGit代码托管与可复现构建完整指南

今天记录的是“开源鸿蒙跨平台开发”系列的第二天。昨天我把基础环境跑通,用 ArkTS/ArkUI 搭了一个同时兼容手机、平板和桌面端的跨平台 Demo,编译产物能正常出包。今天原本只是想“把本地工程推到远程仓库”,给自己留个备份,结果从 SSH 配置到仓库分支再到 push 回滚,整整折腾了一个下午。这篇文章就是这次完整经历的可复现记录,所有坑我都会写明报错原文、成因和最终处理方式,方便后面的人直接抄作业。

如果你是刚接触开源鸿蒙开发,或者正准备把一个本地工程托管到 AtomGit,这篇文章会比较长,但每一步都是实际跑过的。尤其是“上传之后,别人从空仓库克隆回来能不能构建成功”这件事,我会放在最后给你一套完整的验证流程,这也是我最想强调的部分。

1. 项目背景与仓库选型

1.1 这个跨平台工程本身在做什么

先交代一下项目背景。这不是一个纯鸿蒙原生 App,而是一个基于 ArkUI 声明式语法写的跨多设备工程,目标是同一套 UI 代码在不同屏幕尺寸下自适应布局。工程目录里有entry模块、common公共组件目录,还有一些自定义的页面路由和状态管理模块。依赖了三个三方库,分别是图表绘制、网络请求和本地存储封装,都是通过 OpenHarmony 的 ohpm 包管理工具安装的。

这类工程有个特点:文件数量多、依赖关系复杂,oh_modules目录一旦生成会非常大,而且随着版本升级,锁文件里的依赖还可能出现不一致。如果你不把代码托管起来,靠本地磁盘保存工程,换一台电脑或者清一次缓存,代价都非常大。

DAY1 结束的时候,工程在本地是可以正常编译的。DAY2 的目标就很明确:把这个“本地可用”的状态提升到“远端可复现”的状态。所谓可复现,就是说任何人按照仓库里的 README 操作,clone 之后能够在自己的环境里成功 build,而不是拿到一堆报错。

1.2 为什么选了 AtomGit 而不是其他平台

现在的代码托管平台可选范围其实不少,但考虑到这个项目是开源鸿蒙相关的,我最终选了 AtomGit,主要基于几点判断。

第一,AtomGit 是开源原子基金会下面向开发者提供的托管平台,和 OpenHarmony 这个生态的联动天然更紧密。如果你后面想把工程提交到开源鸿蒙相关社区,或者参与上游生态项目,代码放在这个平台,后续做组织转移、关联 issue 都要顺一些。

第二,网络访问体验对国内开发者比较友好。之前我在其他海外平台托管过项目,上传下载经常不稳定,尤其是仓库里有十几个大资源文件的时候,push 到一半就断,重试几次心态就崩了。AtomGit 在国内直接访问,速度非常稳定,至少我这次从 clone 到 push 全程没有因为网络中断过。

第三,平台提供了仓库的完整管理能力,包括分支保护、成员权限、Issue 和 CI/CD 流水线接入。单人或小团队开始可能用不到那么多,但后面如果要接自动化构建,这些能力都是现成的。

下面这个表格是我当时在选型时候的对比,供参考:

对比项AtomGit国内通用代码托管自建 Git 服务
国内访问速度快较快取决于服务器
与开源鸿蒙生态关联强一般无
分支保护 / 权限管理支持支持需自行配置
CI/CD 能力有有需额外部署
维护成本无无高
适合场景开源鸿蒙项目、个人开源通用项目强隐私或内网环境

这个对比不是说你必须选哪家。实际最重要的是:选一个你愿意天天用的平台,并且把这个仓库当作工程真正的“源头”,而不是本地目录的镜像。

2. 上传前准备:把仓库基础打牢

2.1 本地工程状态确认

我在 push 之前做的第一件事,不是创建远程仓库,而是确认本地工程是否处在一个可以被“搬走”的状态。这一步很多人都跳过,但恰恰是后面很多坑的根源。

确认的内容有三项。第一项,工程是否能通过完整构建。不要只看 IDE 里那行绿色提示,最好在命令行跑一次干净构建:

hvigorw clean hvigorw assembleHap --mode module -p product=default

这样做可以把旧的构建缓存清掉,从源码重新编译,如果这一步能过,说明代码本身没有隐藏的本地绝对路径问题。第二项,检查oh-package.json5和hvigorfile.ts是不是完整。这两个文件决定了依赖怎么解析,很多人 push 代码时会通过 IDE 的“忽略文件”功能把某些配置文件过滤掉,这会在 clone 后造成依赖根本拉不下来的现象。第三项,确认本地全局 Git 身份信息已经设置过:

git config --global user.name "你的标识" git config --global user.email "your@example.com"

如果没有设置,后面提交时会报Author identity unknown,那就得中途停下来处理,比较打乱节奏。

2.2 初始化 Git 与 .gitignore 编写

进入工程根目录后,执行:

git init -b main

这里用了-b main是为了让默认分支名是main。很多新仓库远端也默认叫 main,可以避免后面出现“本地 master、远端 main”这种分支对不上的问题。如果你用的 Git 版本比较老,不支持-b main,那就先git init,再git branch -M main改名。

接下来是 .gitignore。OpenHarmony 工程有几个目录特别容易造成仓库膨胀:.hvigor、.idea、build、oh_modules、entry/build,以及各种*.log。我这次用的 .gitignore 最开始长这样:

/.hvigor /.idea /build /oh_modules /entry/build /node_modules *.log .DS_Store

这个文件的出场顺序非常重要。我建议在第一次git add .之前就把它写好,否则一旦oh_modules或者build目录已经被 Git 跟踪了,后面即使把 .gitignore 改好,也不会自动对它们生效,必须手动清理一遍索引。那次我就是因为目录太大,已经 add 进去了,后面才被迫做了第 3 章里那道操作。

2.3 SSH 密钥生成与 AtomGit 配置

本地 Git 仓库准备好之后,要把推送通道打通。AtomGit 支持 HTTPS 和 SSH 两种方式,个人项目我建议直接用 SSH。原因有两个:一是 SSH 不需要每次 push 都输入用户名密码,二是 SSH 密钥在服务器上就表明身份,比较不容易出现密码泄漏的问题。

生成密钥的命令是这样的:

ssh-keygen -t ed25519 -C "your@example.com"

默认保存路径是~/.ssh/id_ed25519,如果你之前没有生成过其他密钥,直接一路回车即可。如果之前已经有一对密钥,建议单独生成一个文件名,比如:

ssh-keygen -t ed25519 -C "your@example.com" -f ~/.ssh/id_ed25519_atomgit

然后把公钥内容添加到 AtomGit 的 SSH 公钥设置里。公钥文件是id_ed25519.pub,内容是ssh-ed25519 AAAA...那一串,不要复制错了,私钥id_ed25519是绝对不能外传的。

加载到 agent 并验证连接:

eval "$(ssh-agent -s)" ssh-add ~/.ssh/id_ed25519_atomgit ssh -T git@atomgit.com

如果看到类似Hi xxx! You've successfully authenticated的提示,说明 SSH 通道已经通了。这里我当时踩的第一个坑是权限问题,报错Permission denied (publickey)。排查了半天发现我添加密钥的路径和 ssh-agent 里默认加载的密钥不是同一把,后来我加了一条 Host 配置才解决,这个在第 5 章的速查表里会专门写。

3. 上传过程的踩坑记录

3.1 第一次 push 被拒:远端 README 与本地历史冲突

我在 AtomGit 上创建远程仓库时,勾选了“初始化仓库时添加 README 文件”。这是一个很容易被忽视的选项。然后我在本地执行:

git remote add origin git@atomgit.com:yourname/yourproject.git git branch -M main git push -u origin main

结果 push 直接报错:

! [rejected] main -> main (fetch first) error: failed to push some refs to 'git@atomgit.com:xxx.git' hint: Updates were rejected because the remote contains work that you do not have locally.

这个错误的本质是:远端仓库里已经有了一次 README 相关的提交,本地仓库里则是从零创建的初始提交,两边没有共同的祖先,Git 拒绝直接推送覆盖。有人会习惯性用git push -f强推,我强烈不建议这样做,尤其是在远端已经有内容的情况下,强推会把远端已有的东西丢掉。

正确的处理是在第一次推送前先把远端内容拉到本地合并:

git pull origin main --allow-unrelated-histories

这个参数允许 Git 合并两个没有共同历史的分支。合并后把远端 README 保留下来,再推送就成功了。实际操作中,如果你在创建远程仓库时就不勾选“添加 README”,后面就不会遇到这个问题,所以我把这个选项叫作“最容易省事的默认反选项”。

3.2 大型依赖目录被误提交,仓库瞬间膨胀

第一次推送成功之后,我顺手在 AtomGit 网页上看了一眼仓库大小,发现体积涨到了 700 多 MB,正常这个工程源码压缩后应该在几 MB 级别。用磁盘分析工具一看,罪魁祸首是oh_modules目录,里面装着各个依赖库的源码,文件数量上万。

这个问题比 push 失败更棘手。如果只是本地提交了,但还没推送到远端,处理起来很简单:把目录从 Git 索引里移除,然后更新 .gitignore 后再提交。我当时属于已经推上去了,远端历史里已经有了那条大型提交记录,这就不是单纯改 .gitignore 能解决的问题了。

处理方式分两步。第一步,先让当前工作区恢复干净:

git rm -r --cached oh_modules

--cached表示只从 Git 的索引中移除,不删除本地磁盘上的文件。然后修改 .gitignore 加上/oh_modules,再提交:

git commit -m "chore: remove oh_modules from version control" git push

提交之后,远端仓库的 HEAD 已经不含oh_modules了,但 Git 的历史里仍然保存着那 700 多 MB 的对象数据,仓库不会自动瘦身。

我尝试了git gc、清理缓存等常规手段,效果都不明显。最终我选择了一个比较果断的方案:因为这是 DAY2 的工程,提交历史还没有多少价值,我直接把远端仓库删掉重建,本地重新初始化后,先写 .gitignore 再提交,最后再推上去。如果你已经提交了很多有价值的 commit,一定不要学我直接删除重建。更稳妥的做法是使用git filter-repo工具重写历史,把大文件彻底洗掉,然后强推一次,并让团队其他人同步换掉本地旧历史。

3.3 仓库中出现多余产物文件

除了依赖目录,还有一类文件容易被忽略,就是各个模块的build目录。ArkUI 工程在编译过程中会生成大量中间文件,你每次打开 IDE 编译一次,entry/build下的产物都会更新一遍。这些文件不参与源码管理,但对仓库体积的贡献非常大。

第一次提交的时候,我的 .gitignore 只写了/build,但实际效果并没有覆盖到模块级目录下的 build,因为 ArkUI 工程里的目录结构是entry/build、common/build。只写/build只能匹配根目录下的 build,模块目录下的不会匹配。

正确的写法是要么写**/build,要么分别写entry/build和common/build。我自己后来改成这样才彻底挡住:

**/build oh_modules/

另外还有一个容易忽略的点:local.properties文件里往往保存着本机 SDK 路径,这个文件也应当忽略,否则别人 clone 下来还得改路径,而且不同电脑 SDK 路径不同,提交上去没有任何意义。

3.4 分支规范与远端分支保护

仓库里的分支一开始比较随意,我直接往 main 上 push。后来发现 AtomGit 默认分支保护策略比较严格,main 分支受到保护,直接 push 时提示:

! [remote rejected] main -> main (pre-receive hook declined)

这里展开说一下。分支保护的目的是防止重要分支被误操作,比如main或release分支,通常需要 PR 评审后才能合入。如果你在早期 solo 开发阶段觉得这个限制很烦,可以到仓库设置里调整保护规则,把“允许直接推送”打开;但如果团队里有两个人以上,建议还是保留保护机制。

我这次最终用的是比较简洁的分支方案:

  • main:始终处于可构建可发布状态
  • dev:日常开发合并分支
  • feature/xxx:具体功能分支

上传时,我会先在本地开一个功能分支,提交完再合并到 main。这种习惯能保证远端 main 的历史是线性的、干净的,也方便后面接 CI 流水线时把“main 分支构建通过”作为硬性门槛。

4. 可复现验证:从零完整跑通

4.1 最小验证环境清单

上传完仓库,你以为今天就结束了吗?并没有。真正让我觉得踏实的环节,是验证“克隆下来能跑”。这个验证在开发环境里做和在一台干净环境里做,效果完全不一样。我特意找了一台没有 OpenHarmony 开发环境的机器,按照 README 从零安装并构建。

以下是这次验证使用的环境清单:

项目版本 / 配置
操作系统64 位 Linux 发行版
Node.js18 LTS 版本
OpenHarmony SDKAPI 10 版本
DevEco Studio / 命令行工具使用 hvigor 命令行
ohpm与 SDK 配套版本
代码来源AtomGit 仓库全新 clone

关键点在于 Node.js 版本。OpenHarmony 的构建工具链对 Node.js 版本有明确要求,太新或太旧都可能出问题。我在这台环境里严格按照官方要求装好指定版本,避免因为环境差异导致验证失败,最后又把“假失败”归结到代码头上。

4.2 新机器克隆与构建的完整过程

验证步骤我全部记录下来,可以直接作为团队新成员的 onboarding 文档。

先克隆代码:

git clone git@atomgit.com:yourname/yourproject.git cd yourproject

然后安装项目依赖。OpenHarmony 工程依赖分为两部分:一部分是 ohpm 管理的开源包,另一部分是 hvigor 构建工具本身。通常环境里安装了 DevEco Studio 或者命令行工具后,直接在工程根目录执行:

ohpm install

这一步会读取oh-package.json5,把依赖包安装到oh_modules目录。执行完毕后,再运行构建命令:

hvigorw clean hvigorw assembleHap --mode module -p product=default

如果一切顺利,最终会看到 BUILD SUCCESSFUL,并生成 HAP 文件。HAP 的默认输出位置是:

entry/build/default/outputs/default/entry-default-signed.hap

我在这次验证过程中,第一次构建其实没有成功。报错信息指向某个依赖包版本与 SDK 版本不匹配。这个现象非常典型:原作者本地可能有一个较旧的缓存包,而新机器会用干净环境解析到最新兼容版本,如果oh-package-lock.json5没有提交,锁不住版本,就会复现出这种不一致。

修复方法是把依赖版本固定好,并确保锁文件进入版本控制。OpenHarmony 工程的 lock 文件默认是oh-package-lock.json5,这个文件里记录了每个依赖包的具体版本。如果它不在仓库里,新环境就会重新解析依赖范围,导致可能拉到不同版本。我最后把 lock 文件补提交上去,问题彻底解决。

4.3 远程仓库健康状态检查清单

可复现验证通过后,并不代表仓库就是健康的。我最后做了一遍系统性检查,把以下这些项全部过一遍,才算真正收工。

第一项,检查.gitignore是否真的拦截住了依赖和构建目录。从远端重新 clone 一份,查看仓库大小:

git clone --bare git@atomgit.com:yourname/yourproject.git du -sh yourproject.git

正常裸仓库应该在几十 MB 以内,如果几百 MB,说明还有大文件在历史里,需要继续清理。

第二项,检查关键文件是否齐全。工程根目录下至少要有oh-package.json5、oh-package-lock.json5、hvigorfile.ts、entry目录和 README。缺少任何一个,别人都很难直接跑起来。

第三项,做一个最小化自动化验证脚本,保存在仓库的scripts/目录下,内容可以是:

#!/bin/bash set -e ohpm install hvigorw clean hvigorw assembleHap --mode module -p product=default

有了这个脚本,任何新机器只需要一行命令就能验证仓库当前状态。很多项目直到后来接 CI/CD 才发现“原来我的仓库根本没法自动构建”,原因就是从来没在干净环境里跑过。提前做这一步,后面接流水线的成本会低很多。

第四项,检查 README 的质量。README 至少要写明环境要求、安装步骤、构建命令、输出路径。这是可复现性最容易被忽视的一环。下面是我这次用到的 README 结构的简化版,你可以直接参考:

# 项目名 基于 ArkUI 的开源鸿蒙跨平台应用工程,适配手机、平板与桌面端。 ## 环境要求 - Node.js 18 LTS - OpenHarmony SDK API 10 - ohpm 配套版本 ## 构建步骤 1. ohpm install 2. hvigorw clean 3. hvigorw assembleHap --mode module -p product=default ## 产物路径 entry/build/default/outputs/default/entry-default-signed.hap

这些都做好之后,远端仓库才不是“一个能下载的代码包”,而是一个能落地交付的工程源。

5. 问题速查与操作心得

5.1 报错速查表

把这次上传过程中遇到的所有问题整理成了一张速查表,方便你在遇到同类问题时直接对照。

报错信息 / 现象根因解决方案
Permission denied (publickey)SSH 密钥未被 agent 加载,或使用了错误的密钥文件确认 Host 配置指向正确密钥;ssh-add加载私钥后重试ssh -T git@atomgit.com
! [rejected] main -> main (fetch first)远端与本地无共同祖先,通常因为远端创建时生成了 READMEgit pull origin main --allow-unrelated-histories合并后重新 push
仓库体积异常大(几百 MB)oh_modules或build目录被 Git 跟踪git rm -r --cached移除索引,配好 .gitignore;如果已推送到远端,需用git filter-repo重写历史或重建仓库
pre-receive hook declined远端分支受保护,不允许直接 push在远端仓库关闭该分支保护规则,或者改用 PR 合并流程
Failed to connect to atomgit.com port 443: timeout网络代理或防火墙拦截检查本地网络代理配置,确认是否能正常访问域名
克隆后ohpm install报版本不兼容锁文件缺失导致依赖版本浮动提交oh-package-lock.json5,固定依赖版本
本地能构建但新环境构建报找不到包oh_package.json5或hvigorfile.ts未完整提交对照本地仓库,把缺失的工程配置补提交
bash: hvigorw: command not found构建脚本未包含在仓库或未被正确执行确认hvigorw脚本在工程根目录,同时注意执行权限

5.2 几点操作心得

这次 DAY2 的折腾,让我对“上传代码”这件事有了完全不同的理解。以前我觉得 push 到远程仓库就是一个备份动作,现在我觉得这是一次工程输出。远端仓库里的内容,应该是一个陌生队友或者未来的自己,只看仓库本身就能重新开始工作的全部依据。

第一个心得:把工程的“出生信息”记录下来,比记录代码功能更优先。项目怎么创建、SDK 版本多少、哪些依赖是通过 ohpm 装的、哪些文件是被刻意忽略的,这些都要让 README 说清楚。我见过很多仓库里代码完整,但一问怎么跑,作者自己也忘了 SDK 版本号。

第二个心得:提交要小步快跑,不要攒一个大提交。我在第一次误提交大目录之后,后面改成“每完成一个模块或修复立刻提交一次”。这样即使后面要重写历史,也不会把大量有效工作一起销毁。

第三个心得:给构建产物打上 tag。当我确认这次 clone 之后构建成功,我立刻给仓库打了一个标签:

git tag v0.1.0-buildable git push origin v0.1.0-buildable

以后只要我想回到“当前这个可靠状态”,就不需要去翻 commit 历史,一个 tag 就解决了。这个习惯看起来简单,但在后续开发中会省掉很多不必要的排查。

第四个心得:失败记录是最有价值的技术文档。这次 DAY2 的坑,包括大目录误提交、SSH 密钥不匹配、远端保护策略冲突,我在网上搜索时都找到了零散的回答,但真正能帮我一次走通的是自己把上下文串联起来。这就是为什么我坚持把每一条报错、每种处理方式都记录在案。你将来再遇到同样问题,直接看这一篇文章,比重新搜索一轮要快得多。

最后再分享一个小技巧:push 失败的时候,先不要着急改仓库。先在本地开一个backup分支,把当前状态固定住,再动手清理大文件或者调整分支。这样即使清理过程出现意外,你也能随时回到原来的状态,不会因为误操作把好不容易写好的代码弄丢。我这次重建远端仓库前就先打了本地备份 tag,整个处理过程虽然看起来“粗暴”,但心里一直有底。实际动手写代码很快,而把工程状态管理清楚,才是慢工出细活的部分。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进