#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'USAGE'
Usage: scripts/setup-android-sdk.sh [options]
Install the official Android command-line SDK pieces needed to build the MoUI
Counter Android APK.
Options:
--android-home <dir> SDK root, default macOS ~/Library/Android/sdk,
Linux ~/Android/Sdk.
--compile-sdk <n> Android compile platform, default 36.
--build-tools <ver> Android build-tools version, default 35.0.0.
--cmake <ver> Android SDK CMake version, default 3.22.1.
--ndk <ver> Android NDK version, default 28.2.13676358
(matches Gradle/prepare-native-build pin).
--accept-licenses Accept Android SDK licenses non-interactively.
--skip-licenses Do not run sdkmanager --licenses.
--dry-run Print the packages that would be installed.
--print-env Print shell exports only, without installing anything.
-h, --help Show this help.
Examples:
scripts/setup-android-sdk.sh --accept-licenses
eval "$(scripts/setup-android-sdk.sh --print-env)"
moui build android counter --mobile-config "$PWD/examples/counter/moui.mobile.json"
USAGE
}
case "$(uname -s 2>/dev/null || true)" in
Darwin)
default_android_home="$HOME/Library/Android/sdk"
;;
*)
default_android_home="$HOME/Android/Sdk"
;;
esac
android_home="$default_android_home"
compile_sdk="36"
build_tools_version="35.0.0"
cmake_version="3.22.1"
ndk_version="28.2.13676358"
accept_licenses=0
skip_licenses=0
dry_run=0
print_env=0
repository_xml_url="${ANDROID_REPOSITORY_XML_URL:-https://dl.google.com/android/repository/repository2-1.xml}"
repository_base_url="${ANDROID_REPOSITORY_BASE_URL:-https://dl.google.com/android/repository}"
while [ "$#" -gt 0 ]; do
case "$1" in
--android-home)
android_home="${2:?missing directory after --android-home}"
shift 2
;;
--compile-sdk)
compile_sdk="${2:?missing API level after --compile-sdk}"
shift 2
;;
--build-tools)
build_tools_version="${2:?missing version after --build-tools}"
shift 2
;;
--cmake)
cmake_version="${2:?missing version after --cmake}"
shift 2
;;
--ndk)
ndk_version="${2:?missing version after --ndk}"
shift 2
;;
--accept-licenses)
accept_licenses=1
shift
;;
--skip-licenses)
skip_licenses=1
shift
;;
--dry-run)
dry_run=1
shift
;;
--print-env)
print_env=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "unknown option: $1" >&2
usage >&2
exit 2
;;
esac
done
case "$android_home" in
"~"|"~/"*)
android_home="$HOME${android_home#\~}"
;;
esac
packages=(
"platform-tools"
"platforms;android-$compile_sdk"
"build-tools;$build_tools_version"
"cmake;$cmake_version"
"ndk;$ndk_version"
)
log() {
printf '[moui-android-setup] %s\n' "$*" >&2
}
require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "required command not found: $1" >&2
exit 1
fi
}
print_exports() {
local cmake_bin="$android_home/cmake/$cmake_version/bin"
printf 'export ANDROID_HOME=%q\n' "$android_home"
printf 'export ANDROID_SDK_ROOT=%q\n' "$android_home"
printf 'export ANDROID_NDK_HOME=%q\n' "$android_home/ndk/$ndk_version"
printf 'export PATH=%q:%q:%q:$PATH\n' \
"$android_home/cmdline-tools/latest/bin" \
"$android_home/platform-tools" \
"$cmake_bin"
}
host_os_for_android_repo() {
case "$(uname -s)" in
Darwin)
printf 'macosx'
;;
Linux)
printf 'linux'
;;
*)
echo "unsupported host OS for this setup script: $(uname -s)" >&2
exit 1
;;
esac
}
sha1_file() {
if command -v shasum >/dev/null 2>&1; then
shasum -a 1 "$1" | awk '{ print $1 }'
elif command -v sha1sum >/dev/null 2>&1; then
sha1sum "$1" | awk '{ print $1 }'
else
printf ''
fi
}
find_latest_cmdline_tools_archive() {
local metadata_file="$1"
local host_os="$2"
awk -v host="$host_os" '
function tag_text(line) {
sub(/^[^>]*>/, "", line)
sub(/<.*$/, "", line)
return line
}
/<remotePackage path="cmdline-tools;latest"/ { in_pkg = 1 }
in_pkg && /<archive>/ { in_archive = 1; url = ""; checksum = ""; os = "" }
in_pkg && in_archive && /<url>/ { url = tag_text($0) }
in_pkg && in_archive && /<checksum>/ { checksum = tag_text($0) }
in_pkg && in_archive && /<host-os>/ { os = tag_text($0) }
in_pkg && in_archive && /<\/archive>/ {
if (os == host) {
print url "\t" checksum
found = 1
exit
}
in_archive = 0
}
in_pkg && /<\/remotePackage>/ { exit }
END {
if (!found) {
exit 1
}
}
' "$metadata_file"
}
download_cmdline_tools() {
local sdkmanager_path="$android_home/cmdline-tools/latest/bin/sdkmanager"
if [ -x "$sdkmanager_path" ]; then
log "using existing sdkmanager: $sdkmanager_path"
return 0
fi
require_cmd curl
require_cmd unzip
local host_os
host_os="$(host_os_for_android_repo)"
local tmp_dir metadata archive_info archive_name expected_sha1 archive_url zip_path actual_sha1
tmp_dir="$(mktemp -d)"
metadata="$tmp_dir/repository2-1.xml"
zip_path="$tmp_dir/cmdline-tools.zip"
log "fetching Android repository metadata"
curl -fsSL --retry 3 "$repository_xml_url" -o "$metadata"
archive_info="$(find_latest_cmdline_tools_archive "$metadata" "$host_os")"
archive_name="${archive_info%%$'\t'*}"
expected_sha1="${archive_info#*$'\t'}"
archive_url="$repository_base_url/$archive_name"
log "downloading official Android command-line tools: $archive_name"
curl -fL --retry 3 "$archive_url" -o "$zip_path"
actual_sha1="$(sha1_file "$zip_path")"
if [ -n "$actual_sha1" ] && [ "$actual_sha1" != "$expected_sha1" ]; then
echo "checksum mismatch for $archive_name" >&2
echo "expected: $expected_sha1" >&2
echo "actual: $actual_sha1" >&2
exit 1
fi
mkdir -p "$android_home/cmdline-tools"
if [ -e "$android_home/cmdline-tools/latest" ]; then
mv "$android_home/cmdline-tools/latest" \
"$android_home/cmdline-tools/latest.bak.$(date +%Y%m%d%H%M%S)"
fi
unzip -q "$zip_path" -d "$tmp_dir/unpacked"
mv "$tmp_dir/unpacked/cmdline-tools" "$android_home/cmdline-tools/latest"
log "installed sdkmanager: $sdkmanager_path"
}
accept_sdk_licenses() {
local sdkmanager_path="$1"
if [ "$skip_licenses" -eq 1 ]; then
log "skipping sdkmanager --licenses"
return 0
fi
if [ "$accept_licenses" -eq 1 ]; then
log "accepting Android SDK licenses non-interactively"
{
for _ in {1..200}; do
printf 'y\n'
done
} | "$sdkmanager_path" --sdk_root="$android_home" --licenses
else
log "running interactive Android SDK license prompt"
"$sdkmanager_path" --sdk_root="$android_home" --licenses
fi
}
install_packages() {
local sdkmanager_path="$1"
log "installing Android SDK packages:"
for package in "${packages[@]}"; do
log " $package"
done
"$sdkmanager_path" --sdk_root="$android_home" --install "${packages[@]}"
}
if [ "$print_env" -eq 1 ]; then
print_exports
exit 0
fi
if [ "$dry_run" -eq 1 ]; then
log "SDK root: $android_home"
log "packages:"
for package in "${packages[@]}"; do
log " $package"
done
exit 0
fi
require_cmd java
download_cmdline_tools
sdkmanager_path="$android_home/cmdline-tools/latest/bin/sdkmanager"
if [ ! -x "$sdkmanager_path" ]; then
echo "sdkmanager was not found after installation: $sdkmanager_path" >&2
exit 1
fi
accept_sdk_licenses "$sdkmanager_path"
install_packages "$sdkmanager_path"
log "Android SDK setup complete"
cat <<EOF
Add this environment to your current shell:
$(print_exports)
Then build the Counter APK:
moui build android counter --mobile-config "$PWD/examples/counter/moui.mobile.json"
EOF