diff --git a/xbuild/src/cargo/mod.rs b/xbuild/src/cargo/mod.rs index 23646d5..164c53a 100644 --- a/xbuild/src/cargo/mod.rs +++ b/xbuild/src/cargo/mod.rs @@ -330,7 +330,9 @@ impl CargoBuild { self.set_sysroot(&path); self.add_cxxflag("-stdlib=libc++"); self.add_cflag(&format!("-mmacosx-version-min={minimum_version}")); - self.add_link_arg("--target=x86_64-apple-darwin"); + if let Some(triple) = self.triple { + self.add_link_arg(&format!("--target={triple}")); + } self.add_link_arg(&format!("-mmacosx-version-min={minimum_version}")); self.add_link_arg("-rpath"); self.add_link_arg("@executable_path/../Frameworks"); @@ -360,7 +362,9 @@ impl CargoBuild { self.set_sysroot(&path); self.add_cxxflag("-stdlib=libc++"); self.add_cflag(&format!("-miphoneos-version-min={minimum_version}")); - self.add_link_arg("--target=arm64-apple-ios"); + if let Some(triple) = self.triple { + self.add_link_arg(&format!("--target={triple}")); + } self.add_link_arg(&format!("-miphoneos-version-min={minimum_version}")); self.add_link_arg("-rpath"); self.add_link_arg("@executable_path/Frameworks"); diff --git a/xbuild/src/command/build.rs b/xbuild/src/command/build.rs index 363b054..33ca054 100644 --- a/xbuild/src/command/build.rs +++ b/xbuild/src/command/build.rs @@ -75,7 +75,25 @@ pub fn build(env: &BuildEnv) -> Result<()> { } } Platform::Android => { - let out = platform_dir.join(format!("{}.{}", env.name(), env.target().format())); + // Include architecture in filename for Android builds + let arch_suffix = if env.target().archs().len() == 1 { + format!("-{}", env.target().archs()[0]) + } else { + // Multi-arch build, use "universal" or concatenate all archs + if env.target().archs().len() > 1 { + let archs: Vec = + env.target().archs().iter().map(|a| a.to_string()).collect(); + format!("-{}", archs.join("-")) + } else { + String::new() + } + }; + let out = platform_dir.join(format!( + "{}{}.{}", + env.name(), + arch_suffix, + env.target().format() + )); ensure!(has_lib, "Android APKs/AABs require a library"); let mut libraries = vec![]; @@ -200,6 +218,7 @@ pub fn build(env: &BuildEnv) -> Result<()> { runner.end_verbose_task(); return Ok(()); } else { + let out_clone = out.clone(); let mut apk = Apk::new( out, env.config().android().manifest.clone(), @@ -220,6 +239,11 @@ pub fn build(env: &BuildEnv) -> Result<()> { } apk.finish(env.target().signer().cloned())?; + + // Handle additional Android signing if release build and signing parameters are provided + if env.target().opt() == Opt::Release { + crate::gradle::handle_android_signing_for_apk(env, &out_clone)?; + } } } Platform::Macos => { diff --git a/xbuild/src/gradle/mod.rs b/xbuild/src/gradle/mod.rs index 3595533..6dd4110 100644 --- a/xbuild/src/gradle/mod.rs +++ b/xbuild/src/gradle/mod.rs @@ -1,4 +1,4 @@ -use crate::{task, BuildEnv, Format, Opt}; +use crate::{task, BuildEnv, Format, Opt, Store}; use anyhow::{Context, Result}; use apk::Target; use std::path::{Path, PathBuf}; @@ -9,6 +9,265 @@ static GRADLE_PROPERTIES: &[u8] = include_bytes!("./gradle.properties"); static SETTINGS_GRADLE: &[u8] = include_bytes!("./settings.gradle"); static IC_LAUNCHER: &[u8] = include_bytes!("./ic_launcher.xml"); +/// Generate a default Android keystore for signing if none provided +fn generate_default_keystore( + env: &BuildEnv, + keystore_path: &Path, + password: &str, + domain: &str, +) -> Result<()> { + std::fs::create_dir_all(keystore_path.parent().unwrap())?; + + let dname = format!("CN={domain}, OU=NA, O=Company, L=City, S=State, C=US"); + let pkg_name = &env.name(); + let alias_name = format!("{pkg_name}-release-key"); + + task::run( + Command::new("keytool") + .arg("-genkeypair") + .arg("-v") + .arg("-noprompt") + .arg("-storetype") + .arg("PKCS12") + .arg("-alias") + .arg(&alias_name) + .arg("-keystore") + .arg(keystore_path) + .arg("-keyalg") + .arg("RSA") + .arg("-keysize") + .arg("2048") + .arg("-validity") + .arg("10000") + .arg("-storepass") + .arg(password) + .arg("-keypass") + .arg(password) + .arg("-dname") + .arg(&dname), + )?; + + // Export the certificate for upload to Google Play + let pem_path = keystore_path + .parent() + .unwrap() + .join(format!("{pkg_name}-release-upload-certificate.pem")); + task::run( + Command::new("keytool") + .arg("-export") + .arg("-rfc") + .arg("-v") + .arg("-noprompt") + .arg("-storepass") + .arg(password) + .arg("-keypass") + .arg(password) + .arg("-keystore") + .arg(keystore_path) + .arg("-alias") + .arg(&alias_name) + .arg("-file") + .arg(&pem_path), + )?; + + Ok(()) +} + +/// Sign AAB with jarsigner +fn sign_aab_with_jarsigner( + aab_path: &Path, + keystore_path: &Path, + storepass: &str, + keyname: &str, + keypass: &str, +) -> Result<()> { + task::run( + Command::new("jarsigner") + .arg("-storepass") + .arg(storepass) + .arg("-keypass") + .arg(keypass) + .arg("-keystore") + .arg(keystore_path) + .arg(aab_path) + .arg(keyname), + )?; + Ok(()) +} + +/// Sign APK with apksigner +fn sign_apk_with_apksigner( + apk_path: &Path, + keystore_path: &Path, + storepass: &str, + keyname: &str, + keypass: &str, +) -> Result<()> { + println!("Starting APK signing process..."); + println!("Input APK: {}", apk_path.display()); + println!("Keystore: {}", keystore_path.display()); + + // First align the APK + let aligned_path = apk_path.with_extension("aligned.apk"); + println!("Aligned APK path: {}", aligned_path.display()); + + // Find zipalign in Android SDK + let android_home = std::env::var("ANDROID_HOME") + .or_else(|_| std::env::var("ANDROID_SDK_ROOT")) + .context("ANDROID_HOME or ANDROID_SDK_ROOT environment variable not set")?; + + let build_tools_dir = Path::new(&android_home).join("build-tools"); + let build_tools_version = std::fs::read_dir(&build_tools_dir)? + .filter_map(|entry| entry.ok()) + .filter(|entry| entry.path().is_dir()) + .map(|entry| entry.file_name().to_string_lossy().to_string()) + .max() + .context("No build-tools found in Android SDK")?; + + let build_tools_path = build_tools_dir.join(&build_tools_version); + let zipalign = build_tools_path.join("zipalign"); + let apksigner = build_tools_path.join("apksigner"); + + println!("Using build tools: {}", build_tools_path.display()); + println!("zipalign: {}", zipalign.display()); + println!("apksigner: {}", apksigner.display()); + + // Align the APK + println!("Aligning APK..."); + task::run( + Command::new(&zipalign) + .arg("-v") + .arg("4") + .arg(apk_path) + .arg(&aligned_path), + )?; + println!("APK alignment completed"); + + // Sign the aligned APK + let apk_name = apk_path.file_stem().unwrap().to_string_lossy(); + let apk_dir = apk_path.parent().unwrap(); + let signed_path = apk_dir.join(format!("{apk_name}-signed.apk")); + + println!("Signing aligned APK..."); + println!("Signed APK will be created at: {}", signed_path.display()); + + task::run( + Command::new(&apksigner) + .arg("sign") + .arg("--ks") + .arg(keystore_path) + .arg("--ks-key-alias") + .arg(keyname) + .arg("--ks-pass") + .arg(format!("pass:{storepass}")) + .arg("--key-pass") + .arg(format!("pass:{keypass}")) + .arg("--out") + .arg(&signed_path) + .arg(&aligned_path), + )?; + + // Verify the signed APK was created + if !signed_path.exists() { + return Err(anyhow::anyhow!( + "Signed APK was not created at: {}", + signed_path.display() + )); + } + + // Verify the APK signature + println!("Verifying APK signature..."); + let verify_result = Command::new(&apksigner) + .arg("verify") + .arg("--verbose") + .arg("--print-certs") + .arg(&signed_path) + .output(); + + match verify_result { + Ok(output) => { + if output.status.success() { + println!("✓ APK signature verification passed"); + println!( + "Signature details:\n{}", + String::from_utf8_lossy(&output.stdout) + ); + } else { + println!( + "⚠ APK signature verification failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + } + Err(e) => { + println!("⚠ Could not verify APK signature: {e}"); + } + } + + println!("✓ Signed APK created successfully"); + println!("✓ Unsigned APK: {}", apk_path.display()); + println!("✓ Signed APK: {}", signed_path.display()); + println!("APK signing process completed successfully"); + + // Clean up the aligned APK + let _ = std::fs::remove_file(&aligned_path); + + Ok(()) +} + +/// Create encrypted keystore for Google Play using pepk.jar +fn create_encrypted_keystore_for_play( + keystore_path: &Path, + keyname: &str, + _storepass: &str, + _keypass: &str, + pubkey_path: &Path, + output_path: &Path, +) -> Result<()> { + // Download pepk.jar if it doesn't exist + let pepk_jar_path = std::env::temp_dir().join("pepk.jar"); + if !pepk_jar_path.exists() { + let response = reqwest::blocking::get( + "https://www.gstatic.com/play-apps-publisher-rapid/signing-tool/prod/pepk.jar", + ) + .context("Failed to download pepk.jar")?; + + let bytes = response + .bytes() + .context("Failed to read pepk.jar response")?; + std::fs::write(&pepk_jar_path, &bytes)?; + } + + task::run( + Command::new("java") + .arg("-jar") + .arg(&pepk_jar_path) + .arg("--keystore") + .arg(keystore_path) + .arg("--alias") + .arg(keyname) + .arg("--output") + .arg(output_path) + .arg("--include-cert") + .arg("--rsa-aes-encryption") + .arg("--encryption-key-path") + .arg(pubkey_path), + )?; + + Ok(()) +} + +/// Get domain from manifest for keystore generation +fn get_domain_from_manifest(env: &BuildEnv) -> String { + env.config() + .android() + .manifest + .package + .as_ref() + .unwrap_or(&"com.example.app".to_string()) + .clone() +} + pub fn prepare(env: &BuildEnv) -> Result<()> { let config = env.config().android(); if config.wry { @@ -88,6 +347,11 @@ pub fn build(env: &BuildEnv, libraries: Vec<(Target, PathBuf)>, out: &Path) -> R versionCode {version_code} versionName '{version_name}' }} + packagingOptions {{ + jniLibs {{ + useLegacyPackaging = true + }} + }} {asset_packs} }} dependencies {{ @@ -226,6 +490,156 @@ pub fn build(env: &BuildEnv, libraries: Vec<(Target, PathBuf)>, out: &Path) -> R (Format::Aab, Opt::Release) => "app-release.aab", _ => unreachable!(), }); + + // Handle signing if release build and signing parameters are provided + if opt == Opt::Release { + handle_android_signing(env, &output, format)?; + } + std::fs::copy(output, out)?; Ok(()) } + +/// Handle Android signing for AAB and APK files +fn handle_android_signing(env: &BuildEnv, file_path: &Path, format: Format) -> Result<()> { + let keystore_path = env.target().android_sign_keystore(); + let storepass = env.target().android_sign_storepass(); + let keyname = env.target().android_sign_keyname(); + let keypass = env.target().android_sign_keypass(); + + // Determine if we need to generate a default keystore + let (final_keystore_path, final_storepass, final_keyname, final_keypass) = + if let (Some(keystore), Some(storepass), Some(keyname), Some(keypass)) = + (keystore_path, storepass, keyname, keypass) + { + ( + keystore.to_path_buf(), + storepass.to_string(), + keyname.to_string(), + keypass.to_string(), + ) + } else { + // Generate default keystore + let pkg_name = &env.name(); + let default_keystore = env + .platform_dir() + .join("keys") + .join(format!("{pkg_name}-release-key.keystore")); + let default_password = "Test123".to_string(); + let default_keyname = format!("{pkg_name}-release-key"); + let domain = get_domain_from_manifest(env); + + if !default_keystore.exists() { + println!("Generating default Android keystore..."); + generate_default_keystore(env, &default_keystore, &default_password, &domain)?; + } + + ( + default_keystore, + default_password.clone(), + default_keyname, + default_password, + ) + }; + + // Sign the file based on format + match format { + Format::Aab => { + println!("Signing AAB with jarsigner..."); + sign_aab_with_jarsigner( + file_path, + &final_keystore_path, + &final_storepass, + &final_keyname, + &final_keypass, + )?; + + // Validate the AAB after signing + println!("Validating AAB with bundletool..."); + let is_valid = validate_aab_with_bundletool(file_path)?; + if !is_valid { + println!("Warning: AAB validation failed. The BundleConfig.pb may be missing."); + } + } + Format::Apk => { + println!("Signing APK with apksigner..."); + println!("APK path: {}", file_path.display()); + sign_apk_with_apksigner( + file_path, + &final_keystore_path, + &final_storepass, + &final_keyname, + &final_keypass, + )?; + println!("APK signing completed successfully"); + } + _ => {} + } + + // Handle Google Play encryption if needed + if env.target().store() == Some(Store::Play) { + if let Some(pubkey_path) = env.target().play_app_sign_enc_pubkey() { + println!("Creating encrypted keystore for Google Play..."); + let output_zip = env.platform_dir().join("app-signing-key-encrypted.zip"); + create_encrypted_keystore_for_play( + &final_keystore_path, + &final_keyname, + &final_storepass, + &final_keypass, + pubkey_path, + &output_zip, + )?; + println!("Encrypted keystore created at: {}", output_zip.display()); + } + } + + Ok(()) +} + +/// Handle Android APK signing for non-gradle builds +pub fn handle_android_signing_for_apk(env: &BuildEnv, apk_path: &Path) -> Result<()> { + handle_android_signing(env, apk_path, Format::Apk) +} + +/// Validate AAB file using bundletool +pub fn validate_aab_with_bundletool(aab_path: &Path) -> Result { + // Download bundletool if it doesn't exist + let bundletool_jar_path = std::env::temp_dir().join("bundletool-all-1.18.1.jar"); + if !bundletool_jar_path.exists() { + println!("Downloading bundletool..."); + let response = reqwest::blocking::get("https://github.com/google/bundletool/releases/latest/download/bundletool-all-1.18.1.jar") + .context("Failed to download bundletool")?; + + let bytes = response + .bytes() + .context("Failed to read bundletool response")?; + std::fs::write(&bundletool_jar_path, &bytes)?; + } + + // Validate the AAB and check for BundleConfig.pb + let output = Command::new("java") + .arg("-jar") + .arg(&bundletool_jar_path) + .arg("validate") + .arg("--bundle") + .arg(aab_path) + .output() + .context("Failed to run bundletool validate")?; + + if !output.status.success() { + println!( + "bundletool validation failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + return Ok(false); + } + + // Check for BundleConfig.pb by grepping the output + let validation_output = String::from_utf8_lossy(&output.stdout); + let bundle_config_count = validation_output.matches("BundleConfig.pb").count(); + + println!( + "AAB validation passed. BundleConfig.pb found: {bundle_config_count} times" + ); + Ok(bundle_config_count > 0) +} diff --git a/xbuild/src/lib.rs b/xbuild/src/lib.rs index 122d6b6..44fc50b 100644 --- a/xbuild/src/lib.rs +++ b/xbuild/src/lib.rs @@ -327,6 +327,21 @@ pub struct BuildTargetArgs { /// Path to an api key. #[clap(long)] api_key: Option, + /// Path to Android AAB/APK signing keystore + #[clap(long)] + android_sign_keystore: Option, + /// Android AAB/APK signing keystore password + #[clap(long)] + android_sign_storepass: Option, + /// Android AAB/APK signing key name/alias + #[clap(long)] + android_sign_keyname: Option, + /// Android AAB/APK signing key password + #[clap(long)] + android_sign_keypass: Option, + /// Path to Google Play app signing encryption public key (for --store play) + #[clap(long)] + play_app_sign_enc_pubkey: Option, } impl BuildTargetArgs { @@ -423,6 +438,11 @@ impl BuildTargetArgs { provisioning_profile, api_key, android_gradle, + android_sign_keystore: self.android_sign_keystore, + android_sign_storepass: self.android_sign_storepass, + android_sign_keyname: self.android_sign_keyname, + android_sign_keypass: self.android_sign_keypass, + play_app_sign_enc_pubkey: self.play_app_sign_enc_pubkey, }) } } @@ -439,6 +459,11 @@ pub struct BuildTarget { provisioning_profile: Option>, api_key: Option, android_gradle: bool, + android_sign_keystore: Option, + android_sign_storepass: Option, + android_sign_keyname: Option, + android_sign_keypass: Option, + play_app_sign_enc_pubkey: Option, } impl BuildTarget { @@ -490,6 +515,26 @@ impl BuildTarget { pub fn api_key(&self) -> Option<&Path> { self.api_key.as_deref() } + + pub fn android_sign_keystore(&self) -> Option<&Path> { + self.android_sign_keystore.as_deref() + } + + pub fn android_sign_storepass(&self) -> Option<&str> { + self.android_sign_storepass.as_deref() + } + + pub fn android_sign_keyname(&self) -> Option<&str> { + self.android_sign_keyname.as_deref() + } + + pub fn android_sign_keypass(&self) -> Option<&str> { + self.android_sign_keypass.as_deref() + } + + pub fn play_app_sign_enc_pubkey(&self) -> Option<&Path> { + self.play_app_sign_enc_pubkey.as_deref() + } } pub struct BuildEnv {