當我們用 Rust 開發 Android 應用時,cargo-apk 是一個自動化整個建置流程的工具——從 Rust 原始碼編譯、產生 AndroidManifest.xml、整合 Gradle、到最後簽章產出 APK。這篇文章深入探討 cargo-apk 的內部運作機制,以及現代替代方案。
什麼是 cargo-apk
cargo-apk 是 rust-mobile 團隊開發的命令列工具,負責:
- 將 Rust cdylib 編譯為多個架構的
.so檔案(ARM64、ARMv7、x86 等) - 從 Cargo.toml 自動產生 AndroidManifest.xml
- 呼叫 aapt/aapt2 處理 Android 資源
- 透過 Gradle 組裝 APK(DEX 編譯 + ZIP 打包)
- 用 keystore 簽章 APK
重要提醒:cargo-apk 在 2024-2025 年已標記為 deprecated,官方推薦改用 xbuild,後者支援跨平台(Android、iOS、Web、桌面)且持續維護。本文仍詳細介紹 cargo-apk 的運作原理,因為理解這些底層機制有助於除錯和選擇合適的建置工具。
1. APK 檔案結構剖析
APK 本質上是一個 ZIP 壓縮檔,包含應用程式執行所需的所有元件。理解 APK 結構是掌握建置流程的第一步。
標準 APK 目錄結構
app.apk (ZIP archive)
├── AndroidManifest.xml # 應用程式中繼資料(二進位 XML)
├── classes.dex # Dalvik Executable(Android Runtime 執行檔)
├── classes2.dex # 額外的 DEX 檔案(若超過大小限制)
├── resources.arsc # 編譯後的資源表(二進位格式)
├── META-INF/ # 簽章和資訊清單
│ ├── MANIFEST.MF # 套件資訊清單
│ ├── CERT.SF # 簽章檔案
│ └── CERT.RSA # 憑證和簽章資料
├── assets/ # 開發者自訂的未編譯資源
│ └── (custom files)
├── res/ # 編譯後的資源(不在 resources.arsc 中)
│ ├── drawable/
│ ├── layout/
│ └── values/
└── lib/ # 平台特定的原生函式庫
├── arm64-v8a/ # ARM 64-bit(現代裝置主流)
│ └── libflashcard_app.so
├── armeabi-v7a/ # ARM 32-bit(舊裝置支援)
│ └── libflashcard_app.so
├── x86/ # Intel 32-bit(模擬器/平板)
│ └── libflashcard_app.so
└── x86_64/ # Intel 64-bit(模擬器)
└── libflashcard_app.so
Rust 為什麼編譯成 .so 檔案?
Android 要求所有原生程式碼編譯為動態連結函式庫(shared object, .so),放在 lib/<ABI>/ 目錄下。Rust 專案必須將 crate-type 設為 ["cdylib"]:
[lib]
crate-type = ["cdylib"]cdylib 產生 C-compatible 的動態函式庫,Android Runtime 可以在執行時載入並呼叫其中的符號(透過 System.loadLibrary())。
2. 建置流程五階段
cargo-apk 的建置流程可以拆解為五個連續的階段。理解每個階段的輸入輸出,有助於除錯建置問題。
Phase 1: Rust 編譯
cargo-apk 針對每個目標架構呼叫 rustc:
# 對於 ARM64
rustc --target aarch64-linux-android \
--crate-type cdylib \
-C linker=aarch64-linux-android-clang \
...
# 對於 ARMv7
rustc --target armv7-linux-androideabi \
--crate-type cdylib \
-C linker=armv7a-linux-androideabi-clang \
...關鍵環節:
- NDK 工具鏈選擇:每個架構使用對應的 clang linker(由
ANDROID_NDK_ROOT提供) - Sysroot 連結:連結到 NDK 的平台 headers 和 libraries
- RUSTFLAGS 設定:加入
-L指定 NDK 函式庫路徑
輸出:
target/aarch64-linux-android/release/libmyapp.so
target/armv7-linux-androideabi/release/libmyapp.so
target/x86_64-linux-android/release/libmyapp.so
Phase 2: AndroidManifest.xml 產生
cargo-apk 從 Cargo.toml 的 [package.metadata.android] 讀取設定,自動產生 manifest:
[package.metadata.android]
package = "com.example.flashcard_app"
min_sdk_version = 21
target_sdk_version = 33
# 權限宣告
permissions = [
"android.permission.INTERNET",
"android.permission.WRITE_EXTERNAL_STORAGE"
]
# 建置目標架構
build_targets = ["aarch64-linux-android", "armv7-linux-androideabi"]
# Activity 主題
activity_theme = "@android:style/Theme.NoTitleBar.Fullscreen"產生的 AndroidManifest.xml 範例:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.flashcard_app">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33"/>
<uses-permission android:name="android.permission.INTERNET"/>
<application>
<activity android:name="android.app.NativeActivity"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
<!-- 指定要載入的原生函式庫名稱 -->
<meta-data android:name="android.app.lib_name"
android:value="flashcard_app"/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>關鍵觀念:
- NativeActivity:Android 提供的類別,自動處理原生程式碼的生命週期
- android.app.lib_name:指定要載入的
.so檔案名稱(不含lib前綴和.so後綴) - Rust 程式碼必須實作
android_main函式作為進入點
Phase 3: 資源處理(aapt/aapt2)
Android Asset Packaging Tool 負責編譯資源檔:
aapt2 compile --dir res/ -o compiled_resources/
aapt2 link --manifest AndroidManifest.xml \
-o base.apk \
compiled_resources/*.flat輸出:
resources.arsc:二進位資源表,包含所有字串、顏色、尺寸定義- 編譯後的 XML 資源檔案
Phase 4: Gradle APK 組裝
cargo-apk 呼叫 Gradle 進行最終組裝:
- 複製 .so 檔案到
jniLibs/<arch>/ - 編譯 Java/Kotlin 程式碼(若有)為 bytecode
- 產生 DEX:將 bytecode 轉換為 Dalvik Executable
- 打包 ZIP:將所有元件組合成 APK
Gradle 的 build.gradle 片段:
android {
compileSdkVersion 33
sourceSets {
main {
jniLibs.srcDirs = ['jniLibs'] // 原生函式庫來源
}
}
}Phase 5: APK 簽章
最後一步是用 keystore 簽章 APK:
Debug 簽章(預設):
apksigner sign --ks ~/.android/debug.keystore \
--ks-pass pass:android \
--ks-key-alias androiddebugkey \
app-debug.apkRelease 簽章(自訂 keystore):
[package.metadata.android]
release_keystore_path = "/path/to/release.jks"
release_keystore_password = "env:KEYSTORE_PASSWORD"
release_keystore_key_alias = "my-key"簽章過程會在 APK 的 META-INF/ 目錄加入三個檔案:
- MANIFEST.MF:列出 APK 中所有檔案及其 SHA-256 雜湊值
- CERT.SF:MANIFEST.MF 的簽章
- CERT.RSA:憑證和 RSA 簽章資料
Android OS 在安裝時會驗證這些簽章,確保 APK 沒有被竄改。
3. Cargo.toml 設定詳解
完整的 [package.metadata.android] 設定範例:
[package.metadata.android]
# 應用程式識別碼
package = "com.example.flashcard_app"
apk_name = "flashcard-app"
# SDK 版本
min_sdk_version = 21 # Android 5.0 Lollipop
target_sdk_version = 33 # Android 13
max_sdk_version = 34
# 建置目標架構
build_targets = [
"aarch64-linux-android", # ARM64(主要)
"armv7-linux-androideabi" # ARMv7(相容性)
]
# 權限宣告
permissions = [
"android.permission.INTERNET",
"android.permission.ACCESS_FINE_LOCATION"
]
# 硬體功能需求
features = ["android.hardware.location"]
# Activity 設定
activity_theme = "@android:style/Theme.NoTitleBar.Fullscreen"
main_activity_attributes = {
"android:screenOrientation" = "portrait"
}
# Intent Filters(深度連結)
[[package.metadata.android.intent_filters]]
actions = ["android.intent.action.VIEW"]
categories = ["android.intent.category.DEFAULT", "android.intent.category.BROWSABLE"]
data = [{ scheme = "https", host = "example.com", path_prefix = "/app" }]
# 除錯設定
strip_symbols = false # 保留符號以供除錯
debug = true
# Release 簽章
release_keystore_path = "/path/to/release.jks"
release_keystore_password = "env:KEYSTORE_PASSWORD"
release_keystore_key_alias = "my-key"
release_keystore_key_password = "env:KEY_PASSWORD"4. NDK 整合機制
cargo-apk 如何找到並使用 Android NDK:
NDK 路徑解析
優先順序:
ANDROID_NDK_ROOT環境變數ANDROID_NDK_HOME環境變數local.properties檔案中的ndk.dir
工具鏈選擇(按架構)
| 架構 | Rust Target | NDK Linker |
|---|---|---|
| ARM 64-bit | aarch64-linux-android |
aarch64-linux-android-clang |
| ARM 32-bit | armv7-linux-androideabi |
armv7a-linux-androideabi-clang |
| Intel 64-bit | x86_64-linux-android |
x86_64-linux-android-clang |
| Intel 32-bit | i686-linux-android |
i686-linux-android-clang |
Sysroot 和 API Level
NDK 為每個 API level 提供不同的 sysroot(系統標頭檔和函式庫):
$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/
├── sysroot/
│ └── usr/
│ ├── include/ # C/C++ 標頭檔
│ └── lib/
│ ├── aarch64-linux-android/21/ # API 21 的 ARM64 函式庫
│ ├── aarch64-linux-android/28/ # API 28 的 ARM64 函式庫
│ └── ...
Rust 編譯時會連結到對應 API level 的 sysroot,確保產生的 .so 與目標 Android 版本相容。
5. 與手動 NDK 整合的比較
| 面向 | cargo-apk | 手動 NDK 整合 |
|---|---|---|
| 設定複雜度 | cargo install cargo-apk |
下載 NDK、設定路徑、撰寫建置腳本 |
| Manifest 產生 | 自動從 Cargo.toml | 手動編寫 XML |
| 多架構建置 | 自動迴圈處理 | 每個架構手動呼叫 |
| APK 組裝 | Gradle 自動整合 | 手動呼叫 aapt + gradle |
| 簽章 | 自動 keystore 處理 | 手動呼叫 apksigner |
| 控制粒度 | 高層自動化 | 完全控制每個步驟 |
| 學習曲線 | 低(單一指令) | 高(理解整個 Android 建置鏈) |
cargo-apk 的核心價值是自動化——一行指令完成從 Rust 到 APK 的所有步驟。但代價是較少的客製化彈性,並且依賴 Gradle(可能增加建置時間)。
6. 目前狀態與替代方案
cargo-apk 的現況
cargo-apk 在 2024 年標記為 deprecated,原因:
- 缺乏跨平台支援(僅限 Android)
- 維護資源有限
- 更現代的工具(xbuild)提供更好的開發體驗
xbuild:現代化的替代方案
xbuild 是 rust-mobile 團隊的新工具,支援多平台:
優勢:
- 跨平台支援:Android、iOS、Windows、Linux、Web
- 統一命令介面:
x build、x run、x doctor - 裝置管理:
x devices列出所有連接的裝置 - 持續開發:活躍維護,積極修復問題
- App Store 發佈:內建發佈到 Google Play 和 App Store 的支援
安裝與使用:
# 安裝
cargo install xbuild
# 檢查環境
x doctor
# 建置並部署到裝置
x devices # 列出裝置
x build --device adb:<device-id> # 建置並安裝三階段建置流程:
- Fetch:下載預編譯的 Rust 標準函式庫
- Build:透過 Cargo 編譯 Rust 程式碼
- Package:產生平台特定套件(APK、iOS bundle 等)
cargo-ndk:函式庫專用工具
cargo-ndk 適合只需要編譯 Rust 函式庫(不需要完整 APK)的專案:
cargo install cargo-ndk
cargo ndk -t arm64-v8a -t armeabi-v7a \
-o ./jniLibs \
build --release輸出為標準的 jniLibs/ 目錄結構,可直接整合到現有的 Android Studio 專案。
工具比較表
| 工具 | 用途 | 平台支援 | 開發狀態 | 適用場景 |
|---|---|---|---|---|
| cargo-apk | 完整 APK 建置 | Android only | Deprecated (2024) | 舊專案維護 |
| xbuild | 跨平台應用建置 | Android/iOS/Web/Desktop | 活躍開發 | 新專案首選 |
| cargo-ndk | NDK 函式庫編譯 | Android (library) | 活躍開發 | 整合到現有 Android 專案 |
7. 踩過的坑與解決方案
實際使用 cargo-apk 時常見的問題:
問題 1:NDK 找不到
Error: Failed to read source.properties: Os { code: 2, kind: NotFound }
原因:cargo-apk 找不到 NDK 安裝路徑。
解法:
# 設定環境變數(Windows PowerShell)
[Environment]::SetEnvironmentVariable("ANDROID_NDK_ROOT", "C:\Users\p47ts\scoop\apps\android-clt\current\ndk\29.0.14206865", "User")
[Environment]::SetEnvironmentVariable("ANDROID_HOME", "C:\Users\p47ts\scoop\apps\android-clt\current", "User")
# 或在當前 shell(bash)
export ANDROID_NDK_ROOT="/path/to/ndk/29.0.14206865"
export ANDROID_HOME="/path/to/android-sdk"問題 2:錯誤的架構
APK 安裝後閃退,logcat 顯示:
E/linker: library "libmyapp.so" not found
原因:裝置的 ABI 與 build_targets 不符(例如裝置是 ARM64,但只編譯了 ARMv7)。
解法:
確認裝置 ABI:
adb shell getprop ro.product.cpu.abi
# 輸出:arm64-v8a修改 Cargo.toml 加入對應架構:
[package.metadata.android]
build_targets = ["aarch64-linux-android"] # 對應 arm64-v8a問題 3:APK 檔案過大
Release APK 超過 100MB,包含大量除錯符號。
解法:
啟用符號剝離:
[package.metadata.android]
strip_symbols = true # 移除除錯符號或在 Cargo.toml 設定 release profile:
[profile.release]
strip = true # 剝離符號
opt-level = "z" # 最小化檔案大小
lto = true # Link-Time Optimization問題 4:Feature flag 統一導致桌面建置失敗
使用 target-conditional 依賴時,Cargo 仍會統一 feature:
# ❌ 錯誤:桌面建置也會拉入 Android 依賴
[target.'cfg(target_os = "android")'.dependencies]
slint = { version = "1.9", features = ["backend-android-activity-06"] }解法:
改用 feature flag:
[features]
android = ["slint/backend-android-activity-06"]
[dependencies]
slint = "1.9"建置時明確啟用:
cargo apk build --features android --target aarch64-linux-android --lib問題 5:Windows 上的 PDB 檔名衝突
warning: output filename collision.
The bin target `flashcard-app` has the same output filename as the lib target `flashcard_app`.
Colliding filename is: flashcard_app.pdb
原因:crate-type = ["cdylib", "lib"] 搭配同名的 [[bin]] 在 Windows 產生相同的 PDB 除錯檔案。
解法:
讓 bin 名稱與 package 名稱不同:
[package]
name = "flashcard-app"
[[bin]]
name = "flashcard" # 不同於 package name
path = "src/main.rs"8. 完整建置範例
從零開始建置 Android APK 的完整流程:
# 1. 建立專案
cargo new --lib my-android-app
cd my-android-app
# 2. 設定 Cargo.toml
cat >> Cargo.toml << 'EOF'
[lib]
crate-type = ["cdylib"]
[package.metadata.android]
package = "com.example.myapp"
min_sdk_version = 21
target_sdk_version = 33
build_targets = ["aarch64-linux-android", "armv7-linux-androideabi"]
EOF
# 3. 撰寫 Rust 入口點(src/lib.rs)
cat > src/lib.rs << 'EOF'
#[cfg(target_os = "android")]
#[no_mangle]
fn android_main(app: android_activity::AndroidApp) {
// 應用程式邏輯
}
EOF
# 4. 設定環境變數(依實際路徑調整)
export ANDROID_NDK_ROOT="/path/to/ndk/29.0.14206865"
export ANDROID_HOME="/path/to/android-sdk"
# 5. 安裝 Rust Android target
rustup target add aarch64-linux-android armv7-linux-androideabi
# 6. 安裝 cargo-apk
cargo install cargo-apk
# 7. 建置 APK
cargo apk build --release
# 內部流程:
# [Phase 1] rustc --target aarch64-linux-android ...
# → target/aarch64-linux-android/release/libmyapp.so
# [Phase 2] Generate AndroidManifest.xml from Cargo.toml
# [Phase 3] aapt2 compile resources
# [Phase 4] Gradle: package DEX + .so → APK
# [Phase 5] apksigner sign with release.jks
#
# 輸出:target/release/apk/my-android-app.apk
# 8. 安裝到裝置
cargo apk run --release建置完成後,APK 位置:
target/
└── release/
└── apk/
└── my-android-app.apk # 已簽章的 APK
檢查 APK 內容:
unzip -l target/release/apk/my-android-app.apk
# 輸出:
# lib/arm64-v8a/libmyapp.so
# lib/armeabi-v7a/libmyapp.so
# AndroidManifest.xml
# classes.dex
# META-INF/MANIFEST.MF
# ...總結
從 Rust 程式碼到可安裝的 Android APK,cargo-apk 自動化了複雜的建置鏈:
| 階段 | 輸入 | 輸出 | 關鍵技術 |
|---|---|---|---|
| 1. Rust 編譯 | src/*.rs + Cargo.toml | libmyapp.so (多架構) | rustc + NDK toolchain |
| 2. Manifest 產生 | [package.metadata.android] | AndroidManifest.xml | NativeActivity 設定 |
| 3. 資源處理 | res/, assets/ | resources.arsc | aapt/aapt2 |
| 4. APK 組裝 | .so + DEX + resources | unsigned APK (ZIP) | Gradle |
| 5. 簽章 | APK + keystore | signed APK | apksigner |
何時使用哪個工具?
- 新專案:優先選擇 xbuild(跨平台、活躍開發、統一工具鏈)
- 整合現有 Android 專案:使用 cargo-ndk(只編譯 .so,由 Android Studio 處理其餘)
- 舊專案維護:可繼續使用 cargo-apk,但建議遷移到 xbuild
關鍵觀念回顧:
- APK 是 ZIP:理解目錄結構有助於除錯打包問題
- cdylib 必要性:Android 只能載入動態函式庫
- 多架構建置:一個 APK 包含所有架構的 .so,安裝時系統自動選擇
- Manifest 自動化:Cargo.toml 配置轉換為 Android 設定
- NDK 整合:正確設定環境變數是成功建置的關鍵
透過理解這些底層機制,即使遇到建置問題也能快速定位原因並解決。
參考資源
- cargo-apk GitHub - 官方原始碼與文件
- xbuild GitHub - 現代化的跨平台建置工具
- cargo-ndk GitHub - NDK 函式庫編譯工具
- Android NDK 文件 - 官方 NDK 開發指南
- APK 檔案格式 - APK 結構詳解
- Android Gradle Build 流程 - Gradle 建置系統文件