Feat: Android Dynamic Arch Support (#3215)

* feat: android multi-arch support

* revision: make krate immutable

* revision: minor tweaks

* revision: make pub crate

* revision: dedup arch access

* revision: minor tweaks
This commit is contained in:
Miles Murgaw 2024-11-14 13:24:48 -05:00 committed by GitHub
parent 5ecbb6594c
commit af53354b28
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 205 additions and 58 deletions

View file

@ -745,7 +745,13 @@ impl AppBundle {
)?;
}
let output = Command::new("./gradlew")
let gradle_exec_name = match cfg!(windows) {
true => "gradlew.bat",
false => "gradlew",
};
let gradle_exec = self.build.root_dir().join(gradle_exec_name);
let output = Command::new(gradle_exec)
.arg("assembleDebug")
.current_dir(self.build.root_dir())
.stderr(std::process::Stdio::piped())

View file

@ -114,10 +114,11 @@ impl BuildRequest {
// We don't want to overwrite the user's .cargo/config.toml since that gets committed to git
// and we want everyone's install to be the same.
if self.build.platform() == Platform::Android {
let linker = self
let ndk = self
.krate
.android_linker()
.android_ndk()
.context("Could not autodetect android linker")?;
let linker = self.build.target_args.arch().android_linker(&ndk);
tracing::trace!("Using android linker: {linker:?}");
@ -252,7 +253,7 @@ impl BuildRequest {
Some(true) => Some("aarch64-apple-ios"),
_ => Some("aarch64-apple-ios-sim"),
},
Platform::Android => Some("aarch64-linux-android"),
Platform::Android => Some(self.build.target_args.arch().android_target_triplet()),
Platform::Server => None,
// we're assuming we're building for the native platform for now... if you're cross-compiling
// the targets here might be different
@ -536,7 +537,7 @@ impl BuildRequest {
.join("src")
.join("main")
.join("jniLibs")
.join("arm64-v8a"),
.join(self.build.target_args.arch().android_jnilib()),
// these are all the same, I think?
Platform::Windows

View file

@ -134,13 +134,20 @@ impl BuildRequest {
/// will do its best to fill in the missing bits by exploring the sdk structure
/// IE will attempt to use the Java installed from android studio if possible.
pub(crate) async fn verify_android_tooling(&self, _rustup: RustupShow) -> Result<()> {
if self.krate.android_linker().is_none() {
return Err(anyhow::anyhow!(
"Android linker not found. Please set the ANDROID_NDK_HOME environment variable to the root of your NDK installation."
).into());
let result = self
.krate
.android_ndk()
.map(|ndk| self.build.target_args.arch().android_linker(&ndk));
if let Some(path) = result {
if path.exists() {
return Ok(());
}
}
Ok(())
Err(anyhow::anyhow!(
"Android linker not found. Please set the `ANDROID_NDK_HOME` environment variable to the root of your NDK installation."
).into())
}
/// Ensure the right dependencies are installed for linux apps.

View file

@ -63,7 +63,7 @@ impl BuildArgs {
let krate =
DioxusCrate::new(&self.target_args).context("Failed to load Dioxus workspace")?;
self.resolve(&krate)?;
self.resolve(&krate).await?;
let bundle = Builder::start(&krate, self.clone())?.finish().await?;
@ -79,7 +79,7 @@ impl BuildArgs {
///
/// IE if they've specified "fullstack" as a feature on `dioxus`, then we want to build the
/// fullstack variant even if they omitted the `--fullstack` flag.
pub(crate) fn resolve(&mut self, krate: &DioxusCrate) -> Result<()> {
pub(crate) async fn resolve(&mut self, krate: &DioxusCrate) -> Result<()> {
let default_platform = krate.default_platform();
let auto_platform = krate.autodetect_platform();
@ -145,6 +145,34 @@ impl BuildArgs {
}
}
// Determine arch if android
if self.platform == Some(Platform::Android) && self.target_args.arch.is_none() {
tracing::debug!("No android arch provided, attempting to auto detect.");
let arch = Arch::autodetect().await;
// Some extra logs
let arch = match arch {
Some(a) => {
tracing::info!(
"Autodetected `{}` Android arch.",
a.android_target_triplet()
);
a.to_owned()
}
None => {
let a = Arch::default();
tracing::info!(
"Could not detect Android arch, defaulting to `{}`",
a.android_target_triplet()
);
a
}
};
self.target_args.arch = Some(arch);
}
Ok(())
}

View file

@ -46,7 +46,7 @@ impl Bundle {
// We always use `release` mode for bundling
self.build_arguments.release = true;
self.build_arguments.resolve(&krate)?;
self.build_arguments.resolve(&krate).await?;
tracing::info!("Building app...");

View file

@ -14,7 +14,7 @@ impl RunArgs {
let krate = DioxusCrate::new(&self.build_args.target_args)
.context("Failed to load Dioxus workspace")?;
self.build_args.resolve(&krate)?;
self.build_args.resolve(&krate).await?;
tracing::trace!("Building crate krate data: {:#?}", krate);
tracing::trace!("Build args: {:#?}", self.build_args);

View file

@ -54,13 +54,13 @@ impl ServeArgs {
Ok(StructuredOutput::Success)
}
pub(crate) fn load_krate(&mut self) -> Result<DioxusCrate> {
pub(crate) async fn load_krate(&mut self) -> Result<DioxusCrate> {
let krate = DioxusCrate::new(&self.build_arguments.target_args)?;
self.resolve(&krate)?;
self.resolve(&krate).await?;
Ok(krate)
}
pub(crate) fn resolve(&mut self, krate: &DioxusCrate) -> Result<()> {
pub(crate) async fn resolve(&mut self, krate: &DioxusCrate) -> Result<()> {
// Enable hot reload.
if self.hot_reload.is_none() {
self.hot_reload = Some(krate.settings.always_hot_reload.unwrap_or(true));
@ -82,7 +82,7 @@ impl ServeArgs {
}
// Resolve the build arguments
self.build_arguments.resolve(krate)?;
self.build_arguments.resolve(krate).await?;
Ok(())
}

View file

@ -1,4 +1,7 @@
use super::*;
use once_cell::sync::OnceCell;
use std::path::Path;
use tokio::process::Command;
/// Information about the target to build
#[derive(Clone, Debug, Default, Deserialize, Parser)]
@ -35,13 +38,11 @@ pub(crate) struct TargetArgs {
#[clap(long)]
pub(crate) no_default_features: bool,
/// The architecture to build for [default: "native"]
///
/// Can either be `arm | arm64 | x86 | x86_64 | native`
#[clap(long)]
pub(crate) arch: Option<String>,
/// The architecture to build for.
#[clap(long, value_enum)]
pub(crate) arch: Option<Arch>,
/// Are we building for a device or just the simulator
/// Are we building for a device or just the simulator.
/// If device is false, then we'll build for the simulator
#[clap(long)]
pub(crate) device: Option<bool>,
@ -50,3 +51,138 @@ pub(crate) struct TargetArgs {
#[clap(long)]
pub(crate) target: Option<String>,
}
impl TargetArgs {
pub(crate) fn arch(&self) -> Arch {
self.arch.unwrap_or_default()
}
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Deserialize, clap::ValueEnum)]
#[non_exhaustive]
pub(crate) enum Arch {
// Android: armv7l, armv7-linux-androideabi
Arm,
// Android: aarch64, aarch64-linux-android
#[default]
Arm64,
// Android: i386, i686-linux-android
X86,
// Android: x86_64, x86_64-linux-android
X64,
}
impl Arch {
pub(crate) async fn autodetect() -> Option<Self> {
// Try auto detecting arch through adb.
static AUTO_ARCH: OnceCell<Option<Arch>> = OnceCell::new();
match AUTO_ARCH.get() {
Some(a) => *a,
None => {
// TODO: Wire this up with --device flag. (add `-s serial`` flag before `shell` arg)
let output = Command::new("adb")
.arg("shell")
.arg("uname")
.arg("-m")
.output()
.await;
let out = match output {
Ok(o) => o,
Err(e) => {
tracing::debug!("ADB command failed: {:?}", e);
return None;
}
};
// Parse ADB output
let Ok(out) = String::from_utf8(out.stdout) else {
tracing::debug!("ADB returned unexpected data.");
return None;
};
let trimmed = out.trim().to_string();
tracing::trace!("ADB Returned: `{trimmed:?}`");
// Set the cell
let arch = Arch::try_from(trimmed).ok();
AUTO_ARCH
.set(arch)
.expect("the cell should have been checked empty by the match condition");
arch
}
}
}
pub(crate) fn android_target_triplet(&self) -> &'static str {
match self {
Arch::Arm => "armv7-linux-androideabi",
Arch::Arm64 => "aarch64-linux-android",
Arch::X86 => "i686-linux-android",
Arch::X64 => "x86_64-linux-android",
}
}
pub(crate) fn android_jnilib(&self) -> &'static str {
match self {
Arch::Arm => "armeabi-v7a",
Arch::Arm64 => "arm64-v8a",
Arch::X86 => "x86",
Arch::X64 => "x86_64",
}
}
pub(crate) fn android_clang_triplet(&self) -> &'static str {
match self {
Self::Arm => "armv7a-linux-androideabi",
_ => self.android_target_triplet(),
}
}
pub(crate) fn android_linker(&self, ndk: &Path) -> PathBuf {
// "/Users/jonkelley/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android24-clang"
let toolchain_dir = ndk.join("toolchains").join("llvm").join("prebuilt");
let triplet = self.android_clang_triplet();
let clang_exec = format!("{}24-clang", triplet);
if cfg!(target_os = "macos") {
// for whatever reason, even on aarch64 macos, the linker is under darwin-x86_64
return toolchain_dir
.join("darwin-x86_64")
.join("bin")
.join(clang_exec);
}
if cfg!(target_os = "linux") {
return toolchain_dir
.join("linux-x86_64")
.join("bin")
.join(clang_exec);
}
if cfg!(target_os = "windows") {
return toolchain_dir
.join("windows-x86_64")
.join("bin")
.join(format!("{}.cmd", clang_exec));
}
unimplemented!("Unsupported target os for android toolchain auodetection")
}
}
impl TryFrom<String> for Arch {
type Error = ();
fn try_from(value: String) -> Result<Self, Self::Error> {
match value.as_str() {
"armv7l" => Ok(Self::Arm),
"aarch64" => Ok(Self::Arm64),
"i386" => Ok(Self::X86),
"x86_64" => Ok(Self::X64),
_ => Err(()),
}
}
}

View file

@ -557,7 +557,7 @@ impl DioxusCrate {
krates
}
fn android_ndk(&self) -> Option<PathBuf> {
pub(crate) fn android_ndk(&self) -> Option<PathBuf> {
// "/Users/jonkelley/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android24-clang"
static PATH: once_cell::sync::Lazy<Option<PathBuf>> = once_cell::sync::Lazy::new(|| {
use std::env::var;
@ -596,37 +596,6 @@ impl DioxusCrate {
PATH.clone()
}
pub(crate) fn android_linker(&self) -> Option<PathBuf> {
// "/Users/jonkelley/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android24-clang"
self.android_ndk().map(|ndk| {
let toolchain_dir = ndk.join("toolchains").join("llvm").join("prebuilt");
if cfg!(target_os = "macos") {
// for whatever reason, even on aarch64 macos, the linker is under darwin-x86_64
return toolchain_dir
.join("darwin-x86_64")
.join("bin")
.join("aarch64-linux-android24-clang");
}
if cfg!(target_os = "linux") {
return toolchain_dir
.join("linux-x86_64")
.join("bin")
.join("aarch64-linux-android24-clang");
}
if cfg!(target_os = "windows") {
return toolchain_dir
.join("windows-x86_64")
.join("bin")
.join("aarch64-linux-android24-clang.cmd");
}
unimplemented!("Unsupported target os for android toolchain auodetection")
})
}
pub(crate) fn mobile_org(&self) -> String {
let identifier = self.bundle_identifier();
let mut split = identifier.splitn(3, '.');

View file

@ -42,7 +42,7 @@ pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> {
let mut tracer = TraceController::redirect();
// Load the krate and resolve the server args against it - this might log so do it after we turn on the tracer first
let krate = args.load_krate()?;
let krate = args.load_krate().await?;
// Note that starting the builder will queue up a build immediately
let mut builder = Builder::start(&krate, args.build_args())?;
@ -158,7 +158,7 @@ pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> {
screen.push_cargo_log(message);
}
BuildUpdate::BuildFailed { err } => {
tracing::error!("Build failed: {}", err);
tracing::error!("Build failed: {:?}", err);
}
BuildUpdate::BuildReady { bundle } => {
let handle = runner