跳到主要内容
自己的包,自己发

自己的包,自己发

原石
原石

· 阅读约 5 分钟

2026 年 9 月 3 日凌晨 4:40 提交的一个 alpha,到现在还在队列里躺着——大家在讨论帖里聊开了,它还没出来。

这个项目十七年了。同一份代码,另一个渠道 9 月 2 日就打好了包、签好名、发出去了。大商店那边,8 月 31 日给了一个拒。

拒的原因我不猜。拒本身不是问题,问题是这中间的两周,你什么都不知道。

很多人把这归成"审核慢"。慢不是重点。F-Droid 也慢,构建队列排到几周是常事。但你能看见它卡在哪个环节——构建容器起不来,还是签名那步挂了,还是前一个包还在跑。同样是等,一个叫排队,一个叫石沉大海。

这区别很贵。

所以我的立场很直接:分发是核心路径。核心路径上的东西,别托管给一个你看不见内部的队列。

先把东西贴上。一个能用的自建仓库,目录就这些:

repo/
  index.json
  index.json.asc
  icons/com.example.app.png
  com.example.app_25000400.apk

比你想的少。三个文件加一个图标,够发。

签名是两件事,别搅一起。APK 用你的 release key 签,那是给系统看的;index 再用一把 repo key 签一次,那是给用户看的——证明这个源还是当初那个源,中间没被谁换过。两把密钥各管一段。

index 我只留这几个字段:

{
  "packages": {
    "com.example.app": {
      "name": "Example",
      "versions": {
        "25000400": {
          "apk": "com.example.app_25000400.apk",
          "versionName": "2.25.0alpha4",
          "signer": "ab12...",
          "size": 17825792
        }
      }
    }
  }
}

signer 那个字段是这套东西里唯一有点意思的地方。用户第一次装的时候,客户端把 APK 的签名指纹记下来;以后每次更新,先比一次指纹。对不上,不装。所以只要你手里那把 release key 不丢,你的用户就永远能从你这里升级——不管哪个商店当下正不高兴。

发布就是一条 make:

release: $(APK)
	./gen-index.sh > repo/index.json
	gpg --detach-sign repo/index.json
	rsync -a --delete repo/ host:/srv/repo/

gen-index.sh 是我自己写的,几十行:读一遍目录、算一遍 size 和指纹、拼出上面那个 JSON。有人会说这不是重复造轮子吗,fdroid 的工具链就是干这个的。

是。但 fdroid update 那玩意儿要读一整套 metadata 格式、要装一个 python 环境、要理解它的目录约定——为的是生成同一个 JSON。

几十行脚本能解决的事,没必要引入一个工具链。

有人提了个提议,给那些已经成功过很多次的开发者上指数退避:审得越多次越顺,就放宽间隔。听着聪明,其实是错的。它是在优化一个你观测不到的黑箱。退避也好,白名单也好,你还是在猜队列的脾气。真正的解法不是让队列更聪明,是让队列不成为唯一的出口。

说句题外话。这事让我想起"企业级"那三个字。凡是一个东西宣称自己适配所有场景,它内部一定长满了你永远用不到的分支。审核系统也是——它要同时处理一个日更三次的聊天应用和一个三个月没动过的记账本,那它对前者就是慢的,对后者就是快的。这不是 bug,是它必须照顾所有人的代价。有人报告一个十六年的老应用三十分钟过审;十七年的那个,两周。同一个队列,同一套规则。你猜不出为什么。

这不是 Google 一家的毛病。Apple 那边有人今年夏天做更新,不到一小时就批;也有人被随机问一句"确认一下定价和国家",然后多等两天。同一件事,两家的数字能反过来。这更说明问题不在谁认真谁不认真——在黑箱里面,你永远不知道今天抽到的是哪支签。

排队之外,还有第二笔账。

同一个开发者说,接下来一个半月休假全职做项目,但上个月大部分时间不是在写功能,是在给每一个界面补 edge-to-edge。因为大商店要求所有界面适配,不做就不给过。

补这个不难,难的是乘出来。这类代码我能背下来:

ViewCompat.setOnApplyWindowInsetsListener(root) { v, insets ->
    val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
    v.updatePadding(top = bars.top, bottom = bars.bottom)
    insets
}

八行。然后这个项目 minSdk 24、还在用 XML 视图、支持横竖屏加折叠屏加平板,几百个屏幕,每个屏幕底下的 ViewGroup 层级都不一样——有的根是 FrameLayout,有的根是 DrawerLayout,有的中间还嵌了一个自己的 Toolbar。八行代码乘几百个屏幕,再乘每种配置,就是几小时一次、来回几百遍。

有人给了个自动化的思路:先给控件打无障碍标记,再让 agent 在模拟器里录操作、复现、截图,最后让 agent 自己跑一遍应用,把该补的地方找出来。这个方向我认为是对的。它跟"让 AI 设计这个模块"不是一回事——它是让模型替你跑重复的验证动作,边界清楚、结果可核,断言还是你自己看。真正费时的那部分它替不了:每个屏幕该怎么垫,还是得你判断。

两头挤。一头是排队,一头是别人的 UI 规则。都不由你决定。

那就更得把能拿回来的那部分拿回来。分发就是你能拿回来的那部分。

我不想把话说得跟自建仓库免费一样。它不是。用户要手动加一次源,这一步劝退一大半人;带宽是你的,机器是你的,index 写坏一次,全量用户升级失败,而且你连个申诉的地方都没有。要做大,这套东西迟早得补镜像、补灰度、补回滚。

但这些都是你能看见的成本。看得见的成本可以排期,看不见的等待只会让人暴躁。

所以我的规则是:凡是一个东西的出口只有一条别人控制的管道,先把第二条铺上,哪怕它又慢又丑又没人用。铺第二条不是为了替代第一条,是为了第一条堵住的时候,你的项目还能往前走一步。

慢但归我,比快但看别人脸色划算。

原石
原石

把代码当文章写的系统工程师,以源码立论、单线程式拒绝复杂度。

查看主页 →