cargo_zng/res/built_in/
apk.rs1use std::process::Command;
2
3use super::*;
4
5const APK_HELP: &str = r#"
6Build an Android APK from a staging directory
7
8The expected file system layout:
9
10| apk/
11| ├── lib/
12| | └── arm64-v8a
13| | └── my-app.so
14| ├── assets/
15| | └── res
16| | └── zng-res.txt
17| ├── res/
18| | └── android-res
19| └── AndroidManifest.xml
20| my-app.zr-apk
21
22Both 'apk/' and 'my-app.zr-apk' will be replaced with the built my-app.apk
23
24Expected .zr-apk file content:
25
26| # Relative path to the staging directory. If not set uses ./apk if it exists
27| # or the parent dir .. if it is named something.apk
28| apk-dir = ./apk
29|
30| # Sign using the debug key. Note that if ZR_APK_KEYSTORE or ZR_APK_KEY_ALIAS are not
31| # set the APK is also signed using the debug key.
32| debug = true
33|
34| # Don't sign and don't zipalign the APK. This outputs an incomplete package that
35| # cannot be installed, but can be modified such as custom linking and signing.
36| raw = true
37|
38| # Don't tar assets. By default `assets/res` are packed as `assets/res.tar`
39| # for use with `android_install_res`.
40| tar-assets-res = false
41
42APK signing is configured using these environment variables:
43
44ZR_APK_KEYSTORE - path to the private .keystore file
45ZR_APK_KEYSTORE_PASS - keystore file password
46ZR_APK_KEY_ALIAS - key name in the keystore
47ZR_APK_KEY_PASS - key password
48"#;
49pub(super) fn apk() {
50 help(APK_HELP);
51 if std::env::var(ZR_FINAL).is_err() {
52 println!("zng-res::on-final=");
53 return;
54 }
55
56 let mut apk_dir = String::new();
58 let mut debug = false;
59 let mut raw = false;
60 let mut tar_assets = true;
61 for line in read_lines(&path(ZR_REQUEST)) {
62 let (ln, line) = line.unwrap_or_else(|e| fatal!("error reading .zr-apk request, {e}"));
63 if let Some((key, value)) = line.split_once('=') {
64 let key = key.trim();
65 let value = value.trim();
66
67 let bool_value = || match value {
68 "true" => true,
69 "false" => false,
70 _ => {
71 error!("unexpected value, line {ln}\n {line}");
72 false
73 }
74 };
75 match key {
76 "apk-dir" => apk_dir = value.to_owned(),
77 "debug" => debug = bool_value(),
78 "raw" => raw = bool_value(),
79 "tar-assets" => tar_assets = bool_value(),
80 _ => error!("unknown key, line {ln}\n {line}"),
81 }
82 } else {
83 error!("syntax error, line {ln}\n{line}");
84 }
85 }
86 let mut keystore = PathBuf::from(env::var("ZR_APK_KEYSTORE").unwrap_or_default());
87 let mut keystore_pass = env::var("ZR_APK_KEYSTORE_PASS").unwrap_or_default();
88 let mut key_alias = env::var("ZR_APK_KEY_ALIAS").unwrap_or_default();
89 let mut key_pass = env::var("ZR_APK_KEY_PASS").unwrap_or_default();
90 if keystore.as_os_str().is_empty() || key_alias.is_empty() {
91 debug = true;
92 }
93
94 let mut apk_folder = path(ZR_TARGET_DD);
95 let output_file;
96 if apk_dir.is_empty() {
97 let apk = apk_folder.join("apk");
98 if apk.exists() {
99 apk_folder = apk;
100 output_file = path(ZR_TARGET).with_extension("apk");
101 } else if apk_folder.extension().map(|e| e.eq_ignore_ascii_case("apk")).unwrap_or(false) {
102 output_file = apk_folder.clone();
103 } else {
104 fatal!("missing ./apk")
105 }
106 } else {
107 apk_folder = apk_folder.join(apk_dir);
108 if !apk_folder.is_dir() {
109 fatal!("{} not found or not a directory", apk_folder.display());
110 }
111 output_file = path(ZR_TARGET).with_extension("apk");
112 }
113 let apk_folder = apk_folder;
114
115 let android_home = match env::var("ANDROID_HOME") {
117 Ok(h) if !h.is_empty() => h,
118 _ => fatal!("please set ANDROID_HOME to the android-sdk dir"),
119 };
120 let build_tools = Path::new(&android_home).join("build-tools/");
121 let mut best_build = None;
122 let mut best_version = semver::Version::new(0, 0, 0);
123
124 #[cfg(not(windows))]
125 const AAPT2_NAME: &str = "aapt2";
126 #[cfg(windows)]
127 const AAPT2_NAME: &str = "aapt2.exe";
128
129 for dir in fs::read_dir(build_tools).unwrap_or_else(|e| fatal!("cannot read $ANDROID_HOME/build-tools/, {e}")) {
130 let dir = dir
131 .unwrap_or_else(|e| fatal!("cannot read $ANDROID_HOME/build-tools/ entry, {e}"))
132 .path();
133
134 if let Some(ver) = dir
135 .file_name()
136 .and_then(|f| f.to_str())
137 .and_then(|f| semver::Version::parse(f).ok())
138 && ver > best_version
139 && dir.join(AAPT2_NAME).exists()
140 {
141 best_build = Some(dir);
142 best_version = ver;
143 }
144 }
145 let build_tools = match best_build {
146 Some(p) => p,
147 None => fatal!("cannot find $ANDROID_HOME/build-tools/<version>/{AAPT2_NAME}"),
148 };
149 let aapt2_path = build_tools.join(AAPT2_NAME);
150
151 let temp_dir = apk_folder.with_extension("apk.tmp");
153 let _ = fs::remove_dir_all(&temp_dir);
154 fs::create_dir(&temp_dir).unwrap_or_else(|e| fatal!("cannot create {}, {e}", temp_dir.display()));
155
156 let assets = apk_folder.join("assets");
158 let assets_res = assets.join("res");
159 if tar_assets && assets_res.exists() {
160 let tar_path = assets.join("res.tar");
161 let r = Command::new("tar")
162 .arg("-cf")
163 .arg(&tar_path)
164 .arg("res")
165 .current_dir(&assets)
166 .status();
167 match r {
168 Ok(s) => {
169 if !s.success() {
170 fatal!("tar failed")
171 }
172 }
173 Err(e) => fatal!("cannot run 'tar', {e}"),
174 }
175 if let Err(e) = fs::remove_dir_all(&assets_res) {
176 fatal!("failed tar-assets-res cleanup, {e}")
177 }
178 }
179
180 let compiled_res = temp_dir.join("compiled_res.zip");
182 let res = apk_folder.join("res");
183 if res.exists() {
184 let mut aapt2 = Command::new(&aapt2_path);
185 aapt2.arg("compile").arg("-o").arg(&compiled_res).arg("--dir").arg(res);
186
187 if aapt2.status().map(|s| !s.success()).unwrap_or(true) {
188 fatal!("resources build failed");
189 }
190 }
191
192 let manifest_path = apk_folder.join("AndroidManifest.xml");
193 let manifest = fs::read_to_string(&manifest_path).unwrap_or_else(|e| fatal!("cannot read AndroidManifest.xml, {e}"));
194 let manifest: AndroidManifest = quick_xml::de::from_str(&manifest).unwrap_or_else(|e| fatal!("error parsing AndroidManifest.xml, {e}"));
195
196 let platforms = Path::new(&android_home).join("platforms");
198 let mut best_platform = None;
199 let mut best_version = 0;
200 for dir in fs::read_dir(platforms).unwrap_or_else(|e| fatal!("cannot read $ANDROID_HOME/platforms/, {e}")) {
201 let dir = dir
202 .unwrap_or_else(|e| fatal!("cannot read $ANDROID_HOME/platforms/ entry, {e}"))
203 .path();
204
205 if let Some(ver) = dir
206 .file_name()
207 .and_then(|f| f.to_str())
208 .and_then(|f| f.strip_prefix("android-"))
209 .and_then(|f| f.parse().ok())
210 && manifest.uses_sdk.matches(ver)
211 && ver > best_version
212 && dir.join("android.jar").exists()
213 {
214 best_platform = Some(dir);
215 best_version = ver;
216 }
217 }
218 let platform = match best_platform {
219 Some(p) => p,
220 None => fatal!("cannot find $ANDROID_HOME/platforms/<version>/android.jar"),
221 };
222
223 let apk_path = temp_dir.join("output.apk");
225 let mut aapt2 = Command::new(&aapt2_path);
226 aapt2
227 .arg("link")
228 .arg("-o")
229 .arg(&apk_path)
230 .arg("--manifest")
231 .arg(manifest_path)
232 .arg("-I")
233 .arg(platform.join("android.jar"));
234 if compiled_res.exists() {
235 aapt2.arg(&compiled_res);
236 }
237 if assets.exists() {
238 aapt2.arg("-A").arg(&assets);
239 }
240 if aapt2.status().map(|s| !s.success()).unwrap_or(true) {
241 fatal!("apk linking failed");
242 }
243
244 let aapt_path = build_tools.join("aapt");
246 for lib in ::glob::glob(apk_folder.join("lib/*/*.so").display().to_string().as_str()).unwrap() {
247 let lib = lib.unwrap_or_else(|e| fatal!("error searching libs, {e}"));
248
249 let lib = lib.display().to_string().replace('\\', "/");
250 let lib = &lib[lib.rfind("/lib/").unwrap() + 1..];
251
252 let mut aapt = Command::new(&aapt_path);
253 aapt.arg("add").arg(&apk_path).arg(lib).current_dir(&apk_folder);
254 if aapt.status().map(|s| !s.success()).unwrap_or(true) {
255 fatal!("apk linking failed");
256 }
257 }
258
259 let final_apk = if raw {
260 apk_path
261 } else {
262 let aligned_apk_path = temp_dir.join("output-aligned.apk");
264 let zipalign_path = build_tools.join("zipalign");
265 let mut zipalign = Command::new(zipalign_path);
266 zipalign.arg("-v").arg("4").arg(apk_path).arg(&aligned_apk_path);
267 if zipalign.status().map(|s| !s.success()).unwrap_or(true) {
268 fatal!("zipalign failed");
269 }
270
271 let signed_apk_path = temp_dir.join("output-signed.apk");
273 if debug {
274 let dirs = directories::BaseDirs::new().unwrap_or_else(|| fatal!("cannot fine $HOME"));
275 keystore = dirs.home_dir().join(".android/debug.keystore");
276 keystore_pass = "android".to_owned();
277 key_alias = "androiddebugkey".to_owned();
278 key_pass = "android".to_owned();
279 if !keystore.exists() {
280 let _ = fs::create_dir_all(keystore.parent().unwrap());
282 let keytool_path = Path::new(&env::var("JAVA_HOME").expect("please set JAVA_HOME")).join("bin/keytool");
283 let mut keytool = Command::new(&keytool_path);
284 keytool
285 .arg("-genkey")
286 .arg("-v")
287 .arg("-keystore")
288 .arg(&keystore)
289 .arg("-storepass")
290 .arg(&keystore_pass)
291 .arg("-alias")
292 .arg(&key_alias)
293 .arg("-keypass")
294 .arg(&key_pass)
295 .arg("-keyalg")
296 .arg("RSA")
297 .arg("-keysize")
298 .arg("2048")
299 .arg("-validity")
300 .arg("10000")
301 .arg("-dname")
302 .arg("CN=Android Debug,O=Android,C=US")
303 .arg("-storetype")
304 .arg("pkcs12");
305
306 match keytool.status() {
307 Ok(s) => {
308 if !s.success() {
309 fatal!("keytool failed generating debug keys");
310 }
311 }
312 Err(e) => fatal!("cannot run '{}', {e}", keytool_path.display()),
313 }
314 }
315 }
316
317 #[cfg(not(windows))]
318 const APKSIGNER_NAME: &str = "apksigner";
319 #[cfg(windows)]
320 const APKSIGNER_NAME: &str = "apksigner.bat";
321
322 let apksigner_path = build_tools.join(APKSIGNER_NAME);
323 let mut apksigner = Command::new(&apksigner_path);
324 apksigner
325 .arg("sign")
326 .arg("--ks")
327 .arg(keystore)
328 .arg("--ks-pass")
329 .arg(format!("pass:{keystore_pass}"))
330 .arg("--ks-key-alias")
331 .arg(key_alias)
332 .arg("--key-pass")
333 .arg(format!("pass:{key_pass}"))
334 .arg("--out")
335 .arg(&signed_apk_path)
336 .arg(&aligned_apk_path);
337
338 match apksigner.status() {
339 Ok(s) => {
340 if !s.success() {
341 fatal!("apksigner failed")
342 }
343 }
344 Err(e) => fatal!("cannot run '{}', {e}", apksigner_path.display()),
345 }
346 signed_apk_path
347 };
348
349 fs::remove_dir_all(&apk_folder).unwrap_or_else(|e| fatal!("apk folder cleanup failed, {e}"));
351 fs::rename(final_apk, output_file).unwrap_or_else(|e| fatal!("cannot copy built apk to final place, {e}"));
352 fs::remove_dir_all(&temp_dir).unwrap_or_else(|e| fatal!("temp dir cleanup failed, {e}"));
353 let _ = fs::remove_file(path(ZR_TARGET));
354}
355#[derive(serde::Deserialize)]
356#[serde(rename = "manifest")]
357struct AndroidManifest {
358 #[serde(rename = "uses-sdk")]
359 #[serde(default)]
360 pub uses_sdk: AndroidSdk,
361}
362#[derive(Default, serde::Deserialize)]
363#[serde(rename = "uses-sdk")]
364struct AndroidSdk {
365 #[serde(rename(serialize = "android:minSdkVersion"))]
366 pub min_sdk_version: Option<u32>,
367 #[serde(rename(serialize = "android:targetSdkVersion"))]
368 pub target_sdk_version: Option<u32>,
369 #[serde(rename(serialize = "android:maxSdkVersion"))]
370 pub max_sdk_version: Option<u32>,
371}
372impl AndroidSdk {
373 pub fn matches(&self, version: u32) -> bool {
374 if let Some(v) = self.target_sdk_version {
375 return v == version;
376 }
377 if let Some(m) = self.min_sdk_version
378 && version < m
379 {
380 return false;
381 }
382 if let Some(m) = self.max_sdk_version
383 && version > m
384 {
385 return false;
386 }
387 true
388 }
389}