Skip to content

Commit 673867a

Browse files
authored
feat(cli): detect Android env and install SDK and NDK if needed (#14094)
* feat(cli): detect Android env and install SDK and NDK if needed changes the Android setup to be a bit more automated - looking up ANDROID_HOME and NDK_HOME from common system paths and installing the Android SDK and NDK if needed using the command line tools * fix windows * clippy * lint * add prmopts and ci check * also check ANDROID_SDK_ROOT
1 parent 4188ffd commit 673867a

File tree

11 files changed

+290
-48
lines changed

11 files changed

+290
-48
lines changed

.changes/ensure-android-env.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"tauri-cli": minor:feat
3+
"@tauri-apps/cli": minor:feat
4+
---
5+
6+
Try to detect ANDROID_HOME and NDK_HOME environment variables from default system locations and install them if needed using the Android Studio command line tools.

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/tauri-cli/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ toml = "0.9"
6969
jsonschema = "0.33"
7070
handlebars = "6"
7171
include_dir = "0.7"
72+
dirs = "6"
7273
minisign = "=0.7.3"
7374
base64 = "0.22"
7475
ureq = { version = "3", default-features = false, features = ["gzip"] }
@@ -110,6 +111,8 @@ memchr = "2"
110111
tempfile = "3"
111112
uuid = { version = "1", features = ["v5"] }
112113
rand = "0.9"
114+
zip = { version = "4", default-features = false, features = ["deflate"] }
115+
which = "8"
113116

114117
[dev-dependencies]
115118
insta = "1"

crates/tauri-cli/src/helpers/cargo_manifest.rs

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -131,20 +131,7 @@ struct CrateIoGetResponse {
131131
pub fn crate_latest_version(name: &str) -> Option<String> {
132132
// Reference: https://github.com/rust-lang/crates.io/blob/98c83c8231cbcd15d6b8f06d80a00ad462f71585/src/controllers/krate/metadata.rs#L88
133133
let url = format!("https://crates.io/api/v1/crates/{name}?include");
134-
#[cfg(feature = "platform-certs")]
135-
let mut response = {
136-
let agent = ureq::Agent::config_builder()
137-
.tls_config(
138-
ureq::tls::TlsConfig::builder()
139-
.root_certs(ureq::tls::RootCerts::PlatformVerifier)
140-
.build(),
141-
)
142-
.build()
143-
.new_agent();
144-
agent.get(&url).call().ok()?
145-
};
146-
#[cfg(not(feature = "platform-certs"))]
147-
let mut response = ureq::get(&url).call().ok()?;
134+
let mut response = super::http::get(&url).ok()?;
148135
let metadata: CrateIoGetResponse =
149136
serde_json::from_reader(response.body_mut().as_reader()).unwrap();
150137
metadata.krate.default_version
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2+
// SPDX-License-Identifier: Apache-2.0
3+
// SPDX-License-Identifier: MIT
4+
5+
use ureq::{http::Response, Body};
6+
7+
pub fn get(url: &str) -> Result<Response<Body>, ureq::Error> {
8+
#[cfg(feature = "platform-certs")]
9+
{
10+
let agent = ureq::Agent::config_builder()
11+
.tls_config(
12+
ureq::tls::TlsConfig::builder()
13+
.root_certs(ureq::tls::RootCerts::PlatformVerifier)
14+
.build(),
15+
)
16+
.build()
17+
.new_agent();
18+
agent.get(url).call()
19+
}
20+
#[cfg(not(feature = "platform-certs"))]
21+
{
22+
ureq::get(url).call()
23+
}
24+
}

crates/tauri-cli/src/helpers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub mod config;
99
pub mod flock;
1010
pub mod framework;
1111
pub mod fs;
12+
pub mod http;
1213
pub mod npm;
1314
#[cfg(target_os = "macos")]
1415
pub mod pbxproj;

crates/tauri-cli/src/mobile/android/android_studio_script.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ pub fn command(options: Options) -> Result<()> {
103103
)?;
104104
}
105105

106-
let env = env()?;
106+
let env = env(std::env::var("CI").is_ok())?;
107107

108108
if cli_options.dev {
109109
let dev_url = tauri_config

crates/tauri-cli/src/mobile/android/build.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> {
163163
MobileTarget::Android,
164164
)?;
165165

166-
let mut env = env()?;
166+
let mut env = env(options.ci)?;
167167
configure_cargo(&mut env, &config)?;
168168

169169
crate::build::setup(&interface, &mut build_options, tauri_config.clone(), true)?;

crates/tauri-cli/src/mobile/android/dev.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ fn run_command(options: Options, noise_level: NoiseLevel) -> Result<()> {
158158
.collect::<Vec<_>>(),
159159
)?;
160160

161-
let env = env()?;
161+
let env = env(false)?;
162162
let device = if options.open {
163163
None
164164
} else {

crates/tauri-cli/src/mobile/android/mod.rs

Lines changed: 235 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ use cargo_mobile2::{
2020
use clap::{Parser, Subcommand};
2121
use std::{
2222
env::set_var,
23-
fs::{create_dir, create_dir_all, write},
24-
process::exit,
23+
fs::{create_dir, create_dir_all, read_dir, write},
24+
io::Cursor,
25+
path::{Path, PathBuf},
26+
process::{exit, Command},
2527
thread::sleep,
2628
time::Duration,
2729
};
@@ -42,6 +44,19 @@ mod build;
4244
mod dev;
4345
pub(crate) mod project;
4446

47+
const NDK_VERSION: &str = "29.0.13846066";
48+
const SDK_VERSION: u8 = 36;
49+
50+
#[cfg(target_os = "macos")]
51+
const CMDLINE_TOOLS_URL: &str =
52+
"https://dl.google.com/android/repository/commandlinetools-mac-13114758_latest.zip";
53+
#[cfg(target_os = "linux")]
54+
const CMDLINE_TOOLS_URL: &str =
55+
"https://dl.google.com/android/repository/commandlinetools-linux-13114758_latest.zip";
56+
#[cfg(windows)]
57+
const CMDLINE_TOOLS_URL: &str =
58+
"https://dl.google.com/android/repository/commandlinetools-win-13114758_latest.zip";
59+
4560
#[derive(Parser)]
4661
#[clap(
4762
author,
@@ -176,11 +191,228 @@ pub fn get_config(
176191
(config, metadata)
177192
}
178193

179-
fn env() -> Result<Env> {
194+
pub fn env(non_interactive: bool) -> Result<Env> {
180195
let env = super::env()?;
196+
ensure_env(non_interactive)?;
181197
cargo_mobile2::android::env::Env::from_env(env).map_err(Into::into)
182198
}
183199

200+
fn download_cmdline_tools(extract_path: &Path) -> Result<()> {
201+
log::info!("Downloading Android command line tools...");
202+
203+
let mut response = crate::helpers::http::get(CMDLINE_TOOLS_URL)?;
204+
let body = response
205+
.body_mut()
206+
.with_config()
207+
.limit(200 * 1024 * 1024 /* 200MB */)
208+
.read_to_vec()?;
209+
210+
let mut zip = zip::ZipArchive::new(Cursor::new(body))?;
211+
212+
log::info!(
213+
"Extracting Android command line tools to {}",
214+
extract_path.display()
215+
);
216+
zip.extract(extract_path)?;
217+
218+
Ok(())
219+
}
220+
221+
fn ensure_env(non_interactive: bool) -> Result<()> {
222+
ensure_java()?;
223+
ensure_sdk(non_interactive)?;
224+
ensure_ndk(non_interactive)?;
225+
Ok(())
226+
}
227+
228+
fn ensure_java() -> Result<()> {
229+
if std::env::var_os("JAVA_HOME").is_none() {
230+
#[cfg(windows)]
231+
let default_java_home = "C:\\Program Files\\Android\\Android Studio\\jbr";
232+
#[cfg(target_os = "macos")]
233+
let default_java_home = "/Applications/Android Studio.app/Contents/jbr/Contents/Home";
234+
#[cfg(target_os = "linux")]
235+
let default_java_home = "/opt/android-studio/jbr";
236+
237+
if Path::new(default_java_home).exists() {
238+
log::info!("Using Android Studio's default Java installation: {default_java_home}");
239+
std::env::set_var("JAVA_HOME", default_java_home);
240+
} else if which::which("java").is_err() {
241+
anyhow::bail!("Java not found in PATH, default Android Studio Java installation not found at {default_java_home} and JAVA_HOME environment variable not set. Please install Java before proceeding");
242+
}
243+
}
244+
245+
Ok(())
246+
}
247+
248+
fn ensure_sdk(non_interactive: bool) -> Result<()> {
249+
let android_home = std::env::var_os("ANDROID_HOME")
250+
.map(PathBuf::from)
251+
.or_else(|| std::env::var_os("ANDROID_SDK_ROOT").map(PathBuf::from));
252+
if !android_home.as_ref().is_some_and(|v| v.exists()) {
253+
log::info!(
254+
"ANDROID_HOME {}, trying to locate Android SDK...",
255+
if let Some(v) = &android_home {
256+
format!("not found at {}", v.display())
257+
} else {
258+
"not set".into()
259+
}
260+
);
261+
262+
#[cfg(target_os = "macos")]
263+
let default_android_home = dirs::home_dir().unwrap().join("Library/Android/sdk");
264+
#[cfg(target_os = "linux")]
265+
let default_android_home = dirs::home_dir().unwrap().join("Android/Sdk");
266+
#[cfg(windows)]
267+
let default_android_home = dirs::data_local_dir().unwrap().join("Android/Sdk");
268+
269+
if default_android_home.exists() {
270+
log::info!(
271+
"Using installed Android SDK: {}",
272+
default_android_home.display()
273+
);
274+
} else if non_interactive {
275+
anyhow::bail!("Android SDK not found. Make sure the SDK and NDK are installed and the ANDROID_HOME and NDK_HOME environment variables are set.");
276+
} else {
277+
log::error!(
278+
"Android SDK not found at {}",
279+
default_android_home.display()
280+
);
281+
282+
let extract_path = if create_dir_all(&default_android_home).is_ok() {
283+
default_android_home.clone()
284+
} else {
285+
std::env::current_dir()?
286+
};
287+
288+
let sdk_manager_path = extract_path
289+
.join("cmdline-tools/bin/sdkmanager")
290+
.with_extension(if cfg!(windows) { "bat" } else { "" });
291+
292+
let mut granted_permission_to_install = false;
293+
294+
if !sdk_manager_path.exists() {
295+
granted_permission_to_install = crate::helpers::prompts::confirm(
296+
"Do you want to install the Android Studio command line tools to setup the Android SDK?",
297+
Some(false),
298+
)
299+
.unwrap_or_default();
300+
301+
if !granted_permission_to_install {
302+
anyhow::bail!("Skipping Android Studio command line tools installation. Please go through the manual setup process described in the documentation: https://tauri.app/start/prerequisites/#android");
303+
}
304+
305+
download_cmdline_tools(&extract_path)?;
306+
}
307+
308+
if !granted_permission_to_install {
309+
granted_permission_to_install = crate::helpers::prompts::confirm(
310+
"Do you want to install the Android SDK using the command line tools?",
311+
Some(false),
312+
)
313+
.unwrap_or_default();
314+
315+
if !granted_permission_to_install {
316+
anyhow::bail!("Skipping Android Studio SDK installation. Please go through the manual setup process described in the documentation: https://tauri.app/start/prerequisites/#android");
317+
}
318+
}
319+
320+
log::info!("Running sdkmanager to install platform-tools, android-{SDK_VERSION} and ndk-{NDK_VERSION} on {}...", default_android_home.display());
321+
let status = Command::new(&sdk_manager_path)
322+
.arg(format!("--sdk_root={}", default_android_home.display()))
323+
.arg("--install")
324+
.arg("platform-tools")
325+
.arg(format!("platforms;android-{SDK_VERSION}"))
326+
.arg(format!("ndk;{NDK_VERSION}"))
327+
.status()?;
328+
329+
if !status.success() {
330+
anyhow::bail!("Failed to install Android SDK");
331+
}
332+
}
333+
334+
std::env::set_var("ANDROID_HOME", default_android_home);
335+
}
336+
337+
Ok(())
338+
}
339+
340+
fn ensure_ndk(non_interactive: bool) -> Result<()> {
341+
// re-evaluate ANDROID_HOME
342+
let android_home = std::env::var_os("ANDROID_HOME")
343+
.map(PathBuf::from)
344+
.or_else(|| std::env::var_os("ANDROID_SDK_ROOT").map(PathBuf::from))
345+
.ok_or_else(|| anyhow::anyhow!("Failed to locate Android SDK"))?;
346+
let mut installed_ndks = read_dir(android_home.join("ndk"))
347+
.map(|dir| {
348+
dir
349+
.into_iter()
350+
.flat_map(|e| e.ok().map(|e| e.path()))
351+
.collect::<Vec<_>>()
352+
})
353+
.unwrap_or_default();
354+
installed_ndks.sort();
355+
356+
if let Some(ndk) = installed_ndks.last() {
357+
log::info!("Using installed NDK: {}", ndk.display());
358+
std::env::set_var("NDK_HOME", ndk);
359+
} else if non_interactive {
360+
anyhow::bail!("Android NDK not found. Make sure the NDK is installed and the NDK_HOME environment variable is set.");
361+
} else {
362+
let sdk_manager_path = android_home
363+
.join("cmdline-tools/bin/sdkmanager")
364+
.with_extension(if cfg!(windows) { "bat" } else { "" });
365+
366+
let mut granted_permission_to_install = false;
367+
368+
if !sdk_manager_path.exists() {
369+
granted_permission_to_install = crate::helpers::prompts::confirm(
370+
"Do you want to install the Android Studio command line tools to setup the Android NDK?",
371+
Some(false),
372+
)
373+
.unwrap_or_default();
374+
375+
if !granted_permission_to_install {
376+
anyhow::bail!("Skipping Android Studio command line tools installation. Please go through the manual setup process described in the documentation: https://tauri.app/start/prerequisites/#android");
377+
}
378+
379+
download_cmdline_tools(&android_home)?;
380+
}
381+
382+
if !granted_permission_to_install {
383+
granted_permission_to_install = crate::helpers::prompts::confirm(
384+
"Do you want to install the Android NDK using the command line tools?",
385+
Some(false),
386+
)
387+
.unwrap_or_default();
388+
389+
if !granted_permission_to_install {
390+
anyhow::bail!("Skipping Android Studio NDK installation. Please go through the manual setup process described in the documentation: https://tauri.app/start/prerequisites/#android");
391+
}
392+
}
393+
394+
log::info!(
395+
"Running sdkmanager to install ndk-{NDK_VERSION} on {}...",
396+
android_home.display()
397+
);
398+
let status = Command::new(&sdk_manager_path)
399+
.arg(format!("--sdk_root={}", android_home.display()))
400+
.arg("--install")
401+
.arg(format!("ndk;{NDK_VERSION}"))
402+
.status()?;
403+
404+
if !status.success() {
405+
anyhow::bail!("Failed to install Android NDK");
406+
}
407+
408+
let ndk_path = android_home.join("ndk").join(NDK_VERSION);
409+
log::info!("Installed NDK: {}", ndk_path.display());
410+
std::env::set_var("NDK_HOME", ndk_path);
411+
}
412+
413+
Ok(())
414+
}
415+
184416
fn delete_codegen_vars() {
185417
for (k, _) in std::env::vars() {
186418
if k.starts_with("WRY_") && (k.ends_with("CLASS_EXTENSION") || k.ends_with("CLASS_INIT")) {

0 commit comments

Comments
 (0)