GeoLibre Android 原生应用构建与发布指南:基于 Tauri v2 Mobile 的 APK 打包、签名与 Google Play 上架全流程
【免费下载链接】GeoLibreA lightweight, cloud-native GIS platform for visualizing, exploring, and analyzing geospatial data. It runs in the web browser, on the desktop, on mobile, and inside Jupyter notebooks.项目地址: https://gitcode.com/GitHub_Trending/ge/GeoLibre
导读
本文是 GeoLibre 在 Android 平台上的完整开发与发布手册,覆盖从工具链搭建、APK/AAB 构建、16 KB 页面对齐、签名验签、CI 自动化、真机/模拟器安装测试,到 Google Play Console 上架与geo:深链接入的全部环节。读完本文,你将掌握:为什么 GeoLibre 无需独立的 Android 代码库即可成为原生应用、如何在本地复现其官方签名构建流程、如何规避 16 KB 内存页对齐与包名覆写等"坑",以及如何为分叉版本或你自己的 GIS 应用跑通 Play 商店发布管线。
架构基础:同一份 React 代码库,Tauri v2 Mobile 原生打包
GeoLibre 的 Android 版本并非一套独立应用,而是与桌面端共享同一份 React 代码库,通过Tauri v2 mobile打包为原生 Android 应用。WebView 界面被打包进 APK 内部,因此应用外壳支持离线运行;地图瓦片与较重的引擎则按需拉取(与桌面构建行为一致)。
从仓库源码可以看到这一架构的直接落地:
- apps/geolibre-desktop/src-tauri/tauri.android.conf.json 将 Android 构建的
productName设为GeoLibre(桌面构建名为 "GeoLibre Desktop"),identifier覆写为org.geolibre.app,并通过清空bundle.resources把 Python 后端从 Android 包里剔除; gen/android目录是npx tauri android init生成的(git-ignored),按需再生成,Android 端无需手写 Kotlin/Gradle 业务代码;- Android 包名的覆写放在
tauri.android.conf.json而非全局tauri.conf.json,是有意为之:identifier是全平台共享的,同时决定 macOS bundle ID、Linux AppStream ID 与 WebView 数据目录。若全局改动,会遗弃既有桌面用户的设置,并破坏仍以org.geolibre.desktop为键的 Linux/COPR/Homebrew 打包(详见 docs/android.md)。
安装途径:Google Play 与侧载 APK
GeoLibre 在 Google Play 上以org.geolibre.app发布,Play 构建已签名并自动更新。若偏好侧载,每个 release 都会附带按 ABI 拆分的签名 APK,可参考 docs/downloads.md#android-installation 获取下载说明。本文其余部分面向自行构建应用的开发者。
Android 与桌面的能力差异:哪些功能在移动端被隐藏
Android 构建自带完整的地图工作区、添加数据(Add Data)、Whitebox 地理处理工具箱(1000+ 工具,通过 WebAssembly 在浏览器内运行)、矢量工具(Turf.js / 基于 Pyodide 的浏览器内 GeoPandas)、SQL 工作区(DuckDB-WASM 与浏览器内 PGlite/PostGIS 引擎)、Python 控制台(Pyodide)、地理编码、统计、AI 助手、故事地图与插件。
依赖本地桌面进程的工具在移动端被隐藏,因为 Android 上没有 Python sidecar 或本地辅助二进制:
- 处理 → GeoLibre Toolbox →栅格(Raster)、格式转换(Conversion)、AI 分割(AI Segmentation)(都需要 Python sidecar);
- 添加数据 →PostgreSQL(由本地 Martin 瓦片服务器提供)。
这些工具通过基于 user-agent 的isMobile()检查进行门控,因此 Add Data 菜单与图层面板(Layer panel)的 add-data 分组中从不出现这些入口,其余功能全部在客户端运行。
源码视角:isMobile()与isDesktopRuntime()
门控逻辑集中在 apps/geolibre-desktop/src/lib/is-mobile.ts:
isMobile()用/Android|iPhone|iPad|iPod/i正则匹配 UA,并额外通过isIpadDesktopUserAgent(userAgent, maxTouchPoints)处理 iPadOS 13+ 伪装桌面 "Macintosh" UA 的情况(多触点能力区分真实 Mac);isAndroid()用/Android/i做更窄的检查,用于 Android 行为不同于 iOS 的平台 API(如原生文档选择器的 MIME 过滤);isDesktopRuntime()定义为isTauri() && !isMobile(...)——其中isTauri()仅检查"__TAURI_INTERNALS__" in window(见 apps/geolibre-desktop/src/lib/is-tauri.ts)。
为什么不能只用isTauri()门控?因为 Tauri 在打包的 Android/iOS 应用中同样为 true。若用isTauri()单独门控,平板/手机用户会一路走到 sidecar 连接并报出原始的 "could not connect to the sidecar at 127.0.0.1:8765" 错误——这正是 GeoLibre#2091 在 iPadOS 上报的问题。isDesktopRuntime()才是"桌面 Tauri 外壳"的正确判定。
PostgreSQL 有一个isMobile()检查覆盖不到的入口:Browser 面板为发现目的在所有平台保留Databases(数据库)分区,其 + 仍会打开 PostgreSQL 对话框。该对话框用isDesktopRuntime()(isTauri() && !isMobile())门控,而非单独isTauri(),因此在 Android 上显示"requires GeoLibre Desktop"提示并禁用 Connect,而不是去调用一个不可能存在的 sidecar(GeoLibre#2091)。具体实现见 apps/geolibre-desktop/src/components/layout/add-data/sources/PostgresSource.tsx:组件用useMemo(() => isDesktopRuntime(), [])求值一次(UA 在会话内稳定),非桌面运行时渲染desktopOnlyNotice提示,且 Connect 按钮disabled={source.isSubmitting || !desktopRuntime};handleConnectEditable与handleConnectPostgres里也都有if (!desktopRuntime) throw new Error(...)的防御性二次检查。
工具链搭建(一次性)
需要 Android SDK + NDK、一个 JDK(17 或 21——更新的 JDK 可能破坏 Android Gradle Plugin),以及 Rust 的 Android 目标。最干净、无需 sudo 的布局是把所有内容放在用户可写的~/Android/Sdk下。
# 1. JDK 17/21(或复用 Android Studio 自带的 JBR,位于 /opt/android-studio/jbr) export JAVA_HOME=/path/to/jdk-21 # 2. Android SDK 组件(sdkmanager 随 Android Studio cmdline-tools 提供) export ANDROID_HOME="$HOME/Android/Sdk" yes | sdkmanager --sdk_root="$ANDROID_HOME" --licenses sdkmanager --sdk_root="$ANDROID_HOME" \ "platform-tools" "platforms;android-36" \ "build-tools;36.0.0" "ndk;27.3.13750724" export NDK_HOME="$ANDROID_HOME/ndk/27.3.13750724" # Tauri 需要 NDK_HOME # 3. Rust + 四个 Android 目标(没有 rustup 先安装) rustup target add aarch64-linux-android armv7-linux-androideabi \ i686-linux-android x86_64-linux-android要点:
- NDK r27 (LTS)是 Tauri v2 支持的主线,把这四个
export写进 shell profile,每个会话都有; - API 36(Android 16)而不是 34:Tauri v2.11 Android 模板生成
compileSdk = 36/targetSdk = 36,Gradle 需要安装匹配的 platform;它同时也是 Google Play 的最低门槛——从 2026-08-31 起,新应用与更新必须 target API 36 才会被接受。
仓库 CI 在 .github/workflows/android.yml 中与此完全一致:ANDROID_PLATFORM: "platforms;android-36"、ANDROID_BUILD_TOOLS: "build-tools;36.0.0"、ANDROID_NDK_VERSION: "27.3.13750724",并显式注释"Keep these in sync with docs/android.md and the local setup"。
16 KB 内存页对齐
Google Play 会拒绝 target Android 15+ 且原生库未按16 KB 内存页对齐的应用,且此类库在 16 KB 设备上会加载失败。NDK r28+ 默认对齐;r27 不会,因此需显式传参。在 Android 构建前导出(CI 在 workflow 级别设置,见 .github/workflows/android.yml):
export RUSTFLAGS="-C link-arg=-Wl,-z,max-page-size=16384 -C link-arg=-Wl,-z,common-page-size=16384"必须是
RUSTFLAGS环境变量。把同样的 flag 写进.cargo/config.toml的target.<triple>.rustflags是不生效的:Tauri CLI 在调用 cargo 构建 Android 时会自己设置RUSTFLAGS,而环境变量RUSTFLAGS会整体覆盖配置文件。config 文件形式会被静默忽略——它照常解析、照常构建,最后交付的是 4 KB 对齐的库,直接被 Play 拒绝。Tauri 会向继承的值追加,所以导出环境变量有效。
在构建好的 APK 上验证结果——检查 Play 实际收到的字节:
unzip -o -q app-arm64-release-unsigned.apk 'lib/*/*.so' -d /tmp/apkcheck "$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-objdump" -p \ /tmp/apkcheck/lib/*/*.so | awk '$1 == "LOAD" { print $NF }' | sort -u # 每个值必须是 2**14 或更大;出现 2**12 说明 flag 未生效用llvm-objdump -p而非readelf -l:readelf 会把每个 LOAD 跨两行换行,很容易解析到错误的列、把地址当成对齐值。CI 对打包的每个.so运行同一检查,一旦回归即失败。
从 .github/workflows/android.yml 可看到这条检查链的完整形态:CI 用unzip解出 APK 的lib/*/*.so与 AAB 的base/lib/*/*.so,逐个.so用llvm-objdump -p提取 LOAD 段对齐指数,要求>= 14(即 2**14=16384);若某个归档没有任何原生库(unzip 退出码 11)也计为问题,避免"空检查通过"。同时签名步骤用zipalign -P 16(而非小写-p,后者只保证 4 KB,且 16 KB 要求有两条独立轴线:ELF 段对齐 + .so 在 APK zip 内的字节偏移,前者靠 RUSTFLAGS,后者靠 zipalign),并在签名后再用zipalign -c -P 16复核最终产物。
构建
cd apps/geolibre-desktop npx tauri android init # 生成 src-tauri/gen/android(一次性) npx tauri android build --apk --split-per-abi # release APK,每 ABI 一个(约 40 MB 每个) npx tauri android build --aab # 面向 Google Play 的通用 AABgen/android是生成的(git-ignored),按需重新生成;构建release而非
--debug:剥离符号、体积优化的 Cargo profile 使每个 APK 约 40 MB;debug 构建约 200 MB(未剥离的.so带 debuginfo);--split-per-abi每个架构产出一个 APK,而不是单个约 150 MB 的通用 APK。真机安装arm64版;输出路径:
src-tauri/gen/android/app/build/outputs/apk/<abi>/release/app-<abi>-release-unsigned.apk, 其中<abi>是 Tauri 的短名——arm64、arm、x86、x86_64——而不是 APK 内部lib/下出现的 Android ABI 目录名(arm64-v8a、armeabi-v7a)。侧载 / GitHub release 路径:按 ABI 拆分的APK;
Google Play 路径:通用AAB(Play 会基于它生成按设备拆分的产物)。不要给 AAB 加
--split-per-abi——Play 要的是那一个 bundle。
应用在 Android 上命名为GeoLibre(桌面构建叫 "GeoLibre Desktop"),包名org.geolibre.app,两者都在src-tauri/tauri.android.conf.json中设置,该文件同时把 Python 后端从 Android 包中剔除。
签名
release APK 默认未签名。要安装需先签名(测试用 debug 密钥即可,分发必须用真实密钥):
BT="$ANDROID_HOME/build-tools/36.0.0" KS="$HOME/.android/debug.keystore" # Android 工具链自动创建;也可自建 # -P 16,不是 -p:-p 只保证 4 KB,而 Play 要求 .so 在 zip 内落在 16 KB 边界上。与 CI 使用的 flag 相同。 "$BT/zipalign" -P 16 -f 4 app-arm64-release-unsigned.apk aligned.apk "$BT/apksigner" sign --ks "$KS" --ks-pass pass:android \ --ks-key-alias androiddebugkey --key-pass pass:android \ --out geolibre-arm64.apk aligned.apk "$BT/apksigner" verify geolibre-arm64.apk示例签名的是 arm64 APK。按需替换 ABI——app-x86_64-release-unsigned.apk→geolibre-x86_64.apk,armeabi-v7a/x86同理。下文模拟器章节安装的geolibre-x86_64.apk就是同一流程把arm64换成x86_64。
用于正式上传/发布的密钥:
keytool -genkeypair -v -keystore upload.jks -alias upload -keyalg RSA \ -keysize 2048 -validity 10000持续集成(CI)
.github/workflows/android.yml 在每个已发布的 GitHub release 上(也可通过 "Run workflow" 按钮按需触发)构建已签名、按 ABI 拆分的 release APK,并上传为geolibre-android-release-apks工件。在已发布的 release 上,APK还会作为可下载资产附加到该 release——但仅当它们带有真实 release 签名时;debug 签名的 APK 只保留为 CI 工件,运行日志会输出警告说明原因。当以下仓库 secrets 已设置时用你的 release keystore 签名,否则回退到一次性 debug 密钥以保证工件仍可安装用于测试:
ANDROID_KEYSTORE_BASE64—base64 -w0 upload.jksANDROID_KEYSTORE_PASSWORDANDROID_KEY_ALIASANDROID_KEY_PASSWORD
CI 还会构建通用AAB并作为独立的geolibre-android-play-aab工件上传——但仅在具备真实 release keystore 的运行时(Play 拒绝 debug 签名的 bundle)。没有 keystore 时 AAB 构建被整体跳过,而不是"构建后丢弃"。在已发布的 release 上,geolibre-android.aab会与 APK 一起附加到 release,使某个 tag 提交的确切 bundle 在 CI 工件过期后仍可回溯。
侧载请取
.apk。.aab是 Google Play 上传格式,无法安装到设备。
CI 工作流还有一些值得借鉴的工程细节:
- 包名三重校验:生成 Gradle 工程后先 grep
applicationId;APK 构建后用aapt2 dump packagename校验合并后的 manifest;AAB 用bundletool dump manifest --xpath=/manifest/@package校验(AAB 的 manifest 是 protobuf 编码,aapt2 读不了)。原因注释写得很清楚:Play 在首次上传时就把包名"烧死",不可更改或复用,所以宁可 fail-fast 也不假设。 - 16 KB 对齐检查覆盖 APK 与 AAB 内所有
.so,并明确警告不要用find gen/android -path '*/release/*'(Gradle 输出目录是arm64Release等大写 R,会匹配为空导致检查"永不运行")。 - 签名 secrets 处理:密钥/密码通过
pass:file:传递避免出现在进程参数与日志;keystore 解码到$RUNNER_TEMP后通过trap ... EXIT清理,防止set -e中途失败留下明文密码文件。 - 并发组隔离:release 运行与手动运行分组隔离且 release 运行不可取消(它是该 tag release 资产的唯一生产者),手动运行之间则允许互相抢占。
安装 / 测试
真机
- 开启开发者选项(连点版本号 7 次)与USB 调试;
- 侧载已签名 APK:
adb install -r geolibre-arm64.apk或把 APK 复制到手机点击安装(允许"安装未知应用")。
需要热重载的实时开发:连接设备后运行npm run tauri android dev。
模拟器
sdkmanager --sdk_root="$ANDROID_HOME" \ "emulator" "system-images;android-36;google_apis_playstore;x86_64" avdmanager create avd -n geolibre \ -k "system-images;android-36;google_apis_playstore;x86_64" -d pixel_7 emulator -avd geolibre # x86_64 APK 匹配 x86_64 系统镜像——模拟器能翻译 arm64 构建,但慢得多, # 且不会真正运行到 x86_64 库。 adb install -r geolibre-x86_64.apk若之后用不同签名密钥重建,先卸载旧副本(
adb uninstall org.geolibre.app)——Android 会拒绝签名变更的更新。这在侧载 APK 与 Play 构建之间切换时同样适用:Play App Signing 用 Google 的密钥重新签名,两者不可升级兼容。
发布到 Google Play
GeoLibre 已作为org.geolibre.app上线;本节记录上架流程,供参考及发布分叉者使用。只有第 3 步在每次发布时重复:用 upload key 构建 AAB 并递增versionCode。构建侧由上文 CI 工作流覆盖;其余是 Play Console 的一次性上架流程。
- 开发者账号($25,一次性)。尽量注册为组织而非个人账号:2023-11-13 之后创建的个人账号必须在申请生产权限前运行封闭测试,12 名主动测试者持续 14 天。组织账号豁免。
- Play App Signing。上传
upload.jks作为upload密钥;Google 持有实际的应用签名密钥并为每个 bundle 重新签名。仓库的ANDROID_KEYSTORE_*secrets 就是这个 upload key——务必备份 keystore,丢失需要走 Play 支持重置。 - 上传 AAB:来自
geolibre-android-play-aabCI 工件,或在该工件过期后使用 release 的geolibre-android.aab资产。versionCode由tauri.conf.json中的版本派生,每次上传必须递增。 - 商店素材:512×512 图标、一张1024×500 功能图,以及至少两张手机截图。再加 7 英寸与 10 英寸平板截图——Play 会降低没有平板截图的应用的排名,而 GIS 工作区确实适合平板。
- 隐私政策 URL——指向已发布的 privacy policy。
- 数据安全表单。如实声明每个网络目的地:地理编码、AI 助手、底图/瓦片抓取,以及 Earth Engine 的 Google OAuth。注明哪些是传输而非收集——GeoLibre 不运营保留用户数据的后端,但表单按用途询问。
- 内容分级问卷与目标受众。
每次更新都要牢记下文已知限制:Android 上仍有若干 Add Data 路径处于惰性状态,用户点了一下没反应就是一星差评,所以要在移动端像 sidecar 工具那样用isMobile()把它们门控起来。
已知限制 / 后续工作
- 本地文件源(MBTiles、本地栅格、项目文件)假设真实文件系统路径;Android 作用域存储返回的是 content URI,这些流程需要适配才能原生工作。
- 下载离线区域(Download Offline Area)工具依赖 service worker,而 Tauri 构建(桌面与 Android)不使用它——这是 PWA 功能。原生离线底图缓存(打包/下载的 MBTiles/PMTiles)是未来的增强方向。
- Earth Engine OAuth使用桌面回环/多窗口流程;移动端 deep-link 重定向是后续工作。
从其他应用打开位置:geo:深链
GeoLibre 处理 AndroidACTION_VIEWintent 的geo:scheme,无论是冷启动应用还是应用已处于打开状态。支持的格式包括:
geo:40.7128,-74.006geo:40.7128,-74.006?z=12geo:0,0?q=40.7128,-74.006(New%20York)&z=12
坐标采用纬度、经度顺序。数字形式的q坐标会覆盖 URI 中的占位坐标;纯地址查询不支持。直接 URI 中可选的第三个坐标是海拔(米),会经过校验但在定位地图时被忽略。缩放默认 14,必须在 0 到 24 之间。无效位置会被忽略。收到的位置只会移动地图,不会替换当前项目的图层。
Tauri 的 deep-link 插件从tauri.android.conf.json生成 Android intent filter,因此 CI 重新生成gen/android时它同样生效。要在已连接设备上同时验证冷启动与运行中的应用,运行两次下面命令(两次之间平移一下地图):
adb shell am start -a android.intent.action.VIEW -d 'geo:0,0?q=40.7128,-74.006&z=12' org.geolibre.app深链的源码实现
- apps/geolibre-desktop/src-tauri/tauri.android.conf.json 中
plugins.deep-link.mobile声明了"scheme": ["geo"](appLink: false),这是 Android intent filter 的唯一来源; - 前端接入点在 apps/geolibre-desktop/src/lib/native-coordinate-open.ts:
initializeNativeCoordinateOpen()先isTauri()短路,再动态 import@tauri-apps/plugin-deep-link,用onOpenUrl订阅应用生命周期内的 URL,同时用getCurrent()读取冷启动 URI;accept()把 URL 列表经coordinateTargetFromGeoUri解析为坐标目标——startup阶段存入initialTarget,之后通过useAppStore.getState().setMapView(location)移动实时地图。finishNativeCoordinateStartup()在启动播种地图后调用,把后续 intent 切换到"移动实时地图"模式; - URI 解析在 apps/geolibre-desktop/src/lib/coordinate-url.ts,其
if (!/^geo:/i.test(uri)) return null;与文档的"无效位置被忽略"对应。
Web 链接可用https://geolibre.app/?lat=40.7128&lon=-74.006&zoom=12或紧凑形式https://geolibre.app/?12/40.7128/-74.006打开同一位置。仅坐标的启动优先于已保存的启动设置并跳过引导流程。与坐标参数组合时,显式的项目或数据链接保留其既有相机/加载行为。
参考资料
- 官方 Android 文档:docs/android.md;安装下载见 docs/downloads.md#android-installation;隐私政策见 docs/privacy.md
- Android 平台配置:apps/geolibre-desktop/src-tauri/tauri.android.conf.json
- 平台判定实现:apps/geolibre-desktop/src/lib/is-mobile.ts、apps/geolibre-desktop/src/lib/is-tauri.ts
- PostgreSQL 桌面运行门控:apps/geolibre-desktop/src/components/layout/add-data/sources/PostgresSource.tsx
- 深链接入与解析:apps/geolibre-desktop/src/lib/native-coordinate-open.ts、apps/geolibre-desktop/src/lib/coordinate-url.ts
- Android CI 工作流:.github/workflows/android.yml
【免费下载链接】GeoLibreA lightweight, cloud-native GIS platform for visualizing, exploring, and analyzing geospatial data. It runs in the web browser, on the desktop, on mobile, and inside Jupyter notebooks.项目地址: https://gitcode.com/GitHub_Trending/ge/GeoLibre
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考