svg
开源 Android 应用的 F-Droid 上架之旅

开源 Android 应用的 F-Droid 上架之旅

2026 年 4 月 7 日

最近我把自己好几年前的开源 Android 项目 WakeUpScreen 提交到了 F-Droid。整个流程其实并不算复杂,但第一次做的时候很容易搞错。这篇文章把完整的流程梳理一遍,包括我自己踩过的坑,希望对同样想把开源 Android 应用送上 F-Droid 的开发者有所帮助。

一、为什么是 F-Droid

聊具体步骤之前,先回答一个问题:已经有 Google Play 了,为什么还要 F-Droid?

F-Droid 是一个完全由社区维护的开源 Android 应用商店,2010 年成立,目前收录了四千多款应用。它和 Google Play 在哲学上有几个根本性的差异:

  • 应用必须是 FOSS (Free and Open Source Software):不能包含任何闭源依赖。Google Play Services、Firebase 这些常用的「全家桶」在 F-Droid 这里是不被允许的,触线即拒。
  • 服务器自己构建:你提交的不是 APK,而是源代码和元数据。F-Droid 的服务器会从你的 git tag checkout 出来重新编译。这意味着用户在 F-Droid 上下载到的 APK 是 F-Droid 自己签名的,不是开发者签名的。
  • 无注册费、无审核拒绝:和 Google Play 那 25 美金注册费、动辄几天审核完全不同,F-Droid 的「审核」是通过一个 GitLab Merge Request 进行的:你提交一个 yml 配置文件,packagers 帮你 review,合并后就自动构建了。

适合 F-Droid 的应用大致有这样的画像:开源、不依赖 Google Play Services、注重用户隐私、希望在 Google Play 之外提供另一个分发渠道。WakeUpScreen 满足所有这些条件:GPL-3.0 协议、不需要联网、不申请网络权限、没有任何统计或广告 SDK。所以这次提交对我来说更像是一次「该做却一直没做」的事情终于补上的过程。

二、变更要点

正式提交流程之前,需要先在自己的项目里做一些准备。

2.1 依赖与许可证自检

F-Droid 第一个会卡你的就是依赖。提交之前我做的第一件事是:

grep -rE "play-services|firebase|com\.google\.android\.gms" .

如果搜出任何匹配,那就是 F-Droid 红线。要么移除,要么这个应用只能被标记为有 anti-feature。

许可证方面,F-Droid 接受所有主流的 FOSS 协议(Apache 2.0、MIT、BSD、GPL 等),完整列表在 fdroiddata 仓库的 templates 目录 里。WakeUpScreen 用的是 GPL-3.0,没问题。

另外要检查依赖的库本身是不是 FOSS。WakeUpScreen 用到的几个第三方库都是允许的:com.tencent:mmkv(BSD)、com.blankj:utilcodex(Apache 2.0)、Glide(BSD)、AndroidX/Compose(Apache 2.0)。这部分大多数项目都不会有问题,注意一下统计/推送类的库就行(友盟、极光、百度统计这些通常都是闭源)。

2.2 fastlane metadata 目录结构

F-Droid 会自动从你的项目里抓取应用描述、截图、changelog,前提是按照 fastlane 的标准结构 放在 fastlane/metadata/android/ 下面。这个目录长这样:

fastlane/metadata/android/
├── en-US/
│   ├── title.txt              (≤ 50 字符)
│   ├── short_description.txt  (≤ 80 字符,不要加句号)
│   ├── full_description.txt   (≤ 4000 字符)
│   ├── changelogs/
│   │   └── 30300.txt          (文件名必须是 versionCode)
│   └── images/
│       ├── icon.png           (512 × 512)
│       └── phoneScreenshots/
│           ├── 1.png
│           └── 2.png
├── zh-CN/
│   └── ...
└── it/
    └── ...

有几个细节容易踩:

  • 每个 locale 的目录都是独立的,title / description 都要单独写一份。如果你的应用只支持一种语言,就只放 en-US/ 也行。
  • changelog 的文件名是 versionCode 而不是 versionName。我的 versionName 是 3.0.3 但 versionCode 是 30300,文件就得叫 30300.txt。这一点 fastlane 文档讲得不够清楚,第一次很容易写错。
  • icon 推荐 512×512,可以直接复用 Android 项目里的 ic_launcher-web.png,那个文件天然就是这个尺寸。

我把 WakeUpScreen 现成的 screenshots/ 目录里的图复用了一下,描述文本则是从 README 提炼的,三种语言(英文、中文、意大利语)都有对应的 README,搬运起来也就是十几分钟的事。

2.3 release.yml:把签名 APK 自动推到 GitHub Releases

这一步是可选的,但强烈推荐。后面提交 F-Droid 的时候,需要从签名 APK 提取一个 SHA-256 cert 指纹填到 yml 里。如果你有一个 GitHub Actions 工作流自动产生签名 APK 并推到 Releases,这一步就只是 gh release download + apksigner verify 两条命令。否则你得自己在本地折腾 keystore、签名、上传,工作量不小。

我的 release.yml 监听的是 gradle.propertiesAppVersionName 字段的变化:每次它变,工作流就用 secrets 里存的 keystore 构建一份签名 APK,自动打 tag、自动创建 GitHub Release。这样我每次发版只要改一行版本号,剩下的全自动,对 F-Droid 提交流程也很友好。

三、提交流程

正式的提交流程分四步:fork fdroiddata 仓库、写 metadata yml、本地验证构建、提交 MR。

3.1 注册 GitLab 账号、fork fdroiddata 仓库

F-Droid 的所有应用元数据都维护在 fdroid/fdroiddata 这个仓库里。注意是 GitLab,不是 GitHub。F-Droid 项目从早期就把所有的 infrastructure 放在 GitLab 上,原因之一是 GitLab 自己也是开源软件,和 F-Droid「一切都应当自由」的理念一致。如果你之前一直只用 GitHub,这里需要稍微调整一下心理预期:接下来的所有操作都在 gitlab.com 上进行。

所以提交流程的第一步是注册一个 GitLab 账号(如果你还没有的话)。访问 gitlab.com/users/sign_up 即可,可以用邮箱注册,也可以直接用 GitHub OAuth 登录,后者其实挺方便,相当于你的 GitHub 账号顺手多了一个 GitLab 镜像身份。注册过程和 GitHub 几乎一样,没什么坑。

注册好之后,访问 gitlab.com/fdroid/fdroiddata,点击右上角的 Fork 按钮,把这个仓库 fork 到你自己的账号下。这是一个标准的 fork & MR 工作流:你在自己的 fork 里改,然后向上游 fdroiddata 提 Merge Request(GitLab 的 MR 等同于 GitHub 的 PR)。第一次 fork 的时候 GitLab 会问你 fork 到哪个 namespace,选你自己的用户名即可。

fork 完成后,把它 clone 到本地,开新分支,拷贝模板:

# clone 你刚 fork 出来的仓库(注意把 <你的用户名> 替换成你的 GitLab 用户名)
git clone --depth=1 [email protected]:<你的用户>/fdroiddata.git
cd fdroiddata

# 为你的应用建分支(分支名 = applicationId)
git checkout -b com.example.myapp

# 拷贝模板
cp templates/build-gradle.yml metadata/com.example.myapp.yml

注意分支名和 yml 文件名都是你的 applicationIdbuild.gradle 里的 applicationId 字段,也就是 package name),不是项目的英文名。比如 WakeUpScreen 的 applicationId 是 com.symeonchen.wakeupscreen,那么分支名和 yml 文件名都得是 com.symeonchen.wakeupscreen

3.2 metadata yml 文件

这是整个流程里最容易出错的一步。下面是我给 WakeUpScreen 写的 yml,把每个字段的意思都标注一下:

Categories:
  - System
License: GPL-3.0-only
AuthorName: riko
AuthorEmail: [email protected]
WebSite: https://riko2chen.github.io/WakeUpScreen/
SourceCode: https://github.com/riko2chen/WakeUpScreen
IssueTracker: https://github.com/riko2chen/WakeUpScreen/issues
Changelog: https://github.com/riko2chen/WakeUpScreen/blob/HEAD/docs/CHANGELOG.md

AutoName: WakeUpScreen

RepoType: git
Repo: https://github.com/riko2chen/WakeUpScreen.git

Builds:
  - versionName: 3.0.3
    versionCode: 30300
    commit: 456b21655618b0baffc27328e01199aa2c2f9a28    # 完整 commit hash,不要用 tag 名
    subdir: app          # build.gradle 所在的子目录
    gradle:
      - yes              # 用默认的 gradle flavor

AllowedAPKSigningKeys: <SHA-256 cert 指纹,小写无空格>

AutoUpdateMode: Version
UpdateCheckMode: Tags
CurrentVersion: 3.0.3
CurrentVersionCode: 30300

几个关键字段值得展开:

  • Categories:F-Droid 的预设分类,常用的有 System、Connectivity、Internet、Multimedia 等。完整列表在 fdroiddata 的 config/categories.yml 里。

  • License:必须是 SPDX 标识符,比如 GPL-3.0-onlyApache-2.0,不能写成 GPL3 这种自定义形式。

  • Builds:可以列多个,对应每个 release 的 commit。F-Droid 会从这个 commit checkout 出来重新构建。commit 字段一定要写完整的 commit hash。tag 看着方便,但毕竟是可移动引用(git tag -f 一行命令就能改指向),hash 才是内容指纹,F-Droid 出于供应链安全只接受 hash。git rev-list -n 1 <tag> 即可拿到。第一次提交时只要写当前最新版本就行,之后由 AutoUpdateMode 自动追加(机器人会把新 tag 解析成 hash)。详见 4.5 节。

  • AllowedAPKSigningKeys:这是新增的安全字段,作用是限定只接受用这个 key 签名的 release,防止有人把恶意 APK 伪装成你的版本提交。提取方式:

    # apksigner 在 Android SDK 的 build-tools 目录下
    ~/Library/Android/sdk/build-tools/35.0.0/apksigner verify --print-certs your-release.apk
    # 找 "Signer #1 certificate SHA-256 digest:" 那一行的内容,去空格转小写
  • AutoUpdateMode + UpdateCheckMode:让 F-Droid 自动监听你的版本号变化,新版本一旦发布就自动收录。最常用的组合是 AutoUpdateMode: Version + UpdateCheckMode: Tags:F-Droid 遍历你的 git tag、checkout 出来用正则在 app/build.gradle 里找 versionCode <数字>versionName "X.Y.Z"。这是开源应用最省心的配置。如果你的 build.gradle 是从 gradle.properties 用 property reference 读版本号(像 WakeUpScreen 这样),Tags 模式默认的 build.gradle 解析器看不懂 property。但有救:给 Tags 模式额外配一个 UpdateCheckData,让 F-Droid 改读 gradle.properties。我在 4.1 节走了一段弯路最后选了 HTTP,4.5 节才在 review 中学到 Tags + 自定义 UpdateCheckData 是更地道的写法。

  • Changelog:F-Droid 推荐用 /HEAD/ 而不是 /master//main/ 来指向默认分支的文件。fdroid lint 会主动 flag 这个。

3.3 本地验证:lint 必跑、build 看情况

本地验证分两层:轻量的 lint / checkupdates / rewritemeta一定要跑),和重量的 fdroid buildApple Silicon 用户基本跑不起来,下面 4.2 节会展开)。

轻量验证不需要 Docker,几秒钟就能跑:

brew install fdroidserver

cd ~/Project/fdroiddata
fdroid readmeta
fdroid rewritemeta com.example.myapp     # 让 yml 格式定型,必跑
fdroid lint com.example.myapp
fdroid checkupdates --allow-dirty com.example.myapp
  • rewritemeta 会按照 F-Droid 的格式规范重排你的 yml(给长字段换行、规范缩进等)。第一次提交一定要跑,否则 CI 会报「files need rewritemeta」(4.3 节有具体例子)。
  • lint 检查 yml 字段完整性、URL 规范、类别合法性等。报错根据提示修。
  • checkupdates 验证你的 UpdateCheckMode 配置能不能正确发现新版本。如果报「Couldn’t find any version information」,看 4.1 节。

完整的 fdroid build 会用 F-Droid 的官方 Docker 镜像 registry.gitlab.com/fdroid/fdroidserver:buildserver 复现整个编译过程,命令大概长这样:

git clone --depth=1 https://gitlab.com/fdroid/fdroidserver ~/fdroidserver

docker run --rm -itu vagrant --entrypoint /bin/bash \
  -v ~/Project/fdroiddata:/build:z \
  -v ~/fdroidserver:/home/vagrant/fdroidserver:Z \
  registry.gitlab.com/fdroid/fdroidserver:buildserver

进入容器后:

. /etc/profile
export PATH="$fdroidserver:$PATH" PYTHONPATH="$fdroidserver"
export JAVA_HOME=$(java -XshowSettings:properties -version 2>&1 > /dev/null | grep 'java.home' | awk -F'=' '{print $2}' | tr -d ' ')
cd /build
fdroid build -v -l com.example.myapp

跑通了基本就稳。跑不通的话常见原因:构建过程中下载外部资源(F-Droid 是离线构建)、NDK / JDK 版本不匹配、proguard 规则依赖闭源库等。

但如果你在 Apple Silicon 上:这个 Docker 镜像只有 amd64 版本,本地几乎跑不动。F-Droid 自己的 CI 跑在 amd64 runner 上,提交后 CI 会帮你跑。详见 4.2 节,那边讲了为什么这是「跳过 build 也 OK」的事。

3.4 提 MR

提 MR 之前先去 RFP 仓库搜一下你的应用名。F-Droid 有一个专门的 Request For Packaging 仓库,让用户向 F-Droid 申请「希望某个开源应用被收录」。很可能你的应用早就被某个用户提过 RFP,issue 还开着。我搜「wakeupscreen」的时候发现 rfp#2023 “New APP: WakeUpScreen” 是 2022 年另一个用户提的,开了两年没人 fulfill。在 MR description 里加一行 Closes rfp#2023,合并时会自动关掉这个老 issue,对 reviewer 也是个好消息:「这是个长期需求」。

搜的命令:

curl -sSL "https://gitlab.com/api/v4/projects/fdroid%2Frfp/issues?search=YOUR_APP_NAME&state=all" \
  | python3 -m json.tool

或者直接到 https://gitlab.com/fdroid/rfp/-/issues 用搜索框找。

然后正式提 MR:

git add metadata/com.example.myapp.yml
git commit -m "New App: com.example.myapp"
git push origin com.example.myapp

push 完 GitLab 会在终端打印一个创建 MR 的链接,直接点开。在 GitLab 创建页面上要做两件事:

  1. 改 target project / branch:默认指向你 fork 内部的 master,要改成上游 fdroid/fdroiddatamaster
  2. 填 description:MR 模板里有几个 checkbox 和一个「Reproducible Builds」选项需要思考一下,下文 4.4 节会展开 RB 这个选择

提完之后会触发 GitLab CI 跑一系列检查(lint、rewritemeta、build)。如果失败,CI 的日志会告诉你原因,4.3 节有一个我自己撞的具体例子。

CI 全过之后就是等 review。F-Droid 的 packagers 会在几天到几周之内 review,期间可能会要求你修改 yml 或者解决构建问题。合并之后,下一次的 build cycle(每天一次)会自动构建、签名、发布到 f-droid.org

四、踩过的坑

文档里的流程是「一切顺利的情况」,实际上提交过程中我撞了好几次墙。把这些都写下来,也许能省你几个小时。

4.1 fdroid checkupdates 看不懂 gradle.properties 引用

UpdateCheckMode: Tags 是最常见、最推荐的配置:F-Droid 服务器克隆你的仓库、遍历每个 git tag、用正则解析 app/build.gradleversionCode/versionName。听起来很自然,但它要求 build.gradle 里这两行必须是字面量

versionCode 30300                    // ✅ 能识别
versionName '3.0.3'                  // ✅ 能识别

versionCode project.AppVersionCode   // ❌ 识别不了
versionName project.AppVersionName   // ❌ 识别不了

WakeUpScreen 的 release.yml 工作流依赖 gradle.properties 作为版本号唯一来源(改一行就能触发 CI 自动构建发版),所以 app/build.gradle 是用 property reference 读取的。第一次跑 fdroid checkupdates 报:

ERROR: Couldn't find any version information

fdroidserver/checkupdates.py 里负责解析的是个简单的 Python 正则,不会执行 Gradle,自然 resolve 不了 property reference。这是工具链的长期限制,不是 bug。

解决方案有三个:

  1. build.gradle 把版本号硬编码:最简单但破坏现有 release 工作流,不划算
  2. UpdateCheckMode: None:关掉自动检测,每次发版手动加 build entry,能用但啰嗦
  3. UpdateCheckMode: HTTP:直接 hit gradle.properties 的 GitHub raw URL,用正则提取版本号

我选了 3,最终配置长这样:

AutoUpdateMode: Version v%v
UpdateCheckMode: HTTP
UpdateCheckData: 
  https://raw.githubusercontent.com/riko2chen/WakeUpScreen/HEAD/gradle.properties|AppVersionCode=(\d+)|.|AppVersionName=([\d.]+)

UpdateCheckData 的格式是 <vercode_url>|<vercode_regex>|<vername_url>|<vername_regex>,第三个字段写 . 表示「跟第一个字段同 URL」。AutoUpdateMode: Version v%v 里的 v%v 是 git tag 命名 pattern:我的 tag 是 v3.0.3v3.0.4 这样,所以前缀是 v

这样配好之后,F-Droid 服务器每天会自动 hit raw URL、提取版本、追加 build entry、触发构建。我未来发版只要改 gradle.properties 一行,剩下全自动,连 fdroiddata 都不用碰。

4.2 Apple Silicon 跑不动 buildserver 镜像

F-Droid 文档强烈建议提交前用 Docker 跑一次 fdroid build 验证。我跟着做,结果发现:

  1. brew install docker 只装了 CLI,没有 daemon。macOS 不像 Linux 有原生 Docker daemon,得用 Colima 或 Docker Desktop 提供 daemon。我装了 Colima(轻量、CLI、开源):brew install colima && colima start,之后 docker CLI 就能用了。注意 colima 不会自动注册 docker context,需要手动 docker context create colima --docker host=unix://$HOME/.colima/default/docker.sock && docker context use colima

  2. registry.gitlab.com/fdroid/fdroidserver:buildserver 镜像只有 amd64 一个版本。F-Droid 的 GitLab CI 跑在 saas-linux-small-amd64 runner 上,他们的 .gitlab-ci.yml 里也没有 buildx 多架构构建。所有 6 个 tag(buildserverbuildserver-bookwormbuildserver-trixie 等)我都查过 manifest,全是 single-arch amd64。这是 F-Droid infrastructure 的一个长期事实,不是 bug。

  3. 在 Apple Silicon 上跑 amd64 镜像意味着 QEMU 模拟,加上 Colima 默认 1.9 GB 内存对 Android 构建来说太小(Gradle 自己就要 2 GB heap),实际是「跑不起来」。

破解方法理论上是 colima delete && colima start --arch x86_64 --memory 8 --cpu 4,让整个 Colima VM 在 QEMU 里以 x86_64 跑,VM 内的容器原生执行。但镜像要重拉、构建要 30-60 分钟,且 Apple Silicon 上 QEMU 性能也就那样。

实用结论:Apple Silicon 用户的最优解是跳过本地 fdroid build,依赖 F-Droid 的 CI 验证fdroid lintfdroid checkupdates 在本地跑没问题(不需要构建环境),这两步过了就放心交 MR。F-Droid 服务器自己有 amd64 环境,他们会跑 build。在 MR description 里诚实说明「本地没跑 build,因为 buildserver 镜像 amd64-only 加 Apple Silicon,依赖 CI 验证」,packagers 完全接受。

4.3 fdroid rewritemeta 的本地版本和 CI 不一致

提交后 CI 第一次失败,报错说 metadata 文件需要 rewritemeta

These files need rewritemeta:
metadata/com.symeonchen.wakeupscreen.yml

These are the formatting issues:
- UpdateCheckData: https://raw.githubusercontent.com/riko2chen/WakeUpScreen/HEAD/gradle.properties|...
+ UpdateCheckData:
+   https://raw.githubusercontent.com/riko2chen/WakeUpScreen/HEAD/gradle.properties|...

CI 把我的 UpdateCheckData 行改成了 YAML block scalar 格式(值换到下一行,缩进 2 格),并要求我的文件跟它的输出 byte-by-byte 一致。这是 F-Droid 对 metadata 文件的格式强制规范,正常应该在本地跑 fdroid rewritemeta 之后再提交,让格式定型。

我本地跑 fdroid rewritemeta 之后才发现:brew 装的 fdroidserver 2.4.3 比 CI 用的版本更激进。它把 ChangelogAllowedAPKSigningKeysUpdateCheckData 三个字段全部换行了,而 CI 那边只换行 UpdateCheckData。换行后的「字段名: + 值另起一行」格式还会带一个 trailing space,触发 fdroid lint 的 trailing-spaces warning。

最后的修法是:跑一次本地 rewritemeta,然后用 Python 脚本手动 unwrap 不该换行的字段(在我这是 ChangelogAllowedAPKSigningKeys),保留 UpdateCheckData 的 wrapped 格式。这样跟 CI 的输出对齐了,重新 push 后 CI 通过。

教训:

  • 第一次提交一定先在本地跑 fdroid rewritemeta 让格式定型再 push
  • 如果 CI 还报 rewritemeta 错,仔细看 CI 给出的 diff,按 CI 的格式改,不要相信本地 rewritemeta 的输出
  • fdroid lint 报 trailing-space warning 在 wrapped 行上是正常的(warning 不阻塞,exit 0),不要试图修

4.4 Reproducible Builds 是个永久性决定

F-Droid 的 MR 模板里有一个 checkbox 「Enable Reproducible Builds」,注释里有这么一句话:

Do note that if you don’t enable reproducible build then the apk will be signed with our key so you can’t enable it later.

很关键的话,但解释得不够清楚。展开一下:

  • 不启用 RB(默认):F-Droid 用他们自己的 key 重新签你的 APK。F-Droid 上的版本和你 Google Play 上的版本是两个独立的 APK,签名不同。Android 系统认为「签名不同 = 不同的应用」,所以用户在两个商店之间装的版本不能互相升级、不能共享数据
  • 启用 RB:你提前把签名证书的公钥提交给 F-Droid,F-Droid 服务器构建出 APK 后跟你本地构建的版本对比 byte-by-byte 一致,然后用你的 key 签名。这样 F-Droid 和 Google Play 上的 APK 完全等价,用户可以无缝切换。

启用门槛真的不低:你要让本地构建和 F-Droid Linux 服务器的构建 byte-by-byte 一致,需要消除时间戳、统一路径、固定 ZIP entry 顺序等等一堆事,反复测试。Apple Silicon 上更难(环境差异更大)。

「永久性」指的是:不启用 → F-Droid 用他们的 key 签 → 已经下载的用户拿到的是这个签名 → 后来想启用 RB(用你的 key),已下载的用户没法升级(Android 拒绝跨签名升级)。唯一的破解办法是发一个新 applicationId,相当于换了应用。

对 WakeUpScreen 这种「设置简单、跨平台同步需求弱」的工具应用来说,第一次提交不启用是合理选择。F-Droid 上四千多个应用大部分都没启用 RB。如果你的应用是数据库类(密码管理、笔记),跨平台数据迁移是核心需求,那就值得啃下 RB,但建议单独一次集中精力做,而不是混在第一次提交里。

4.5 Review 之后才学到的:commit 用完整 hash,UpdateCheckMode 还是该选 Tags

CI 全过、MR 提交完之后,F-Droid 的 packager 一天之内就 review 了。两条意见,把前面 4.1 节的「最终方案」彻底推翻。

第一条:Builds.commit 必须写完整的 commit hash。

我原来的 build entry 写的是 commit: v3.0.3,packager 直接说:

Don’t use tag in commit. Use the full commit hash instead.

原因是 tag 在 git 里是可移动引用git tag -f v3.0.3 <另一个 commit> 一行命令就能让 v3.0.3 指向完全不同的代码。如果哪天我有意或无意移了 tag,F-Droid 下次构建出来的代码就可能跟当初 review 通过的版本完全两样。commit hash 则是内容指纹,永远固定,456b21655618b0baffc27328e01199aa2c2f9a28 唯一且永远地对应那一份代码。F-Droid 出于供应链安全要求所有 Builds.commit 字段都写完整 hash。

获取 hash 一行命令:

git rev-list -n 1 v3.0.3
# 或者从远程仓库查
git ls-remote https://github.com/riko2chen/WakeUpScreen.git refs/tags/v3.0.3

好消息是这件事只需要手动做一次AutoUpdateMode: Version 配合 F-Droid 的 checkupdates 机器人之后,机器人在每次检测到新 tag 时会自己把 tag 解析成 hash 写进新的 build entry。我未来发版只要在 WakeUpScreen 仓库里打 tag,fdroiddata 这边的 build entry 是机器人自动追加的,连 commit hash 都不用我自己写。

第二条:UpdateCheckMode 应该用 Tags,而不是我在 4.1 里折腾出来的 HTTP

这条更让我意外,因为 4.1 节的结论是「Tags 模式看不懂 gradle.properties property reference,所以只能用 HTTP」。packager 给的 suggestion 是:

AutoUpdateMode: Version
UpdateCheckMode: Tags
UpdateCheckData: gradle.properties|AppVersionCode=(\d+)|.|AppVersionName=([\d.]+)

注意这里的 UpdateCheckData 写的是仓库内的相对路径:F-Droid 会在每个 git tag 的 checkout 里去读这个文件。我之前一直以为 UpdateCheckData 只能配合 UpdateCheckMode: HTTP 使用,但其实 Tags 模式同样接受 UpdateCheckData:F-Droid 会遍历每个 git tag,在 tag 的 checkout 内用这个文件路径加正则提取版本号,既绕过了 build.gradle 的 property reference 问题,也不需要走 HTTP 拉远程文件。

回头看 4.1,错的根源是我当时只读了 fdroidserver 文档里 Tags 模式那一段,把「Tags 模式只能解析 build.gradle」这个错误结论当成了事实。Tags + 自定义 UpdateCheckData 才是 F-Droid 最推荐、最干净的方案。它仍然走 git tag 这条主路径(和 F-Droid 「从 tag 构建」的整体哲学一致),只是把版本号提取的细节交给我自己定义的正则。HTTP 模式相比之下既绕路又依赖 raw.githubusercontent.com 的可达性,属于次等方案。

修正后我的 yml 末尾就变成了:

Builds:
  - versionName: 3.0.3
    versionCode: 30300
    commit: 456b21655618b0baffc27328e01199aa2c2f9a28
    subdir: app
    gradle:
      - yes

AllowedAPKSigningKeys: 7044dcb7b20edcefdb22923e649c84187e3aa818fc3a58dfcb2eccc92cddef1c

AutoUpdateMode: Version
UpdateCheckMode: Tags
UpdateCheckData: gradle.properties|AppVersionCode=(\d+)|.|AppVersionName=([\d.]+)

教训:

  • Builds.commit 永远写完整 hash。F-Droid 的硬性安全要求,CI 不会拦但 reviewer 一定会指出
  • UpdateCheckMode: Tags + 自定义 UpdateCheckData 是 gradle.properties property reference 项目的正解,比 HTTP 模式更简洁、更不依赖外部资源,也和 F-Droid「从 tag 构建」的整体哲学保持一致
  • CI 跑通不等于配置最优。F-Droid 的 packagers 看过太多 yml,他们的直觉比 linter 准。第一次提交不要因为 CI 绿了就满足,老老实实等真人 review 给的 suggestion,多半能学到更地道的写法

五、总结

整个流程梳理下来,把一个开源 Android 应用上架 F-Droid 大致就是这几件事:

  1. 确认依赖干净:grep 一遍 play-services / firebase / gms,确认许可证是 FOSS
  2. 加 fastlane metadata:title / short / full description / changelog / icon / screenshots,按 locale 分目录
  3. 每个 release 都打 git tag:F-Droid 是从 tag 构建的
  4. 写 fdroiddata yml:在 GitLab 上 fork、写文件、本地 rewritemeta + lint + checkupdates 验证、提 MR
  5. CI 来回几次:rewritemeta 格式、UpdateCheckMode 配置这些 CI 会帮你抓出来
  6. 耐心等 review:几天到几周

相比 Google Play 那种「上传 APK → 审核 → 上线」的流程,F-Droid 多了「写 metadata → CI 跑通 → 等服务器构建」这几步,但好处是整个过程是公开的、可追溯的、永远开源的。F-Droid 服务器构建出来的 APK 用 F-Droid 的 key 签名,意味着即使开发者本人消失了,应用还是可以由社区维护下去。这一点在 Google Play 的世界里是无法想象的。

我觉得这种「再普通不过的开源软件分发渠道」,恰好是当下最稀缺的东西。Google Play 越来越像一个商业平台,App Store 从来就是一个商业平台,而 F-Droid 还坚持着 2010 年那种「软件应该自由」的初心。所以即使工作量稍大一点,把开源应用送一份到 F-Droid,是值得做的事。

F-Droid 首页上常年挂着一句口号:「Free as in freedom」。2010 年读这句话像理想主义宣言,2026 年再读更像一记警钟。Google 这两年在 Android 的开放性上不断踩刹车,所有上架开发者强制实名认证、应用签名托管走 Play App Signing、就连 sideload 安装也开始被纳入开发者注册体系,整个 Android 生态正在悄悄地从「开放平台」向「带闸的花园」迁移。在这个背景下,F-Droid 这样一个完全社区驱动、对开发者零门槛、对用户零追踪、永远开源的商店,越来越像稀有物种。把一款开源应用上架到 F-Droid 这件事本身,就已经是一种站队,给这个稀有物种多续一份口粮。

这篇文章是我为 WakeUpScreen 提交 F-Droid 过程中的实践记录,所有 yml 字段、命令和踩过的坑都来自真实经历。如果你正在做同样的事,欢迎在 GitHub issues 里交流。

让我们为世界创造一些美妙和优雅

If you have anything you'd like to discuss, any ideas you want to bounce around, please send me a message.

svg