featured.svg

當我們用 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 進行最終組裝:

  1. 複製 .so 檔案jniLibs/<arch>/
  2. 編譯 Java/Kotlin 程式碼(若有)為 bytecode
  3. 產生 DEX:將 bytecode 轉換為 Dalvik Executable
  4. 打包 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.apk

Release 簽章(自訂 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 路徑解析

優先順序:

  1. ANDROID_NDK_ROOT 環境變數
  2. ANDROID_NDK_HOME 環境變數
  3. 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 buildx runx doctor
  • 裝置管理x devices 列出所有連接的裝置
  • 持續開發:活躍維護,積極修復問題
  • App Store 發佈:內建發佈到 Google Play 和 App Store 的支援

安裝與使用

# 安裝
cargo install xbuild

# 檢查環境
x doctor

# 建置並部署到裝置
x devices                        # 列出裝置
x build --device adb:<device-id> # 建置並安裝

三階段建置流程

  1. Fetch:下載預編譯的 Rust 標準函式庫
  2. Build:透過 Cargo 編譯 Rust 程式碼
  3. 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

關鍵觀念回顧

  1. APK 是 ZIP:理解目錄結構有助於除錯打包問題
  2. cdylib 必要性:Android 只能載入動態函式庫
  3. 多架構建置:一個 APK 包含所有架構的 .so,安裝時系統自動選擇
  4. Manifest 自動化:Cargo.toml 配置轉換為 Android 設定
  5. NDK 整合:正確設定環境變數是成功建置的關鍵

透過理解這些底層機制,即使遇到建置問題也能快速定位原因並解決。

參考資源