mcpField
S
solana-mobile-publishing

solana-mobile-publishing

github.com/solana-mobile云与 DevOps87.7k 次安装1 周前更新
代码仓库

关于 solana-mobile-publishing

面向 Solana dApp Store 发布 Android 应用的指南和命令行工作流,涵盖发布者设置、签名 APK 构建、版本发布及中断后的恢复。

主要功能

  1. 01设置发布者和 App NFT
  2. 02签名 APK 构建指南
  3. 03通过命令行发布版本
  4. 04恢复符合条件的失败会话
  5. 05配置 API 密钥和发布门户

适用场景

  1. 01注册发布者和应用列表
  2. 02发布首个 Android 版本
  3. 03使用原有签名密钥发布更新
  4. 04继续处理中断的发布会话

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/solana-mobile/solana-mobile-skills --skill solana-mobile-publishing

安装后在 Claude Code 中输入/solana-mobile-publishing确认已加载。

附带文件references/signing.md

详细介绍

发布到 Solana dApp Store

发布到 dApp Store 分为一次性设置和每次发布都要执行的构建、签名和发布流程。一次性设置会铸造链上 App NFT,其中包含不可逆的决策,因此务必在首次发布前处理妥当,而不是事后补救。

npm install -g @solana-mobile/dapp-store-cli@latest

需要 Node 18 或更高版本。如果不想全局安装,也可以使用 npx @solana-mobile/dapp-store-cli。

请使用 1.0.1 或更高版本。 早期版本已弃用,无法正常工作。

不容妥协的限制

只能使用 APK。.aab 会被拒绝。 使用 assembleRelease 构建,切勿使用 bundleRelease。这能避免那些习惯了 Google Play(默认使用 bundle)的开发者踩坑。

密钥库和发布者钱包都是永久的。 更新必须使用与上一个版本相同的密钥签名,而 App NFT 存储在发布者钱包中。丢失其中任何一个都无法恢复,也没有任何支持工单可以解决——该应用只能以新身份从头重新上架。两者都不应放在仓库中:生成密钥库前,先确认它应该存放在哪里,并确保它不在项目目录树中。

如果也要发布到 Google Play,请使用不同的签名密钥。 每个商店使用一个密钥。共用一个密钥意味着一旦密钥泄露或丢失,两个商店的应用都会同时受影响。

首先:判断你属于哪种情况

情况操作
还没有发布者账户一次性设置
已有发布者账户,准备发布一个版本构建签名 APK,然后发布
为已上线的应用发布更新发布更新
发布版本崩溃,但调试版本正常阅读 references/signing.md
发布过程中途失败恢复失败的发布
应用是封装的 Web 应用,而非 React Native 应用Web 应用

一次性设置

先准备发布者密钥对

CLI 使用密钥对文件以发布者身份签名,而在浏览器门户中连接钱包并不会生成密钥对文件。注册任何内容之前,先准备好该文件,因为门户中连接的钱包必须与该密钥一致。如果顺序反了,就得将浏览器钱包的私钥导出到磁盘,这在各方面都更糟。

如果已有用于此目的的密钥对,请使用它的路径,并先询问应该使用哪个密钥对,不要随手使用 ~/.config/solana/id.json 中的内容——那里通常是临时的开发密钥,用错密钥签名会导致门户无法识别发布者。

如果还没有密钥对,应由开发者生成,而不是 Agent:

solana-keygen new --silent --outfile ~/.config/solana/publisher.json

--silent 会隐藏助记词。如果不加此参数,十二个单词会输出到 stdout,而 Agent 的 stdout 会被记录为对话记录和日志文件——完整的钱包内容会以纯文本形式出现在无人保护的地方。只有在输出不会被记录的终端中才省略 --silent,无论如何都要备份密钥对文件:助记词被隐藏后,该文件就是唯一副本。Agent 只能拿到文件路径。

应像对待发布密钥库一样对待该文件:它与其控制的 App NFT 一样永久,因此必须存放在机密信息存储库或密码管理器中,绝不能放在仓库里。有关 Android 密钥的同样说明,请参阅 references/signing.md。

接着注册发布者

将该密钥对导入由你长期控制的钱包,并使用它在 https://publish.solanamobile.com 上注册。该门户会创建发布者、收集 dApp 列表详情,并铸造 App NFT。

创建列表与铸造 App NFT 不是同一个步骤,而且 CLI 两者都无法完成——它发布的是已存在应用的版本。列表信息已填写但从未铸造 App NFT 时,门户中看起来一切完整,但发布流程会在第 2 步失败,并显示 App NFT and wallet authority must exist before publishing a version。首次发布前,请确认列表已有 App NFT 和钱包权限主体。

开始前先为钱包注资——目前门户要求大约 0.2 SOL,用于支付交易费和元数据存储费用。请将此数值视为参考,并以门户实际报价为准,因为该费用会随租金和存储定价变动。这是注册费用,与 CLI 自身的预检费用分开;CLI 的预检要求签名者持有 0.016 SOL,否则不会开始发布。

然后在 Dashboard > Settings > API keys 中创建 API 密钥,并将其放入环境中:

export DAPP_STORE_API_KEY='paste-the-portal-api-key'

DAPP_STORE_API_KEY 是 CLI 默认读取的变量。在交互式提示符中输入时,export 也会写入 shell 的历史文件,因此最好通过密钥管理器加载,而不是直接粘贴。--api-key-env <name> 可让 CLI 使用其他变量,--api-key-stdin 则从标准输入读取密钥。在 CI 中,环境变量比管道更容易被转储,因此后者是更好的选择:

printf '%s' "$DAPP_STORE_API_KEY" | dapp-store --api-key-stdin <publish flags>

这只是传递密钥的形式——完整调用方式见 发布,此时你还没有构建所需的 APK。

有两个标志可用于让 CLI 指向非生产环境的门户:--local-dev 允许使用 localhost 门户并跳过自更新检查;--skip-self-update 则单独跳过该检查——但仅限与 --local-dev 一起使用,因此单独传入会失败,并显示 `--skip-self-update` is only allowed together with `--local-dev`。本地开发模式会拒绝任何非本地门户 URL,因此无法通过它访问托管的预发布门户;请改用 --portal-url。

构建已签名的 APK

具体使用哪条命令取决于谁持有密钥库——与 references/signing.md 中的划分相同。

EAS 管理的凭据。 使用 dapp-store 配置构建,该配置会强制生成 APK:

eas build --platform android --profile dapp-store

EAS 会为其签名并返回下载 URL,因此此流程中没有 android/app/build/outputs/ 路径。你可以下载构建产物并发布本地文件,也可以将 URL 传给 --apk-url 来跳过下载——但如果是更新,请下载文件,因为 --apk-url 也会跳过下文的签名检查。

本地签名——适用于裸 React Native,或在本地构建的 Expo:

(cd android && ./gradlew :app:assembleRelease)

子 Shell 很重要:如果没有它,cd 会持续生效,后续每条命令中相对于仓库根目录的路径都会在 android/ 下解析并导致失败。

APK 会生成在 android/app/build/outputs/apk/release/app-release.apk。

无论使用哪种方式,上传前都要确认 APK 已由你预期的密钥签名。 此处以及发布中的 $APK 指构建生成的文件——上面的 Gradle 输出路径中的文件,或从 EAS 下载的构建产物。

"$ANDROID_HOME"/build-tools/<version>/apksigner verify --print-certs "$APK"

在常规 Android SDK 安装中,apksigner 不在 PATH 中——直接运行 apksigner 会得到 command not found。它位于 $ANDROID_HOME/build-tools/<version>/ 下;请选择已安装的最高版本。

apksigner verify 只读取 APK,不需要其他内容——无需密钥库或私钥,因此它既适用于下载的 EAS 构建产物,也适用于本地构建。更新时,将输出的 SHA-256 指纹与上一版本的指纹进行比较。这是上传前唯一能发现构建使用了错误密钥库的方法;更新时使用了错误密钥,是唯一无法恢复的故障。

要查看证书名称,而不只是退出代码。 标准的 Expo 预构建会将 release 变体配置为使用 debug 密钥库,因此,未配置的 release APK 也会有签名——只是密钥不对。apksigner verify 会顺利通过。如果输出的 DN 是 CN=Android Debug,说明签名配置没有生效;请参阅 references/signing.md。

上传前还有一项值得检查:默认的 React Native release APK 会打包所有 ABI,大小远超 100 MB。针对单一架构构建(./gradlew :app:assembleRelease -PreactNativeArchitectures=arm64-v8a)通常可以将体积缩小一半以上。

有关密钥库创建、Gradle 签名配置、EAS 构建配置,以及仅在 release 构建中出现的故障模式,请参阅:references/signing.md。

发布

首次发布及之后每次发布,都只需一条命令:

dapp-store \
  --apk-file "$APK" \
  --keypair ~/.config/solana/publisher.json \
  --whats-new "Initial release"

$APK 是构建步骤生成的文件——Gradle 输出路径或下载的 EAS 构建产物。 它没有默认值,只有在你自行将文件复制到该位置时,裸路径 ./app-release.apk 才能使用。 也可以直接将 EAS 构建 URL 传给 --apk-url https://…,这样无需本地副本即可发布托管的 APK。

--keypair <path> 用于选择 Solana 签名密钥。CLI 不会假定默认的密钥对路径,因此请明确传入; 上面的路径只是占位符,应替换为持有发布者身份的密钥对路径。--verbose 会在发布和会话标识 生成时打印它们;运行失败时,你需要这些信息。

发布本身由门户处理,因此无需配置 RPC 端点。--rpc-url <url> 确实存在,但在 --help 中被 隐藏;它只用于预检,在提交前检查签名者的余额。门户默认地址为 https://publish.solanamobile.com;--portal-url <url> 或 DAPP_STORE_PORTAL_URL 可更改 门户地址,只有在 Solana Mobile 为你提供了暂存门户时才需要这样做。

更改门户地址意味着也要更改 RPC 地址。 无论 --portal-url 指向哪里,预检都会读取 主网余额,因此在 devnet 上使用暂存门户时,如果钱包有资金,发布也会在开始前被拒绝:

Signer <address> has 0.000000 SOL, but publishing needs at least 0.016000 SOL available

余额确实存在,只是在另一个集群上。请同时传入 --portal-url 和 --rpc-url:

dapp-store \
  --apk-file "$APK" \
  --keypair ~/.config/solana/publisher.json \
  --whats-new "Initial release" \
  --portal-url https://staging.publish.solanamobile.com \
  --rpc-url https://api.devnet.solana.com

CLI 会在启动时调用 dotenv.config(),因此工作目录中的 .env 文件会悄然提供该变量和 API 密钥。 在自己的项目中这样做很方便,但在克隆的项目中却有风险:别人提交的 DAPP_STORE_PORTAL_URL 会将你的 API 密钥发送到他们的主机,而 CLI 对该地址唯一的检查就是它使用了 HTTPS。 从并非由你创建的仓库发布前,请先查看 .env,并显式传入 --portal-url,以覆盖其中设置的值。

门户会根据 APK 的包名判断要更新哪个应用,因此该名称必须与设置期间创建的列表相符。不匹配时, 系统会显示应用不存在,而不是名称错误。在 Expo 项目中,名称来自 expo.android.package; 在纯原生项目中,则来自 android/app/build.gradle 中的 applicationId——请检查项目实际使用的 配置项。

如果你要编写脚本,请传入 --idempotency-key <key>,并在该版本发布的每次重试中重复使用同一个值。 省略它并非中性的默认行为:CLI 会在每次调用时生成一个新的 randomUUID(),因此重试会被视为一次 新的发布,可能导致重复上架。在首次尝试前,将该密钥与发布 ID 一并保存。

恢复失败的发布

一次发布包含多个步骤。失败后是否有内容可供恢复,取决于失败发生的位置:如果在铸造步骤中失败, 系统会回滚,CLI 会给出提示——Rolling back failed publication release / Failed publication release cleaned up。出现该提示后,会话已不复存在,此时应重新发布。若运行 失败后没有清理,通常是会话中途发生网络中断,这时应使用 resume:

dapp-store resume \
  --release-id "$RELEASE_ID" \
  --keypair ~/.config/solana/publisher.json

resume 会像发布操作一样验证 --keypair;如果未提供,则会报错 --keypair is required。 必须在 --release-id 和 --session-id 中恰好选择一个——两者同时提供会直接被拒绝, 尽管 --help 中的用法说明显示它们可以一起使用。

这两个标识都来自原始运行的输出,因此对任何非交互式操作来说,使用 --verbose 很有必要。 注意 --verbose 何时打印这些信息:只有 APK 成功导入后,才会输出发布 ID 和发布会话 ID。 如果运行在导入期间失败,只会打印一个导入会话 ID,而这是另一个标识——--session-id 需要的是发布会话 ID,不接受这个标识。如此早期的失败本来也无从恢复;修复原因后重新发布即可。

发布更新

与首次发布使用相同的命令。必须满足以下三项,否则会失败或悄无声息地没有发布任何新内容:

  • 递增 versionCode——但要在实际管理它的文件中修改。对于裸项目,该文件是 android/app/build.gradle。对于 Expo,它是 app.json 或 app.config.* 中的 expo.android.versionCode;在 Expo 项目中编辑 build.gradle 毫无意义,因为 prebuild 会重新生成该文件。刚搭建的 Expo 应用通常在 app.json 中既没有 expo.version,也没有 expo.android.versionCode——Expo 会回退到 1.0.0 和 1。应在首次发布前添加这两个键,不要等到第二次发布需要提高版本时才发现它们缺失。 如果 cli.appVersionSource 是 "remote",versionCode 完全由 EAS 管理:在构建配置文件中设置 autoIncrement,而不是编辑任何本地文件。Android 根据这个整数对发布版本排序,商店也会用它来判断哪个版本更新。
  • 同时递增 versionName——Expo 项目中是 expo.version,裸项目中是 build.gradle 里的 versionName。CLI 会将两者都作为发布元数据提交,而 versionName 是用户在应用页面上看到的字符串。即使它保持不变也不会被拒绝,因此若更新只更改了 versionCode,发布后显示的仍会是之前的版本。
  • 使用原始密钥库签名。 请参阅上文的限制;无法恢复。

审核

提交内容会进入人工审核,通常需要几个工作日——请查阅文档中的最新时长,不要根据这里引用的数字来安排发布计划。常见的拒绝原因包括 APK 未签名、包名与应用页面不匹配、缺少隐私政策,以及内容违反发布者政策。最后一项应以政策为准,而且政策会变化——请阅读政策,不要自行猜测。

Web 应用

封装 Web 应用需要使用不同的流程:运行 npx solana-mobile@latest webshell init 和 webshell build,将网站放入 Android WebView,并创建签名密钥库。只有当应用确实是需要封装的 Web 应用时,才使用此流程。React Native 应用应遵循上文的原生 APK 流程。

不要使用 Bubblewrap。它的 Trusted Web Activity 在 Chrome 中运行,而 Chrome 的本地网络访问限制会阻止 Mobile Wallet Adapter,或在其前面弹出权限提示。Webshell 会通过 Android 将钱包请求交给钱包应用,无需 Digital Asset Links。页面需要使用 @solana-mobile/wallet-standard-mobile 0.5.1 或更高版本,才能跳过该提示。

从 Bubblewrap 迁移:将现有的 twa-manifest.json 传给 webshell init --manifest。 它会沿用包 ID、版本号和签名密钥,因此现有应用页面会原地更新。导入的 versionCode 会原样复用,因此请传入高于应用页面当前值的 --version-code,否则商店不会接受此次更新。标志参数见 solana-mobile 技能的 CLI 参考。

完整指南:https://docs.solanamobile.com/recipes/general/publishing-a-web-app

参考资料

  • references/signing.md — 密钥库创建、Gradle 签名配置、EAS 构建配置文件,以及仅在发布时出现的失败情况,包括 ProGuard 陷阱

相关技能

  • solana-mobile — 搭建项目、检查工具链、开发构建
  • solana-mobile-wallet — 必须确保钱包集成在发布构建中正常运行

链接

评论

支持 Markdown