///|
let android_compile_sdk : String = "36"
///|
let android_target_sdk : String = "35"
///|
let android_build_tools_version : String = "35.0.0"
///|
let android_cmake_version : String = "3.22.1"
///|
let android_ndk_version : String = "28.2.13676358"
///|
let android_gradle_version : String = "9.6.1"
///|
fn valid_android_ndk(root : String) -> Bool {
if !path_is_nonempty_file(join_path(root, "source.properties")) {
return false
}
let prebuilt = join_path(root, "toolchains/llvm/prebuilt")
if !path_is_dir(prebuilt) {
return false
}
let hosts : Array[String] = @fs.read_dir(prebuilt) catch { _ => [] }
hosts.any(name => {
first_executable([
join_path(prebuilt, name + "/bin/clang"),
join_path(prebuilt, name + "/bin/clang.exe"),
])
is Some(_)
})
}
///|
fn android_gradle_command(
project_root : String,
host : HostPlatform,
) -> (String, String) {
for root in project_search_roots(project_root) {
match
first_executable([
join_path(root, "gradlew"),
join_path(root, "gradlew.bat"),
]) {
Some(path) => return (path, root)
None => ()
}
}
(find_executable("gradle", host).unwrap_or(""), project_root)
}
///|
fn android_doctor_check(
project_root : String,
host : HostPlatform,
required : Bool,
) -> DoctorCheck {
if !required {
return skipped_check(
"android", "Android is not required by the selected platforms",
)
}
let sdk = env_directory(["ANDROID_SDK_ROOT", "ANDROID_HOME"])
guard sdk is Some(sdk_root) else {
return doctor_check(
"android",
Fail,
true,
"Android SDK root was not detected",
remediation="Set ANDROID_SDK_ROOT or ANDROID_HOME to an installed Android SDK.",
)
}
let requested_ndk = @env.get_env_var("MOUI_ANDROID_NDK_VERSION").unwrap_or(
android_ndk_version,
)
let requested_ndk_version = match first_numeric_version(requested_ndk) {
Some(value) => value
None =>
return doctor_check(
"android",
Fail,
true,
"MOUI_ANDROID_NDK_VERSION is invalid",
detail=requested_ndk,
remediation="Use an installed NDK revision at or above 28.2.",
)
}
if !numeric_version_at_least(requested_ndk_version, [28, 2]) {
return doctor_check(
"android",
Fail,
true,
"Configured Android NDK is below the 28.2 floor",
detail=requested_ndk,
remediation="Install Android NDK 28.2 and unset or update MOUI_ANDROID_NDK_VERSION.",
)
}
let ndk_candidates = [
@env.get_env_var("ANDROID_NDK_HOME").unwrap_or(""),
@env.get_env_var("ANDROID_NDK_ROOT").unwrap_or(""),
join_path(sdk_root, "ndk/" + requested_ndk),
]
let ndk = Ref("")
let ndk_revision_text = Ref("")
let ndk_mismatch = Ref("")
for candidate in ndk_candidates {
if candidate == "" || !valid_android_ndk(candidate) {
continue
}
match ndk_revision(candidate) {
Some(revision) if numeric_version_at_least(revision, [28, 2]) &&
numeric_version_has_prefix(revision, requested_ndk_version) => {
ndk.val = candidate
ndk_revision_text.val = numeric_version_text(revision)
break
}
Some(revision) =>
ndk_mismatch.val = candidate +
" reports " +
numeric_version_text(revision) +
", expected " +
requested_ndk
None => ndk_mismatch.val = candidate + " has no readable Pkg.Revision"
}
}
if ndk.val == "" {
return doctor_check(
"android",
Fail,
true,
"Android SDK is present but the required NDK 28.2 installation was not found",
detail=if ndk_mismatch.val == "" { sdk_root } else { ndk_mismatch.val },
remediation="Install Android NDK " +
requested_ndk +
" or set MOUI_ANDROID_NDK_VERSION and ANDROID_NDK_HOME to the same complete 28.2+ revision.",
)
}
let java_home = env_directory(["JAVA_HOME"])
let java = find_executable_in_home("java", java_home, host)
let javac = find_executable_in_home("javac", java_home, host)
let jlink = find_executable_in_home("jlink", java_home, host)
if java is None || javac is None || jlink is None {
return doctor_check(
"android",
Fail,
true,
"Android builds require a complete JDK",
detail="java=" +
java.unwrap_or("missing") +
" javac=" +
javac.unwrap_or("missing") +
" jlink=" +
jlink.unwrap_or("missing"),
remediation="Install a Java 17+ JDK (Java 21 recommended) and set JAVA_HOME.",
)
}
let java_version = match probe_java_floor(java.unwrap()) {
Ok(value) => value
Err(message) =>
return doctor_check(
"android",
Fail,
true,
"Android builds require Java 17 or newer",
detail=message,
remediation="Install a Java 17+ JDK (Java 21 recommended) and set JAVA_HOME.",
)
}
if find_executable("node", host) is None {
return doctor_check(
"android",
Fail,
true,
"Node.js is missing from PATH",
remediation="Install Node.js for the MoUI Android build preparation step.",
)
}
let (gradle, gradle_root) = android_gradle_command(project_root, host)
if gradle == "" {
return doctor_check(
"android",
Fail,
true,
"Gradle was not found",
remediation="Add a Gradle wrapper at the project root or install Gradle.",
)
}
let gradle_version = match gradle_version_floor(gradle_root, gradle, host) {
Some(Ok(value)) => Some(value)
Some(Err(message)) =>
return doctor_check(
"android",
Fail,
true,
"Gradle is below the window-hosted build floor",
detail=message,
remediation="Install Gradle " +
android_gradle_version +
"+ or update gradle-wrapper.properties without running the wrapper from doctor.",
)
None => None
}
let compile_android_jar = join_path(
sdk_root,
"platforms/android-" + android_compile_sdk + "/android.jar",
)
let target_android_jar = join_path(
sdk_root,
"platforms/android-" + android_target_sdk + "/android.jar",
)
let build_tools = versioned_android_executable(
join_path(sdk_root, "build-tools"),
"aapt2",
[35, 0, 0],
)
let cmake = versioned_android_executable(
join_path(sdk_root, "cmake"),
"bin/cmake",
[3, 22, 1],
)
if !path_is_nonempty_file(compile_android_jar) ||
!path_is_nonempty_file(target_android_jar) ||
build_tools is None ||
cmake is None {
return doctor_check(
"android",
Fail,
true,
"Android SDK build components are incomplete",
detail="compileSdk" +
android_compile_sdk +
"=" +
(if path_is_nonempty_file(compile_android_jar) {
compile_android_jar
} else {
"missing"
}) +
" targetSdk" +
android_target_sdk +
"=" +
(if path_is_nonempty_file(target_android_jar) {
target_android_jar
} else {
"missing"
}) +
" buildTools>=35=" +
(match build_tools {
Some((path, _)) => path
None => "missing"
}) +
" cmake>=3.22.1=" +
(match cmake {
Some((path, _)) => path
None => "missing"
}),
remediation="Install Android Platforms " +
android_target_sdk +
" and " +
android_compile_sdk +
", Build-Tools " +
android_build_tools_version +
"+, and CMake " +
android_cmake_version +
"+.",
)
}
let (build_tools_path, build_tools_version) = match build_tools {
Some(value) => value
None => abort("validated Android build tools must be present")
}
let (cmake_path, cmake_directory_version) = match cmake {
Some(value) => value
None => abort("validated Android CMake must be present")
}
let cmake_version = match
probe_version_floor(
cmake_path,
DoubleDashVersion,
"cmake version",
[3, 22, 1],
"CMake",
) {
Ok(value) => value
Err(message) =>
return doctor_check(
"android",
Fail,
true,
"Android CMake is below the window-hosted build floor",
detail=message,
remediation="Install Android SDK CMake " +
android_cmake_version +
" or newer.",
)
}
let warnings : Array[String] = []
if gradle_version is None {
warnings.push(
"Gradle version could not be detected without executing a wrapper",
)
}
let adb = first_executable([
join_path(sdk_root, "platform-tools/adb"),
join_path(sdk_root, "platform-tools/adb.exe"),
])
if adb is None {
warnings.push("adb was not found")
}
let detail = "sdk=" +
sdk_root +
" compileSdk=" +
android_compile_sdk +
" targetSdk=" +
android_target_sdk +
" buildTools=" +
build_tools_version +
" aapt2=" +
build_tools_path +
" ndk=" +
ndk_revision_text.val +
" java=" +
java_version +
" gradle=" +
gradle_version.unwrap_or("unknown") +
" cmake=" +
cmake_version +
" (sdk " +
cmake_directory_version +
")" +
" adb=" +
adb.unwrap_or("missing")
if warnings.is_empty() {
doctor_check(
"android",
Pass,
true,
"Android SDK and window-hosted toolchain floors are satisfied",
detail~,
)
} else {
doctor_check(
"android",
Warn,
true,
"Android build floors are satisfied with runtime/tool version warnings",
detail=detail + " | " + warnings.join("; "),
remediation="Install Android Platform Tools and expose verifiable Gradle " +
android_gradle_version +
"+ metadata before release builds.",
)
}
}