From 72b30ca57b5d782aad91831256e5c392bf9a0ab9 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Mon, 3 Aug 2026 14:27:26 +0700 Subject: [PATCH 01/12] Add Android ICU4C cross-compile packaging and CI job. Enables building SIL-modified ICU for Android ABIs and verifies the APK-ready output contract in FW Branch CI. Co-authored-by: Cursor --- .github/workflows/fw_icu4c_ci.yml | 38 ++++++ README.md | 1 + icu4c/packaging/README.android.md | 101 ++++++++++++++ icu4c/packaging/build-android.ps1 | 43 ++++++ icu4c/packaging/build-android.sh | 218 ++++++++++++++++++++++++++++++ 5 files changed, 401 insertions(+) create mode 100644 icu4c/packaging/README.android.md create mode 100644 icu4c/packaging/build-android.ps1 create mode 100644 icu4c/packaging/build-android.sh diff --git a/.github/workflows/fw_icu4c_ci.yml b/.github/workflows/fw_icu4c_ci.yml index ecbf5fba0dbe..7fa7119940d9 100644 --- a/.github/workflows/fw_icu4c_ci.yml +++ b/.github/workflows/fw_icu4c_ci.yml @@ -125,6 +125,44 @@ jobs: echo 'Running ICU4C cintltst' make -j2 check + android-build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Set up Android SDK command-line tools + uses: android-actions/setup-android@v4 + + - name: Install Android NDK + run: | + sdkmanager --install 'ndk;27.0.12077973' + echo "ANDROID_NDK_HOME=$ANDROID_HOME/ndk/27.0.12077973" >> "$GITHUB_ENV" + + - name: Cache Android ICU build output + uses: actions/cache@v6 + with: + path: icu4c/out/android-icu + key: android-icu-${{ runner.os }}-${{ hashFiles('icu4c/packaging/build-android.sh', 'icu4c/source/common/unicode/uvernum.h') }} + + - name: Build Android ICU libraries + run: | + set -xeo pipefail + bash icu4c/packaging/build-android.sh --clean-arch=x86_64 --arch=x86_64,arm64-v8a + + - name: Verify Android ICU output contract + run: | + set -xeuo pipefail + major=$(grep -oP '^#define U_ICU_VERSION_MAJOR_NUM \K[0-9]+' icu4c/source/common/unicode/uvernum.h) + test -f "icu4c/out/android-icu/icudt${major}l.dat" + for abi in x86_64 arm64-v8a; do + for library in libc++_shared.so libicuuc.so libicui18n.so libicudata.so; do + test -f "icu4c/out/android-icu/$abi/$library" + done + done + debian-packaging: needs: set-version-number runs-on: ubuntu-latest diff --git a/README.md b/README.md index d46ccc544eda..4920e941b4ef 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ The ICU project is under the stewardship of [The Unicode Consortium](https://www #### Subdirectories that we modify and package - [`icu4c/`](./icu4c/) [ICU for C/C++](./icu4c/readme.html) + - [Android native build guide](./icu4c/packaging/README.android.md) #### Subdirectories that we don't care about - [`icu4j/`](./icu4j/) [ICU for Java](./icu4j/readme.html) - [`tools/`](./tools/) Tools diff --git a/icu4c/packaging/README.android.md b/icu4c/packaging/README.android.md new file mode 100644 index 000000000000..6c7e36e1911b --- /dev/null +++ b/icu4c/packaging/README.android.md @@ -0,0 +1,101 @@ +# Android ICU4C build + +`build-android.sh` cross-compiles the checked-out, SIL-modified ICU4C source in +`../source` for Android. It does not download, unpack, or substitute an +upstream ICU release. The current `fw` checkout determines the ICU version and +therefore the generated data file name. + +The build creates host ICU tools first, then uses them with the Android NDK +toolchain to cross-compile each requested ABI. It validates native-library +output only; it does not build or run an Android application, emulator, or +device test. + +## Prerequisites + +On Linux or macOS install a POSIX shell environment with `bash`, `make`, `sed`, +`find`, `sort`, a C/C++ host compiler, and the Android NDK. Android API 21 is +the default minimum API. The CI configuration uses NDK `27.0.12077973`. + +Set `ANDROID_NDK_HOME` to the NDK installation. Alternatively, set +`ANDROID_HOME` to the Android SDK root and the script selects the latest +installed `ndk/` directory. + +```bash +export ANDROID_NDK_HOME=/opt/android-ndk-r27 +bash ./icu4c/packaging/build-android.sh --arch=x86_64,arm64-v8a +``` + +On Windows, install Git for Windows and an Android NDK installed for Windows. +From PowerShell, run: + +```powershell +$env:ANDROID_NDK_HOME = 'C:\Android\Sdk\ndk\27.0.12077973' +.\icu4c\packaging\build-android.ps1 -Arch x86_64,arm64-v8a +``` + +The PowerShell wrapper deliberately invokes Git Bash. It does not use WSL: +WSL must run `bash ./icu4c/packaging/build-android.sh` and use an NDK +installed for Linux inside WSL. A Windows NDK cannot be reused from WSL, and a +Linux NDK cannot be used by Git Bash. + +## Controls and cleanup + +```bash +# Default fast validation ABI +bash ./icu4c/packaging/build-android.sh + +# Select an API level and both supported ABIs +bash ./icu4c/packaging/build-android.sh --api=21 --arch=x86_64,arm64-v8a + +# Delete one ABI's build and installed output before rebuilding it +bash ./icu4c/packaging/build-android.sh --clean-arch=x86_64 --arch=x86_64 + +# Delete all generated host tools, cross-build trees, data, and libraries +bash ./icu4c/packaging/build-android.sh --clean +``` + +Supported ABI values are `x86_64` and `arm64-v8a`. Use `--help` for the full +command reference. The generated `icu4c/out/` directory is already ignored by +this repository. + +## Output and Android integration + +For the checked-out ICU `70.1`, the output layout is: + +```text +icu4c/out/android-icu/ + icudt70l.dat + x86_64/ + libc++_shared.so + libicuuc.so + libicui18n.so + libicudata.so + libicu*.so.70[.1] + arm64-v8a/ + libc++_shared.so + libicuuc.so + libicui18n.so + libicudata.so + libicu*.so.70[.1] +``` + +The library major and data-file name are derived from the local ICU source, so +they change automatically when this branch advances to a different ICU major. +The unversioned `.so` files are the names Android APK packaging expects; the +versioned copies remain alongside them for native dependency compatibility. + +Android consumers are responsible for embedding `icudt70l.dat` (or the +locally-derived replacement after an ICU update) as an app data asset and for +packaging the ABI-specific, unversioned `libc++_shared.so`, `libicuuc.so`, +`libicui18n.so`, and `libicudata.so` files in their APK/AAB. This repository +only produces the native inputs; it does not publish a package or validate an +application's integration. + +## Troubleshooting + +- If the script reports a missing NDK, set `ANDROID_NDK_HOME` explicitly. +- If it reports a mismatched NDK toolchain, install the NDK for the host that + executes the script: Linux for Linux/WSL, Windows for Git Bash, or macOS for + macOS. +- Use `--clean` after changing host compilers, NDK versions, or ICU source + configuration to ensure host tools and cross-build output are recreated. \ No newline at end of file diff --git a/icu4c/packaging/build-android.ps1 b/icu4c/packaging/build-android.ps1 new file mode 100644 index 000000000000..c3bf8cc9fd62 --- /dev/null +++ b/icu4c/packaging/build-android.ps1 @@ -0,0 +1,43 @@ +#Requires -Version 5.1 +<#+ +.SYNOPSIS +Builds this checkout's ICU4C source for Android through Git Bash. + +.DESCRIPTION +Git Bash must use a Windows NDK installation. WSL users should invoke +build-android.sh from WSL and configure a Linux NDK there instead. +#> +[CmdletBinding()] +param( + [string] $Arch = 'x86_64', + [int] $ApiLevel = 21, + [switch] $Clean, + [string] $CleanArch, + [switch] $Help +) + +$ErrorActionPreference = 'Stop' +$bashScript = Join-Path $PSScriptRoot 'build-android.sh' + +function Find-GitBash { + $candidates = @( + 'C:\Program Files\Git\bin\bash.exe', + 'C:\Program Files (x86)\Git\bin\bash.exe', + (Join-Path ${env:ProgramFiles} 'Git\bin\bash.exe'), + (Join-Path ${env:ProgramFiles(x86)} 'Git\bin\bash.exe') + ) | Where-Object { $_ -and (Test-Path $_) } + $candidates | Select-Object -First 1 +} + +$bash = Find-GitBash +if (-not $bash) { + throw 'Git Bash was not found. Install Git for Windows, then re-run this command. For WSL, run ./icu4c/packaging/build-android.sh inside WSL with a Linux NDK installation.' +} + +$arguments = @($bashScript, "--arch=$Arch", "--api=$ApiLevel") +if ($Clean) { $arguments += '--clean' } +if ($CleanArch) { $arguments += "--clean-arch=$CleanArch" } +if ($Help) { $arguments = @($bashScript, '--help') } +Write-Host "Running: $bash $($arguments -join ' ')" +& $bash @arguments +exit $LASTEXITCODE \ No newline at end of file diff --git a/icu4c/packaging/build-android.sh b/icu4c/packaging/build-android.sh new file mode 100644 index 000000000000..ecd4e419cbd2 --- /dev/null +++ b/icu4c/packaging/build-android.sh @@ -0,0 +1,218 @@ +#!/usr/bin/env bash +# Copyright (c) 2026 SIL Global +# Cross-compile this checkout's ICU4C sources for Android APK packaging. +# Adapted from https://github.com/patrickgold/icu4c-android (Apache 2.0). + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ICU4C_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +ICU_SRC_DIR="$ICU4C_DIR/source" +OUTPUT_DIR="$ICU4C_DIR/out/android-icu" +HOST_BUILD_DIR="$OUTPUT_DIR/build/host" +ARCHS="${ARCHS:-x86_64}" +API_LEVEL="${API_LEVEL:-21}" + +usage() { + cat < is used + ARCHS Default ABI list when --arch is omitted + API_LEVEL Default API level when --api is omitted +EOF +} + +log() { echo "[build-android] $*"; } +die() { echo "[build-android] ERROR: $*" >&2; exit 1; } + +host_platform() { + case "$(uname -s)" in + Linux) echo Linux ;; + Darwin) echo MacOSX ;; + MINGW*|MSYS*|CYGWIN*) echo MinGW ;; + *) die "Unsupported host OS: $(uname -s)" ;; + esac +} + +detect_ndk_host_tag() { + case "$(uname -s)" in + Linux) echo "linux-$(uname -m)" ;; + Darwin) echo "darwin-$(uname -m)" ;; + MINGW*|MSYS*|CYGWIN*) echo windows-x86_64 ;; + *) die "Unsupported host OS for NDK toolchain: $(uname -s)" ;; + esac +} + +resolve_ndk_toolchain_tag() { + local ndk="$1" preferred host_os + preferred="$(detect_ndk_host_tag)" + if [[ -d "$ndk/toolchains/llvm/prebuilt/$preferred" ]]; then + echo "$preferred" + return + fi + host_os="$(uname -s)" + if [[ "$host_os" == Linux && -d "$ndk/toolchains/llvm/prebuilt/windows-x86_64" ]]; then + die "NDK at $ndk has the Windows toolchain. WSL/Linux requires a Linux NDK installation; set ANDROID_NDK_HOME to it." + fi + if [[ "$host_os" =~ ^(MINGW|MSYS|CYGWIN) && -d "$ndk/toolchains/llvm/prebuilt/linux-x86_64" ]]; then + die "NDK at $ndk has the Linux toolchain. Git Bash requires a Windows NDK installation." + fi + die "NDK toolchain not found under $ndk/toolchains/llvm/prebuilt/ (expected $preferred)" +} + +resolve_ndk() { + if [[ -n "${ANDROID_NDK_HOME:-}" && -d "$ANDROID_NDK_HOME" ]]; then + echo "$ANDROID_NDK_HOME" + return + fi + if [[ -n "${ANDROID_HOME:-}" && -d "$ANDROID_HOME/ndk" ]]; then + local latest + latest="$(ls -d "$ANDROID_HOME/ndk/"* 2>/dev/null | sort -V | tail -1)" + [[ -n "$latest" ]] && echo "$latest" && return + fi + die "Android NDK not found. Set ANDROID_NDK_HOME or ANDROID_HOME." +} + +arch_to_target() { + case "$1" in + x86_64) echo x86_64-linux-android ;; + arm64-v8a) echo aarch64-linux-android ;; + *) die "Unsupported ABI '$1'. Supported ABIs: x86_64, arm64-v8a" ;; + esac +} + +icu_major() { + sed -n 's/^#define U_ICU_VERSION_MAJOR_NUM \([0-9][0-9]*\)$/\1/p' \ + "$ICU_SRC_DIR/common/unicode/uvernum.h" | head -1 +} + +parallel_jobs() { + nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4 +} + +copy_ndk_cpp_shared() { + local ndk="$1" host_tag="$2" target="$3" install_dir="$4" + local cxx_lib="$ndk/toolchains/llvm/prebuilt/$host_tag/sysroot/usr/lib/$target/libc++_shared.so" + [[ -f "$cxx_lib" ]] || die "NDK libc++ not found: $cxx_lib" + cp -f "$cxx_lib" "$install_dir/" +} + +install_android_apk_libs() { + local install_dir="$1" major="$2" lib src + for lib in icuuc icui18n icudata; do + src="$(find "$install_dir" -maxdepth 1 -type f -name "lib${lib}.so.*" -print | sort -V | tail -1)" + [[ -n "$src" ]] || die "Missing lib${lib} in $install_dir" + cp -f "$src" "$install_dir/lib${lib}.so" + cp -f "$src" "$install_dir/lib${lib}.so.$major" + done +} + +build_host_icu() { + if [[ -f "$HOST_BUILD_DIR/icu_build/lib/libicuuc.so" || -f "$HOST_BUILD_DIR/icu_build/lib/libicuuc.a" ]]; then + log "Host ICU build already present" + return + fi + log "Building host ICU tools from $ICU_SRC_DIR" + mkdir -p "$HOST_BUILD_DIR" + pushd "$HOST_BUILD_DIR" >/dev/null + "$ICU_SRC_DIR/runConfigureICU" "$(host_platform)" \ + --enable-static=no --enable-shared=yes --enable-tests=no \ + --enable-samples=no --enable-extras=no --enable-draft=yes \ + --prefix="$HOST_BUILD_DIR/icu_build" + make -j"$(parallel_jobs)" + popd >/dev/null +} + +install_icu_data_file() { + local major="$1" dat_name="icudt${major}l.dat" dat_src + dat_src="$(find "$HOST_BUILD_DIR/data/out" -type f -name "$dat_name" -print -quit)" + [[ -n "$dat_src" ]] || die "Missing generated ICU data file $dat_name" + cp -f "$dat_src" "$OUTPUT_DIR/$dat_name" +} + +build_android_arch() { + local abi="$1" ndk="$2" host_tag="$3" major="$4" + local target build_dir install_dir toolchain + target="$(arch_to_target "$abi")" + build_dir="$OUTPUT_DIR/build/android/$abi" + install_dir="$OUTPUT_DIR/$abi" + toolchain="$ndk/toolchains/llvm/prebuilt/$host_tag" + mkdir -p "$build_dir" "$install_dir" + log "Cross-compiling ICU for $abi ($target, API $API_LEVEL)" + pushd "$build_dir" >/dev/null + export PATH="$toolchain/bin:$PATH" + export CC="$toolchain/bin/${target}${API_LEVEL}-clang" + export CXX="$toolchain/bin/${target}${API_LEVEL}-clang++" + export AR="$toolchain/bin/llvm-ar" + export RANLIB="$toolchain/bin/llvm-ranlib" + export LDFLAGS="-Wl,--gc-sections -Wl,-z,max-page-size=16384" + "$ICU_SRC_DIR/configure" \ + --with-cross-build="$HOST_BUILD_DIR" --host="$target" \ + --enable-static=no --enable-shared=yes --enable-tests=no \ + --enable-samples=no --enable-extras=no --enable-draft=yes \ + --with-data-packaging=dll + make -j"$(parallel_jobs)" + cp -f "$build_dir/lib"/libicu*.so* "$install_dir/" + [[ -d "$build_dir/stubdata" ]] && cp -f "$build_dir/stubdata"/libicudata*.so* "$install_dir/" || true + copy_ndk_cpp_shared "$ndk" "$host_tag" "$target" "$install_dir" + install_android_apk_libs "$install_dir" "$major" + popd >/dev/null +} + +CLEAN=no +CLEAN_ARCHS="" +for arg in "$@"; do + case "$arg" in + --help|-h) usage; exit 0 ;; + --clean) CLEAN=yes ;; + --clean-arch=*) CLEAN_ARCHS="${arg#*=}" ;; + --arch=*) ARCHS="${arg#*=}" ;; + --api=*) API_LEVEL="${arg#*=}" ;; + *) die "Unknown option: $arg" ;; + esac +done + +[[ "$API_LEVEL" =~ ^[0-9]+$ ]] || die "API level must be a positive integer" +[[ -f "$ICU_SRC_DIR/configure" ]] || die "Local ICU source is missing: $ICU_SRC_DIR" +MAJOR="$(icu_major)" +[[ -n "$MAJOR" ]] || die "Could not determine ICU major version from uvernum.h" + +if [[ "$CLEAN" == yes ]]; then + rm -rf "$OUTPUT_DIR" + log "Cleaned $OUTPUT_DIR" + exit 0 +fi +if [[ -n "$CLEAN_ARCHS" ]]; then + IFS=',' read -ra CLEAN_ARCH_LIST <<< "$CLEAN_ARCHS" + for abi in "${CLEAN_ARCH_LIST[@]}"; do + abi="$(echo "$abi" | xargs)"; [[ -z "$abi" ]] && continue + arch_to_target "$abi" >/dev/null + rm -rf "$OUTPUT_DIR/build/android/$abi" "$OUTPUT_DIR/$abi" + log "Cleaned $abi" + done +fi + +for cmd in make sed find sort; do command -v "$cmd" >/dev/null || die "Required command not found: $cmd"; done +NDK="$(resolve_ndk)" +HOST_TAG="$(resolve_ndk_toolchain_tag "$NDK")" +log "Using local ICU $MAJOR.x sources, NDK $NDK, ABIs $ARCHS" +build_host_icu +install_icu_data_file "$MAJOR" +IFS=',' read -ra ARCH_LIST <<< "$ARCHS" +for abi in "${ARCH_LIST[@]}"; do + abi="$(echo "$abi" | xargs)"; [[ -z "$abi" ]] && continue + build_android_arch "$abi" "$NDK" "$HOST_TAG" "$MAJOR" +done +log "Done. APK-ready libraries are in $OUTPUT_DIR/{abi}/" \ No newline at end of file From 9623fd22e944a5a78feeaf4541b0038d21cd0982 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Mon, 3 Aug 2026 14:43:25 +0700 Subject: [PATCH 02/12] Fix unbound major in install_icu_data_file under set -u. Bash unsets every name in a multi-assignment local before applying values, so referencing major in the same statement failed after the host build. Co-authored-by: Cursor --- icu4c/packaging/build-android.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/icu4c/packaging/build-android.sh b/icu4c/packaging/build-android.sh index ecd4e419cbd2..942c6e926995 100644 --- a/icu4c/packaging/build-android.sh +++ b/icu4c/packaging/build-android.sh @@ -136,7 +136,10 @@ build_host_icu() { } install_icu_data_file() { - local major="$1" dat_name="icudt${major}l.dat" dat_src + # Declare separately: bash unsets all names in one `local` before assigning. + local major="$1" + local dat_name="icudt${major}l.dat" + local dat_src dat_src="$(find "$HOST_BUILD_DIR/data/out" -type f -name "$dat_name" -print -quit)" [[ -n "$dat_src" ]] || die "Missing generated ICU data file $dat_name" cp -f "$dat_src" "$OUTPUT_DIR/$dat_name" From 907a4fbf9ed9b24cafd985343eca77a983ef506a Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Mon, 3 Aug 2026 15:32:57 +0700 Subject: [PATCH 03/12] Use shallow sparse checkout for android-build CI. Only packaging and ICU source are needed for the cross-compile, so skip full history and unrelated trees. Co-authored-by: Cursor --- .github/workflows/fw_icu4c_ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/fw_icu4c_ci.yml b/.github/workflows/fw_icu4c_ci.yml index 7fa7119940d9..85d956174310 100644 --- a/.github/workflows/fw_icu4c_ci.yml +++ b/.github/workflows/fw_icu4c_ci.yml @@ -131,7 +131,10 @@ jobs: - name: Checkout uses: actions/checkout@v7 with: - fetch-depth: 0 + fetch-depth: 1 + sparse-checkout: | + icu4c/packaging + icu4c/source - name: Set up Android SDK command-line tools uses: android-actions/setup-android@v4 From 90d3b8f826a8506be3f52175df6251301a49234e Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Mon, 3 Aug 2026 15:52:42 +0700 Subject: [PATCH 04/12] Add Icu4c.Android.Fw.Lib NuGet packaging and CI publish. Packs Android ICU natives and data for MAUI consumers, uploads the nupkg on CI, and publishes to nuget.org on fw pushes. Co-authored-by: Cursor --- .github/workflows/fw_icu4c_ci.yml | 32 +++++ .../android/build/Icu4c.Android.Fw.Lib.props | 9 ++ .../build/Icu4c.Android.Fw.Lib.targets | 18 +++ .../assets/android/icu-android-fw-lib.nuspec | 31 +++++ icu4c/packaging/README.android.md | 39 ++++-- icu4c/packaging/pack-android-nuget.sh | 119 ++++++++++++++++++ 6 files changed, 240 insertions(+), 8 deletions(-) create mode 100644 icu4c/nugetpackage/assets/android/build/Icu4c.Android.Fw.Lib.props create mode 100644 icu4c/nugetpackage/assets/android/build/Icu4c.Android.Fw.Lib.targets create mode 100644 icu4c/nugetpackage/assets/android/icu-android-fw-lib.nuspec create mode 100644 icu4c/packaging/pack-android-nuget.sh diff --git a/.github/workflows/fw_icu4c_ci.yml b/.github/workflows/fw_icu4c_ci.yml index 85d956174310..abdf7949efa0 100644 --- a/.github/workflows/fw_icu4c_ci.yml +++ b/.github/workflows/fw_icu4c_ci.yml @@ -126,19 +126,28 @@ jobs: make -j2 check android-build: + needs: set-version-number runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v7 with: fetch-depth: 1 + sparse-checkout-cone-mode: false sparse-checkout: | icu4c/packaging icu4c/source + icu4c/nugetpackage/assets/android + icu4c/LICENSE - name: Set up Android SDK command-line tools uses: android-actions/setup-android@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v6 + with: + dotnet-version: 8.0.x + - name: Install Android NDK run: | sdkmanager --install 'ndk;27.0.12077973' @@ -166,6 +175,29 @@ jobs: done done + - name: Pack Android NuGet package + run: | + set -xeuo pipefail + bash icu4c/packaging/pack-android-nuget.sh \ + --version="${{ needs.set-version-number.outputs.version }}" \ + --output=icu4c/nugetpackage + + - name: Upload Android NuGet package + uses: actions/upload-artifact@v4 + with: + name: android-nuget + path: icu4c/nugetpackage/Icu4c.Android.Fw.Lib.*.nupkg + if-no-files-found: error + + - name: Publish Android NuGet package + if: github.event_name == 'push' + run: | + set -xeuo pipefail + dotnet nuget push icu4c/nugetpackage/Icu4c.Android.Fw.Lib.*.nupkg \ + --source 'https://api.nuget.org/v3/index.json' \ + --api-key '${{ secrets.ICU_NUGET_API_KEY }}' \ + --skip-duplicate + debian-packaging: needs: set-version-number runs-on: ubuntu-latest diff --git a/icu4c/nugetpackage/assets/android/build/Icu4c.Android.Fw.Lib.props b/icu4c/nugetpackage/assets/android/build/Icu4c.Android.Fw.Lib.props new file mode 100644 index 000000000000..57dfbba6fdd2 --- /dev/null +++ b/icu4c/nugetpackage/assets/android/build/Icu4c.Android.Fw.Lib.props @@ -0,0 +1,9 @@ + + + + 70 + $(MSBuildThisFileDirectory) + $(IcuFwAndroidPackageDir)native\ + $(IcuFwAndroidPackageDir)assets\ + + diff --git a/icu4c/nugetpackage/assets/android/build/Icu4c.Android.Fw.Lib.targets b/icu4c/nugetpackage/assets/android/build/Icu4c.Android.Fw.Lib.targets new file mode 100644 index 000000000000..98faa732b4fd --- /dev/null +++ b/icu4c/nugetpackage/assets/android/build/Icu4c.Android.Fw.Lib.targets @@ -0,0 +1,18 @@ + + + + + + + + %(Filename)%(Extension) + + + diff --git a/icu4c/nugetpackage/assets/android/icu-android-fw-lib.nuspec b/icu4c/nugetpackage/assets/android/icu-android-fw-lib.nuspec new file mode 100644 index 000000000000..839eda96956d --- /dev/null +++ b/icu4c/nugetpackage/assets/android/icu-android-fw-lib.nuspec @@ -0,0 +1,31 @@ + + + + Icu4c.Android.Fw.Lib + $version$ + SIL International + Copyright (c) 2016-2026 SIL International + LICENSE + https://github.com/sillsdev/icu + false + native android maui + ICU - International Components for Unicode (Android) + ICU - International Components for Unicode + +This package contains the FieldWorks version of ICU4C shared libraries for Android +(x86_64 and arm64-v8a), including libc++_shared.so and the ICU data file for MAUI / +.NET Android apps. + +The major version number of the nuget package corresponds to the ICU release. + + en-US + +70.1.0 - Initial Android package for MAUI / .NET Android (x86_64, arm64-v8a) + + + + + + + + diff --git a/icu4c/packaging/README.android.md b/icu4c/packaging/README.android.md index 6c7e36e1911b..4b5ff1ded30d 100644 --- a/icu4c/packaging/README.android.md +++ b/icu4c/packaging/README.android.md @@ -10,6 +10,9 @@ toolchain to cross-compile each requested ABI. It validates native-library output only; it does not build or run an Android application, emulator, or device test. +CI packs the build into the `Icu4c.Android.Fw.Lib` NuGet package for MAUI / +.NET Android consumers. + ## Prerequisites On Linux or macOS install a POSIX shell environment with `bash`, `make`, `sed`, @@ -58,7 +61,7 @@ Supported ABI values are `x86_64` and `arm64-v8a`. Use `--help` for the full command reference. The generated `icu4c/out/` directory is already ignored by this repository. -## Output and Android integration +## Output layout For the checked-out ICU `70.1`, the output layout is: @@ -84,12 +87,32 @@ they change automatically when this branch advances to a different ICU major. The unversioned `.so` files are the names Android APK packaging expects; the versioned copies remain alongside them for native dependency compatibility. -Android consumers are responsible for embedding `icudt70l.dat` (or the -locally-derived replacement after an ICU update) as an app data asset and for -packaging the ABI-specific, unversioned `libc++_shared.so`, `libicuuc.so`, -`libicui18n.so`, and `libicudata.so` files in their APK/AAB. This repository -only produces the native inputs; it does not publish a package or validate an -application's integration. +## NuGet package (MAUI / .NET Android) + +CI packs unversioned ABI libraries plus `icudt*l.dat` into +[`Icu4c.Android.Fw.Lib`](https://www.nuget.org/packages/Icu4c.Android.Fw.Lib). +The major version of the package matches the ICU release (same scheme as +`Icu4c.Win.Fw.Lib`). + +```xml + + + +``` + +The package’s MSBuild targets add `AndroidNativeLibrary` entries for +`x86_64` and `arm64-v8a` and embed `icudt70l.dat` as an `AndroidAsset`. +Consumers do not need manual `AndroidNativeLibrary` / `AndroidAsset` +ItemGroups. + +Pack a local `.nupkg` after a successful Android build: + +```bash +bash ./icu4c/packaging/pack-android-nuget.sh --version=70.1.0 +``` + +Keep the consumer’s ICU major (for example `AndroidIcuBootstrap` in +icu-dotnet) aligned with the referenced package major. ## Troubleshooting @@ -98,4 +121,4 @@ application's integration. executes the script: Linux for Linux/WSL, Windows for Git Bash, or macOS for macOS. - Use `--clean` after changing host compilers, NDK versions, or ICU source - configuration to ensure host tools and cross-build output are recreated. \ No newline at end of file + configuration to ensure host tools and cross-build output are recreated. diff --git a/icu4c/packaging/pack-android-nuget.sh b/icu4c/packaging/pack-android-nuget.sh new file mode 100644 index 000000000000..a5a118882285 --- /dev/null +++ b/icu4c/packaging/pack-android-nuget.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# Copyright (c) 2026 SIL Global +# Stage Android ICU build output and pack Icu4c.Android.Fw.Lib. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ICU4C_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +INPUT_DIR="${INPUT_DIR:-$ICU4C_DIR/out/android-icu}" +ASSETS_DIR="$ICU4C_DIR/nugetpackage/assets/android" +OUTPUT_DIR="${OUTPUT_DIR:-$ICU4C_DIR/nugetpackage}" +PKG_VERSION="" +ABIS="${ABIS:-x86_64,arm64-v8a}" + +usage() { + cat <&2; exit 1; } + +for arg in "$@"; do + case "$arg" in + --help|-h) usage; exit 0 ;; + --version=*) PKG_VERSION="${arg#*=}" ;; + --input=*) INPUT_DIR="${arg#*=}" ;; + --output=*) OUTPUT_DIR="${arg#*=}" ;; + --abis=*) ABIS="${arg#*=}" ;; + *) die "Unknown option: $arg" ;; + esac +done + +[[ -n "$PKG_VERSION" ]] || die "Missing --version" +[[ -d "$INPUT_DIR" ]] || die "Input directory not found: $INPUT_DIR" +[[ -f "$ASSETS_DIR/build/Icu4c.Android.Fw.Lib.targets" ]] || die "Missing targets under $ASSETS_DIR/build" +[[ -f "$ICU4C_DIR/LICENSE" ]] || die "Missing LICENSE at $ICU4C_DIR/LICENSE" + +command -v dotnet >/dev/null || die "dotnet SDK is required to pack the NuGet package" + +STAGE_DIR="$(mktemp -d "${TMPDIR:-/tmp}/icu-android-nuget.XXXXXX")" +cleanup() { rm -rf "$STAGE_DIR"; } +trap cleanup EXIT + +mkdir -p "$STAGE_DIR/build/native" "$STAGE_DIR/build/assets" +cp -f "$ICU4C_DIR/LICENSE" "$STAGE_DIR/" +cp -f "$ASSETS_DIR/build/Icu4c.Android.Fw.Lib.props" "$STAGE_DIR/build/" +cp -f "$ASSETS_DIR/build/Icu4c.Android.Fw.Lib.targets" "$STAGE_DIR/build/" + +dat_files=("$INPUT_DIR"/icudt*l.dat) +[[ -f "${dat_files[0]}" ]] || die "Missing icudt*l.dat under $INPUT_DIR" +cp -f "${dat_files[@]}" "$STAGE_DIR/build/assets/" + +dat_base="$(basename "${dat_files[0]}")" +[[ "$dat_base" =~ ^icudt([0-9]+)l\.dat$ ]] || die "Unexpected data file name: $dat_base" +ICU_MAJOR="${BASH_REMATCH[1]}" +sed -i.bak "s/[0-9][0-9]*<\/IcuFwAndroidMajorVersion>/${ICU_MAJOR}<\/IcuFwAndroidMajorVersion>/" \ + "$STAGE_DIR/build/Icu4c.Android.Fw.Lib.props" +rm -f "$STAGE_DIR/build/Icu4c.Android.Fw.Lib.props.bak" + +IFS=',' read -ra ABI_LIST <<< "$ABIS" +for abi in "${ABI_LIST[@]}"; do + abi="$(echo "$abi" | xargs)" + [[ -z "$abi" ]] && continue + src="$INPUT_DIR/$abi" + [[ -d "$src" ]] || die "Missing ABI directory: $src" + dest="$STAGE_DIR/build/native/$abi" + mkdir -p "$dest" + for library in libc++_shared.so libicuuc.so libicui18n.so libicudata.so; do + [[ -f "$src/$library" ]] || die "Missing $src/$library" + cp -f "$src/$library" "$dest/" + done +done + +# SDK-style pack project so Linux CI can pack without mono/nuget.exe. +# Metadata mirrors icu-android-fw-lib.nuspec. +cat > "$STAGE_DIR/Icu4c.Android.Fw.Lib.csproj" < + + netstandard2.0 + true + false + true + Icu4c.Android.Fw.Lib + $PKG_VERSION + ICU - International Components for Unicode (Android) + SIL International + Copyright (c) 2016-2026 SIL International + https://github.com/sillsdev/icu + LICENSE + native;android;maui + FieldWorks ICU4C shared libraries for Android (x86_64, arm64-v8a) for MAUI / .NET Android apps. The major version number corresponds to the ICU release. + 70.1.0 - Initial Android package for MAUI / .NET Android (x86_64, arm64-v8a) + false + + + + + + +EOF + +mkdir -p "$OUTPUT_DIR" +log "Packing Icu4c.Android.Fw.Lib $PKG_VERSION from $INPUT_DIR" +dotnet pack "$STAGE_DIR/Icu4c.Android.Fw.Lib.csproj" \ + -o "$OUTPUT_DIR" \ + --nologo + +nupkg="$OUTPUT_DIR/Icu4c.Android.Fw.Lib.$PKG_VERSION.nupkg" +[[ -f "$nupkg" ]] || die "Expected package not found: $nupkg" +log "Created $nupkg" From 8687032d70f7f874435a977343d1b833312e14c1 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Mon, 3 Aug 2026 15:56:50 +0700 Subject: [PATCH 05/12] Bump CI actions past deprecated cache and artifact versions. GitHub rejects upload-artifact@v2 and cache@v2; also refresh checkout, setup-msbuild, and GITHUB_OUTPUT. Co-authored-by: Cursor --- .github/workflows/fw_icu4c_ci.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/fw_icu4c_ci.yml b/.github/workflows/fw_icu4c_ci.yml index abdf7949efa0..7b38fce790a5 100644 --- a/.github/workflows/fw_icu4c_ci.yml +++ b/.github/workflows/fw_icu4c_ci.yml @@ -23,7 +23,7 @@ jobs: version: ${{ steps.calc-version.outputs.result }} steps: - name: Checkout - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v7 with: fetch-depth: 0 @@ -33,7 +33,7 @@ jobs: major=$(grep -oP '.*U_ICU_VERSION_MAJOR_NUM \K.*' icu4c/source/common/unicode/uvernum.h) minor=$(grep -oP '.*U_ICU_VERSION_MINOR_NUM \K.*' icu4c/source/common/unicode/uvernum.h) build=${{ github.run_number }} - echo "::set-output name=result::$major.$minor.$build" + echo "result=$major.$minor.$build" >> "$GITHUB_OUTPUT" win-build-test-package: needs: set-version-number @@ -47,13 +47,13 @@ jobs: echo "Version ${{ needs.set-version-number.outputs.version }}" - name: Checkout - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v7 with: fetch-depth: 0 # Add MSBuild to the PATH: https://github.com/microsoft/setup-msbuild - name: Setup MSBuild.exe (Windows OS) - uses: microsoft/setup-msbuild@v1.1 + uses: microsoft/setup-msbuild@v3 - name: Build ICU 32 bit working-directory: icu4c @@ -89,7 +89,7 @@ jobs: run: msbuild nugetpackage/build/icu4c.proj /t:BuildPackage /p:PkgVersion=${{ needs.set-version-number.outputs.version }} - name: Upload Artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v7 with: name: packages path: ./**/*.nupkg @@ -107,7 +107,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v7 with: fetch-depth: 0 @@ -183,7 +183,7 @@ jobs: --output=icu4c/nugetpackage - name: Upload Android NuGet package - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: android-nuget path: icu4c/nugetpackage/Icu4c.Android.Fw.Lib.*.nupkg @@ -203,7 +203,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v7 - name: Checkout ci-build-tools run: | @@ -217,7 +217,7 @@ jobs: # TODO more will need done to do this successfully since some files are root readable only - name: Cache - uses: actions/cache@v2 + uses: actions/cache@v6 with: path: /var/lib/schroot/chroots key: icu-debian-package-${{ github.ref_name }} @@ -252,7 +252,7 @@ jobs: --no-upload' - name: Upload Artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v7 with: name: linux-packages path: | From 7a4597b5f3833c35ceb63ab43e8e42b2c041a33f Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 7 Aug 2026 14:32:12 +0700 Subject: [PATCH 06/12] Fix Android packaging SONAME, stub data, and Windows arch args. Ship archive-packaged stub libicudata with icudt*.dat, link unversioned SONAMEs for APK loading, and correct the PowerShell multi-ABI wrapper plus host/CI cache behavior. Co-authored-by: Cursor --- .github/workflows/fw_icu4c_ci.yml | 4 +- .../android/build/Icu4c.Android.Fw.Lib.props | 7 +- .../build/Icu4c.Android.Fw.Lib.targets | 11 +-- .../assets/android/icu-android-fw-lib.nuspec | 2 +- icu4c/packaging/README.android.md | 17 ++-- icu4c/packaging/build-android.ps1 | 32 +++++--- icu4c/packaging/build-android.sh | 78 ++++++++++++------- icu4c/packaging/pack-android-nuget.sh | 6 +- 8 files changed, 103 insertions(+), 54 deletions(-) diff --git a/.github/workflows/fw_icu4c_ci.yml b/.github/workflows/fw_icu4c_ci.yml index 129a2fdedf8e..bedc7ea627e5 100644 --- a/.github/workflows/fw_icu4c_ci.yml +++ b/.github/workflows/fw_icu4c_ci.yml @@ -157,12 +157,12 @@ jobs: uses: actions/cache@v6 with: path: icu4c/out/android-icu - key: android-icu-${{ runner.os }}-${{ hashFiles('icu4c/packaging/build-android.sh', 'icu4c/source/common/unicode/uvernum.h') }} + key: android-icu-${{ runner.os }}-${{ hashFiles('icu4c/packaging/build-android.sh', 'icu4c/packaging/pack-android-nuget.sh', 'icu4c/source/common/unicode/uvernum.h', 'icu4c/source/configure') }} - name: Build Android ICU libraries run: | set -xeo pipefail - bash icu4c/packaging/build-android.sh --clean-arch=x86_64 --arch=x86_64,arm64-v8a + bash icu4c/packaging/build-android.sh --arch=x86_64,arm64-v8a - name: Verify Android ICU output contract run: | diff --git a/icu4c/nugetpackage/assets/android/build/Icu4c.Android.Fw.Lib.props b/icu4c/nugetpackage/assets/android/build/Icu4c.Android.Fw.Lib.props index 57dfbba6fdd2..e875b61c3dda 100644 --- a/icu4c/nugetpackage/assets/android/build/Icu4c.Android.Fw.Lib.props +++ b/icu4c/nugetpackage/assets/android/build/Icu4c.Android.Fw.Lib.props @@ -1,9 +1,10 @@ - + 70 $(MSBuildThisFileDirectory) - $(IcuFwAndroidPackageDir)native\ - $(IcuFwAndroidPackageDir)assets\ + + $(IcuFwAndroidPackageDir)native/ + $(IcuFwAndroidPackageDir)assets/ diff --git a/icu4c/nugetpackage/assets/android/build/Icu4c.Android.Fw.Lib.targets b/icu4c/nugetpackage/assets/android/build/Icu4c.Android.Fw.Lib.targets index 98faa732b4fd..e4b8bcb7073f 100644 --- a/icu4c/nugetpackage/assets/android/build/Icu4c.Android.Fw.Lib.targets +++ b/icu4c/nugetpackage/assets/android/build/Icu4c.Android.Fw.Lib.targets @@ -5,13 +5,14 @@ --> + Include="$(IcuFwAndroidNativeDir)x86_64/*.so" + Condition="Exists('$(IcuFwAndroidNativeDir)x86_64/libicuuc.so')" /> + Include="$(IcuFwAndroidNativeDir)arm64-v8a/*.so" + Condition="Exists('$(IcuFwAndroidNativeDir)arm64-v8a/libicuuc.so')" /> - + %(Filename)%(Extension) diff --git a/icu4c/nugetpackage/assets/android/icu-android-fw-lib.nuspec b/icu4c/nugetpackage/assets/android/icu-android-fw-lib.nuspec index 839eda96956d..361c2e4be40b 100644 --- a/icu4c/nugetpackage/assets/android/icu-android-fw-lib.nuspec +++ b/icu4c/nugetpackage/assets/android/icu-android-fw-lib.nuspec @@ -20,7 +20,7 @@ The major version number of the nuget package corresponds to the ICU release. en-US -70.1.0 - Initial Android package for MAUI / .NET Android (x86_64, arm64-v8a) +$version$ - FieldWorks ICU Android natives (x86_64, arm64-v8a) plus icudt*l.dat diff --git a/icu4c/packaging/README.android.md b/icu4c/packaging/README.android.md index 4b5ff1ded30d..371d363641c4 100644 --- a/icu4c/packaging/README.android.md +++ b/icu4c/packaging/README.android.md @@ -73,19 +73,22 @@ icu4c/out/android-icu/ libicuuc.so libicui18n.so libicudata.so - libicu*.so.70[.1] arm64-v8a/ libc++_shared.so libicuuc.so libicui18n.so libicudata.so - libicu*.so.70[.1] ``` +Cross builds use `--with-data-packaging=archive`, so `libicudata.so` is the +**stub** data library. Locale data lives in `icudt*l.dat`. Consumers (for +example `AndroidIcuBootstrap` in icu-dotnet) must load that file via +`udata_setCommonData` (or equivalent) before using ICU. + +Shared libraries are linked with **unversioned** SONAMEs (`libicuuc.so`, and so +on) so Android APK packaging of those file names alone satisfies `DT_NEEDED`. The library major and data-file name are derived from the local ICU source, so they change automatically when this branch advances to a different ICU major. -The unversioned `.so` files are the names Android APK packaging expects; the -versioned copies remain alongside them for native dependency compatibility. ## NuGet package (MAUI / .NET Android) @@ -103,7 +106,9 @@ The major version of the package matches the ICU release (same scheme as The package’s MSBuild targets add `AndroidNativeLibrary` entries for `x86_64` and `arm64-v8a` and embed `icudt70l.dat` as an `AndroidAsset`. Consumers do not need manual `AndroidNativeLibrary` / `AndroidAsset` -ItemGroups. +ItemGroups. The props file also stamps `IcuFwAndroidMajorVersion` for +consumers that need the ICU major (for example aligning +`AndroidIcuBootstrap`). Pack a local `.nupkg` after a successful Android build: @@ -122,3 +127,5 @@ icu-dotnet) aligned with the referenced package major. macOS. - Use `--clean` after changing host compilers, NDK versions, or ICU source configuration to ensure host tools and cross-build output are recreated. +- ICU operations that need locale data will fail until the app loads + `icudt*l.dat` into ICU (stub `libicudata.so` alone is not enough). diff --git a/icu4c/packaging/build-android.ps1 b/icu4c/packaging/build-android.ps1 index c3bf8cc9fd62..e05bcc096f15 100644 --- a/icu4c/packaging/build-android.ps1 +++ b/icu4c/packaging/build-android.ps1 @@ -9,10 +9,10 @@ build-android.sh from WSL and configure a Linux NDK there instead. #> [CmdletBinding()] param( - [string] $Arch = 'x86_64', + [string[]] $Arch = @('x86_64'), [int] $ApiLevel = 21, [switch] $Clean, - [string] $CleanArch, + [string[]] $CleanArch, [switch] $Help ) @@ -22,11 +22,15 @@ $bashScript = Join-Path $PSScriptRoot 'build-android.sh' function Find-GitBash { $candidates = @( 'C:\Program Files\Git\bin\bash.exe', - 'C:\Program Files (x86)\Git\bin\bash.exe', - (Join-Path ${env:ProgramFiles} 'Git\bin\bash.exe'), - (Join-Path ${env:ProgramFiles(x86)} 'Git\bin\bash.exe') - ) | Where-Object { $_ -and (Test-Path $_) } - $candidates | Select-Object -First 1 + 'C:\Program Files (x86)\Git\bin\bash.exe' + ) + if ($env:ProgramFiles) { + $candidates += (Join-Path $env:ProgramFiles 'Git\bin\bash.exe') + } + if (${env:ProgramFiles(x86)}) { + $candidates += (Join-Path ${env:ProgramFiles(x86)} 'Git\bin\bash.exe') + } + $candidates | Where-Object { $_ -and (Test-Path $_) } | Select-Object -First 1 } $bash = Find-GitBash @@ -34,10 +38,18 @@ if (-not $bash) { throw 'Git Bash was not found. Install Git for Windows, then re-run this command. For WSL, run ./icu4c/packaging/build-android.sh inside WSL with a Linux NDK installation.' } -$arguments = @($bashScript, "--arch=$Arch", "--api=$ApiLevel") +$archList = ($Arch | ForEach-Object { "$_".Trim() } | Where-Object { $_ }) -join ',' +if (-not $archList) { + throw 'At least one -Arch value is required (x86_64 and/or arm64-v8a).' +} + +$arguments = @($bashScript, "--arch=$archList", "--api=$ApiLevel") if ($Clean) { $arguments += '--clean' } -if ($CleanArch) { $arguments += "--clean-arch=$CleanArch" } +if ($CleanArch -and $CleanArch.Count -gt 0) { + $cleanList = ($CleanArch | ForEach-Object { "$_".Trim() } | Where-Object { $_ }) -join ',' + if ($cleanList) { $arguments += "--clean-arch=$cleanList" } +} if ($Help) { $arguments = @($bashScript, '--help') } Write-Host "Running: $bash $($arguments -join ' ')" & $bash @arguments -exit $LASTEXITCODE \ No newline at end of file +exit $LASTEXITCODE diff --git a/icu4c/packaging/build-android.sh b/icu4c/packaging/build-android.sh index 942c6e926995..4a7f916a58fc 100644 --- a/icu4c/packaging/build-android.sh +++ b/icu4c/packaging/build-android.sh @@ -19,6 +19,11 @@ Usage: $0 [options] Build the checked-out ICU4C source for Android. No ICU release is downloaded. +Data packaging uses --with-data-packaging=archive: libicudata.so is the stub +library, and locale data is the separate icudt*l.dat file (for AndroidAsset / +udata_setCommonData). Shared libraries are linked with unversioned SONAMEs so +APK packaging of lib*.so alone works with the Android loader. + Options: --arch=LIST Comma-separated ABIs: x86_64, arm64-v8a (default: x86_64) --api=LEVEL Minimum Android API (default: 21) @@ -109,29 +114,33 @@ copy_ndk_cpp_shared() { cp -f "$cxx_lib" "$install_dir/" } +# Copy versioned ICU shared objects to the unversioned lib*.so names Android +# packages. SONAMEs are already unversioned (see build_android_arch make override). install_android_apk_libs() { - local install_dir="$1" major="$2" lib src + local install_dir="$1" lib src for lib in icuuc icui18n icudata; do - src="$(find "$install_dir" -maxdepth 1 -type f -name "lib${lib}.so.*" -print | sort -V | tail -1)" + src="$(find "$install_dir" -maxdepth 1 -type f -name "lib${lib}.so.*" -print 2>/dev/null | sort -V | tail -1 || true)" [[ -n "$src" ]] || die "Missing lib${lib} in $install_dir" cp -f "$src" "$install_dir/lib${lib}.so" - cp -f "$src" "$install_dir/lib${lib}.so.$major" done } build_host_icu() { - if [[ -f "$HOST_BUILD_DIR/icu_build/lib/libicuuc.so" || -f "$HOST_BUILD_DIR/icu_build/lib/libicuuc.a" ]]; then - log "Host ICU build already present" - return - fi - log "Building host ICU tools from $ICU_SRC_DIR" + local host_plat mkdir -p "$HOST_BUILD_DIR" pushd "$HOST_BUILD_DIR" >/dev/null - "$ICU_SRC_DIR/runConfigureICU" "$(host_platform)" \ - --enable-static=no --enable-shared=yes --enable-tests=no \ - --enable-samples=no --enable-extras=no --enable-draft=yes \ - --prefix="$HOST_BUILD_DIR/icu_build" + if [[ ! -f config/icucross.mk ]]; then + host_plat="$(host_platform)" + log "Configuring host ICU tools from $ICU_SRC_DIR" + "$ICU_SRC_DIR/runConfigureICU" "$host_plat" \ + --enable-static=no --enable-shared=yes --enable-tests=no \ + --enable-samples=no --enable-extras=no --enable-draft=yes \ + --prefix="$HOST_BUILD_DIR/icu_build" + else + log "Host ICU already configured; rebuilding incrementally" + fi make -j"$(parallel_jobs)" + [[ -f config/icucross.mk ]] || die "Host build did not produce config/icucross.mk" popd >/dev/null } @@ -140,13 +149,13 @@ install_icu_data_file() { local major="$1" local dat_name="icudt${major}l.dat" local dat_src - dat_src="$(find "$HOST_BUILD_DIR/data/out" -type f -name "$dat_name" -print -quit)" - [[ -n "$dat_src" ]] || die "Missing generated ICU data file $dat_name" + dat_src="$(find "$HOST_BUILD_DIR/data/out" -type f -name "$dat_name" -print -quit 2>/dev/null || true)" + [[ -n "$dat_src" ]] || die "Missing generated ICU data file $dat_name under $HOST_BUILD_DIR/data/out" cp -f "$dat_src" "$OUTPUT_DIR/$dat_name" } build_android_arch() { - local abi="$1" ndk="$2" host_tag="$3" major="$4" + local abi="$1" ndk="$2" host_tag="$3" local target build_dir install_dir toolchain target="$(arch_to_target "$abi")" build_dir="$OUTPUT_DIR/build/android/$abi" @@ -161,16 +170,33 @@ build_android_arch() { export AR="$toolchain/bin/llvm-ar" export RANLIB="$toolchain/bin/llvm-ranlib" export LDFLAGS="-Wl,--gc-sections -Wl,-z,max-page-size=16384" - "$ICU_SRC_DIR/configure" \ - --with-cross-build="$HOST_BUILD_DIR" --host="$target" \ - --enable-static=no --enable-shared=yes --enable-tests=no \ - --enable-samples=no --enable-extras=no --enable-draft=yes \ - --with-data-packaging=dll - make -j"$(parallel_jobs)" - cp -f "$build_dir/lib"/libicu*.so* "$install_dir/" - [[ -d "$build_dir/stubdata" ]] && cp -f "$build_dir/stubdata"/libicudata*.so* "$install_dir/" || true + if [[ ! -f config.status ]] || ! grep -qE '^PKGDATA_MODE=common$' icudefs.mk 2>/dev/null; then + # archive packaging => PKGDATA_MODE=common (stub libicudata + .dat) + rm -f config.status + "$ICU_SRC_DIR/configure" \ + --with-cross-build="$HOST_BUILD_DIR" --host="$target" \ + --enable-static=no --enable-shared=yes --enable-tests=no \ + --enable-samples=no --enable-extras=no --enable-draft=yes \ + --with-data-packaging=archive + fi + # Override mh-linux SONAME (MIDDLE_SO_TARGET / libicu*.so.N) so DT_NEEDED + # entries match the unversioned lib*.so names Android packages into the APK. + make -j"$(parallel_jobs)" \ + 'LD_SONAME=-Wl,-soname -Wl,$(notdir $(SO_TARGET))' + + rm -f "$install_dir"/libicu*.so* + # With archive packaging, libicudata is the stub (data lives in icudt*l.dat). + # Prefer stubdata/ explicitly; also copy other ICU libs from lib/. + cp -f "$build_dir/lib"/libicuuc.so* "$install_dir/" 2>/dev/null || die "Missing libicuuc in $build_dir/lib" + cp -f "$build_dir/lib"/libicui18n.so* "$install_dir/" 2>/dev/null || die "Missing libicui18n in $build_dir/lib" + if [[ -d "$build_dir/stubdata" ]]; then + cp -f "$build_dir/stubdata"/libicudata.so* "$install_dir/" \ + || die "Missing stub libicudata in $build_dir/stubdata" + else + die "Missing stubdata directory in $build_dir" + fi copy_ndk_cpp_shared "$ndk" "$host_tag" "$target" "$install_dir" - install_android_apk_libs "$install_dir" "$major" + install_android_apk_libs "$install_dir" popd >/dev/null } @@ -216,6 +242,6 @@ install_icu_data_file "$MAJOR" IFS=',' read -ra ARCH_LIST <<< "$ARCHS" for abi in "${ARCH_LIST[@]}"; do abi="$(echo "$abi" | xargs)"; [[ -z "$abi" ]] && continue - build_android_arch "$abi" "$NDK" "$HOST_TAG" "$MAJOR" + build_android_arch "$abi" "$NDK" "$HOST_TAG" done -log "Done. APK-ready libraries are in $OUTPUT_DIR/{abi}/" \ No newline at end of file +log "Done. APK-ready libraries are in $OUTPUT_DIR/{abi}/" diff --git a/icu4c/packaging/pack-android-nuget.sh b/icu4c/packaging/pack-android-nuget.sh index a5a118882285..cde5ca3059f0 100644 --- a/icu4c/packaging/pack-android-nuget.sh +++ b/icu4c/packaging/pack-android-nuget.sh @@ -65,6 +65,8 @@ ICU_MAJOR="${BASH_REMATCH[1]}" sed -i.bak "s/[0-9][0-9]*<\/IcuFwAndroidMajorVersion>/${ICU_MAJOR}<\/IcuFwAndroidMajorVersion>/" \ "$STAGE_DIR/build/Icu4c.Android.Fw.Lib.props" rm -f "$STAGE_DIR/build/Icu4c.Android.Fw.Lib.props.bak" +grep -q ">${ICU_MAJOR}" "$STAGE_DIR/build/Icu4c.Android.Fw.Lib.props" \ + || die "Failed to stamp IcuFwAndroidMajorVersion=${ICU_MAJOR} into props" IFS=',' read -ra ABI_LIST <<< "$ABIS" for abi in "${ABI_LIST[@]}"; do @@ -81,7 +83,7 @@ for abi in "${ABI_LIST[@]}"; do done # SDK-style pack project so Linux CI can pack without mono/nuget.exe. -# Metadata mirrors icu-android-fw-lib.nuspec. +# Metadata intentionally mirrors icu-android-fw-lib.nuspec (reference copy). cat > "$STAGE_DIR/Icu4c.Android.Fw.Lib.csproj" < @@ -98,7 +100,7 @@ cat > "$STAGE_DIR/Icu4c.Android.Fw.Lib.csproj" <LICENSE native;android;maui FieldWorks ICU4C shared libraries for Android (x86_64, arm64-v8a) for MAUI / .NET Android apps. The major version number corresponds to the ICU release. - 70.1.0 - Initial Android package for MAUI / .NET Android (x86_64, arm64-v8a) + $PKG_VERSION - FieldWorks ICU $ICU_MAJOR Android natives (x86_64, arm64-v8a) plus icudt${ICU_MAJOR}l.dat false From 2df733e066784367dccc5f8f31fc8ef51002f7c2 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 7 Aug 2026 15:51:02 +0700 Subject: [PATCH 07/12] Preserve Android NuGet native/assets layout when packing. Without %(RecursiveDir) MSBuild flattens ABI folders into build/, so consumer Exists() checks skip all natives and data; verify the nupkg contents after pack. Co-authored-by: Cursor --- icu4c/packaging/pack-android-nuget.sh | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/icu4c/packaging/pack-android-nuget.sh b/icu4c/packaging/pack-android-nuget.sh index cde5ca3059f0..86d15fede82f 100644 --- a/icu4c/packaging/pack-android-nuget.sh +++ b/icu4c/packaging/pack-android-nuget.sh @@ -45,6 +45,7 @@ done [[ -f "$ICU4C_DIR/LICENSE" ]] || die "Missing LICENSE at $ICU4C_DIR/LICENSE" command -v dotnet >/dev/null || die "dotnet SDK is required to pack the NuGet package" +command -v unzip >/dev/null || die "unzip is required to verify the NuGet package layout" STAGE_DIR="$(mktemp -d "${TMPDIR:-/tmp}/icu-android-nuget.XXXXXX")" cleanup() { rm -rf "$STAGE_DIR"; } @@ -84,6 +85,8 @@ done # SDK-style pack project so Linux CI can pack without mono/nuget.exe. # Metadata intentionally mirrors icu-android-fw-lib.nuspec (reference copy). +# PackagePath must include %(RecursiveDir) or native// and assets/ flatten +# into build/ and the consumer Exists() checks silently fail. cat > "$STAGE_DIR/Icu4c.Android.Fw.Lib.csproj" < @@ -105,7 +108,7 @@ cat > "$STAGE_DIR/Icu4c.Android.Fw.Lib.csproj" < - + EOF @@ -118,4 +121,20 @@ dotnet pack "$STAGE_DIR/Icu4c.Android.Fw.Lib.csproj" \ nupkg="$OUTPUT_DIR/Icu4c.Android.Fw.Lib.$PKG_VERSION.nupkg" [[ -f "$nupkg" ]] || die "Expected package not found: $nupkg" + +verify_dir="$(mktemp -d "${TMPDIR:-/tmp}/icu-android-nuget-verify.XXXXXX")" +unzip -q "$nupkg" -d "$verify_dir" +[[ -f "$verify_dir/build/Icu4c.Android.Fw.Lib.props" ]] || die "nupkg missing build props" +[[ -f "$verify_dir/build/Icu4c.Android.Fw.Lib.targets" ]] || die "nupkg missing build targets" +[[ -f "$verify_dir/build/assets/icudt${ICU_MAJOR}l.dat" ]] || die "nupkg missing build/assets/icudt${ICU_MAJOR}l.dat" +for abi in "${ABI_LIST[@]}"; do + abi="$(echo "$abi" | xargs)" + [[ -z "$abi" ]] && continue + for library in libc++_shared.so libicuuc.so libicui18n.so libicudata.so; do + [[ -f "$verify_dir/build/native/$abi/$library" ]] \ + || die "nupkg missing build/native/$abi/$library" + done +done +rm -rf "$verify_dir" + log "Created $nupkg" From 9ae2b51cd43bc3c3bc4f53e7b223b8a5935e5178 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Mon, 10 Aug 2026 14:27:22 +0700 Subject: [PATCH 08/12] Pack Android NuGet with explicit per-folder PackagePaths. CI showed build/%(RecursiveDir) still omitted build/assets; emit concrete PackagePath entries for props, assets, and each ABI instead. Co-authored-by: Cursor --- icu4c/packaging/pack-android-nuget.sh | 38 ++++++++++++++++++++------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/icu4c/packaging/pack-android-nuget.sh b/icu4c/packaging/pack-android-nuget.sh index 86d15fede82f..dd58aa8aa252 100644 --- a/icu4c/packaging/pack-android-nuget.sh +++ b/icu4c/packaging/pack-android-nuget.sh @@ -83,10 +83,21 @@ for abi in "${ABI_LIST[@]}"; do done done -# SDK-style pack project so Linux CI can pack without mono/nuget.exe. -# Metadata intentionally mirrors icu-android-fw-lib.nuspec (reference copy). -# PackagePath must include %(RecursiveDir) or native// and assets/ flatten -# into build/ and the consumer Exists() checks silently fail. +# Emit explicit PackagePath entries per folder. A single build/** with +# PackagePath=build/%(RecursiveDir) still dropped build/assets in CI. +pack_items="$(mktemp "${TMPDIR:-/tmp}/icu-android-nuget-items.XXXXXX")" +{ + echo ' ' + echo ' ' + echo ' ' + echo ' ' + for abi in "${ABI_LIST[@]}"; do + abi="$(echo "$abi" | xargs)" + [[ -z "$abi" ]] && continue + echo " " + done +} > "$pack_items" + cat > "$STAGE_DIR/Icu4c.Android.Fw.Lib.csproj" < @@ -107,11 +118,11 @@ cat > "$STAGE_DIR/Icu4c.Android.Fw.Lib.csproj" <false - - +$(cat "$pack_items") EOF +rm -f "$pack_items" mkdir -p "$OUTPUT_DIR" log "Packing Icu4c.Android.Fw.Lib $PKG_VERSION from $INPUT_DIR" @@ -124,15 +135,22 @@ nupkg="$OUTPUT_DIR/Icu4c.Android.Fw.Lib.$PKG_VERSION.nupkg" verify_dir="$(mktemp -d "${TMPDIR:-/tmp}/icu-android-nuget-verify.XXXXXX")" unzip -q "$nupkg" -d "$verify_dir" -[[ -f "$verify_dir/build/Icu4c.Android.Fw.Lib.props" ]] || die "nupkg missing build props" -[[ -f "$verify_dir/build/Icu4c.Android.Fw.Lib.targets" ]] || die "nupkg missing build targets" -[[ -f "$verify_dir/build/assets/icudt${ICU_MAJOR}l.dat" ]] || die "nupkg missing build/assets/icudt${ICU_MAJOR}l.dat" +fail_verify() { + echo "[pack-android-nuget] ERROR: $*" >&2 + echo "[pack-android-nuget] Package contents:" >&2 + (cd "$verify_dir" && find . -type f | sort) >&2 || true + rm -rf "$verify_dir" + exit 1 +} +[[ -f "$verify_dir/build/Icu4c.Android.Fw.Lib.props" ]] || fail_verify "nupkg missing build props" +[[ -f "$verify_dir/build/Icu4c.Android.Fw.Lib.targets" ]] || fail_verify "nupkg missing build targets" +[[ -f "$verify_dir/build/assets/icudt${ICU_MAJOR}l.dat" ]] || fail_verify "nupkg missing build/assets/icudt${ICU_MAJOR}l.dat" for abi in "${ABI_LIST[@]}"; do abi="$(echo "$abi" | xargs)" [[ -z "$abi" ]] && continue for library in libc++_shared.so libicuuc.so libicui18n.so libicudata.so; do [[ -f "$verify_dir/build/native/$abi/$library" ]] \ - || die "nupkg missing build/native/$abi/$library" + || fail_verify "nupkg missing build/native/$abi/$library" done done rm -rf "$verify_dir" From a8d2ecd6731cb7c39df336c3839a84eb9bb1b34e Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 11 Aug 2026 16:38:13 +0700 Subject: [PATCH 09/12] Add armeabi-v7a to Android ICU build and NuGet package. CI and packaging now include 32-bit ARM alongside x86_64 and arm64-v8a so apps can run on older ARM devices. Co-authored-by: Cursor --- .github/workflows/fw_icu4c_ci.yml | 4 +-- .../build/Icu4c.Android.Fw.Lib.targets | 5 +++- .../assets/android/icu-android-fw-lib.nuspec | 6 ++--- icu4c/packaging/README.android.md | 23 ++++++++++------- icu4c/packaging/build-android.ps1 | 2 +- icu4c/packaging/build-android.sh | 25 ++++++++++++++----- icu4c/packaging/pack-android-nuget.sh | 8 +++--- 7 files changed, 47 insertions(+), 26 deletions(-) diff --git a/.github/workflows/fw_icu4c_ci.yml b/.github/workflows/fw_icu4c_ci.yml index bedc7ea627e5..aa6b09d4bf7e 100644 --- a/.github/workflows/fw_icu4c_ci.yml +++ b/.github/workflows/fw_icu4c_ci.yml @@ -162,14 +162,14 @@ jobs: - name: Build Android ICU libraries run: | set -xeo pipefail - bash icu4c/packaging/build-android.sh --arch=x86_64,arm64-v8a + bash icu4c/packaging/build-android.sh --arch=x86_64,arm64-v8a,armeabi-v7a - name: Verify Android ICU output contract run: | set -xeuo pipefail major=$(grep -oP '^#define U_ICU_VERSION_MAJOR_NUM \K[0-9]+' icu4c/source/common/unicode/uvernum.h) test -f "icu4c/out/android-icu/icudt${major}l.dat" - for abi in x86_64 arm64-v8a; do + for abi in x86_64 arm64-v8a armeabi-v7a; do for library in libc++_shared.so libicuuc.so libicui18n.so libicudata.so; do test -f "icu4c/out/android-icu/$abi/$library" done diff --git a/icu4c/nugetpackage/assets/android/build/Icu4c.Android.Fw.Lib.targets b/icu4c/nugetpackage/assets/android/build/Icu4c.Android.Fw.Lib.targets index e4b8bcb7073f..5eb8df7e41c8 100644 --- a/icu4c/nugetpackage/assets/android/build/Icu4c.Android.Fw.Lib.targets +++ b/icu4c/nugetpackage/assets/android/build/Icu4c.Android.Fw.Lib.targets @@ -1,7 +1,7 @@ + diff --git a/icu4c/nugetpackage/assets/android/icu-android-fw-lib.nuspec b/icu4c/nugetpackage/assets/android/icu-android-fw-lib.nuspec index 361c2e4be40b..7da058aaa6ed 100644 --- a/icu4c/nugetpackage/assets/android/icu-android-fw-lib.nuspec +++ b/icu4c/nugetpackage/assets/android/icu-android-fw-lib.nuspec @@ -13,14 +13,14 @@ ICU - International Components for Unicode This package contains the FieldWorks version of ICU4C shared libraries for Android -(x86_64 and arm64-v8a), including libc++_shared.so and the ICU data file for MAUI / -.NET Android apps. +(x86_64, arm64-v8a, and armeabi-v7a), including libc++_shared.so and the ICU data +file for MAUI / .NET Android apps. The major version number of the nuget package corresponds to the ICU release. en-US -$version$ - FieldWorks ICU Android natives (x86_64, arm64-v8a) plus icudt*l.dat +$version$ - FieldWorks ICU Android natives (x86_64, arm64-v8a, armeabi-v7a) plus icudt*l.dat diff --git a/icu4c/packaging/README.android.md b/icu4c/packaging/README.android.md index 371d363641c4..064219bbe867 100644 --- a/icu4c/packaging/README.android.md +++ b/icu4c/packaging/README.android.md @@ -25,7 +25,7 @@ installed `ndk/` directory. ```bash export ANDROID_NDK_HOME=/opt/android-ndk-r27 -bash ./icu4c/packaging/build-android.sh --arch=x86_64,arm64-v8a +bash ./icu4c/packaging/build-android.sh --arch=x86_64,arm64-v8a,armeabi-v7a ``` On Windows, install Git for Windows and an Android NDK installed for Windows. @@ -33,9 +33,8 @@ From PowerShell, run: ```powershell $env:ANDROID_NDK_HOME = 'C:\Android\Sdk\ndk\27.0.12077973' -.\icu4c\packaging\build-android.ps1 -Arch x86_64,arm64-v8a +.\icu4c\packaging\build-android.ps1 -Arch x86_64,arm64-v8a,armeabi-v7a ``` - The PowerShell wrapper deliberately invokes Git Bash. It does not use WSL: WSL must run `bash ./icu4c/packaging/build-android.sh` and use an NDK installed for Linux inside WSL. A Windows NDK cannot be reused from WSL, and a @@ -47,8 +46,8 @@ Linux NDK cannot be used by Git Bash. # Default fast validation ABI bash ./icu4c/packaging/build-android.sh -# Select an API level and both supported ABIs -bash ./icu4c/packaging/build-android.sh --api=21 --arch=x86_64,arm64-v8a +# Select an API level and all supported ABIs +bash ./icu4c/packaging/build-android.sh --api=21 --arch=x86_64,arm64-v8a,armeabi-v7a # Delete one ABI's build and installed output before rebuilding it bash ./icu4c/packaging/build-android.sh --clean-arch=x86_64 --arch=x86_64 @@ -57,9 +56,9 @@ bash ./icu4c/packaging/build-android.sh --clean-arch=x86_64 --arch=x86_64 bash ./icu4c/packaging/build-android.sh --clean ``` -Supported ABI values are `x86_64` and `arm64-v8a`. Use `--help` for the full -command reference. The generated `icu4c/out/` directory is already ignored by -this repository. +Supported ABI values are `x86_64`, `arm64-v8a`, and `armeabi-v7a`. Use `--help` +for the full command reference. The generated `icu4c/out/` directory is already +ignored by this repository. ## Output layout @@ -78,6 +77,11 @@ icu4c/out/android-icu/ libicuuc.so libicui18n.so libicudata.so + armeabi-v7a/ + libc++_shared.so + libicuuc.so + libicui18n.so + libicudata.so ``` Cross builds use `--with-data-packaging=archive`, so `libicudata.so` is the @@ -104,7 +108,8 @@ The major version of the package matches the ICU release (same scheme as ``` The package’s MSBuild targets add `AndroidNativeLibrary` entries for -`x86_64` and `arm64-v8a` and embed `icudt70l.dat` as an `AndroidAsset`. +`x86_64`, `arm64-v8a`, and `armeabi-v7a` and embed `icudt70l.dat` as an +`AndroidAsset`. Consumers do not need manual `AndroidNativeLibrary` / `AndroidAsset` ItemGroups. The props file also stamps `IcuFwAndroidMajorVersion` for consumers that need the ICU major (for example aligning diff --git a/icu4c/packaging/build-android.ps1 b/icu4c/packaging/build-android.ps1 index e05bcc096f15..ad91c8f1ac36 100644 --- a/icu4c/packaging/build-android.ps1 +++ b/icu4c/packaging/build-android.ps1 @@ -40,7 +40,7 @@ if (-not $bash) { $archList = ($Arch | ForEach-Object { "$_".Trim() } | Where-Object { $_ }) -join ',' if (-not $archList) { - throw 'At least one -Arch value is required (x86_64 and/or arm64-v8a).' + throw 'At least one -Arch value is required (x86_64, arm64-v8a, and/or armeabi-v7a).' } $arguments = @($bashScript, "--arch=$archList", "--api=$ApiLevel") diff --git a/icu4c/packaging/build-android.sh b/icu4c/packaging/build-android.sh index 4a7f916a58fc..becdeb88e8b6 100644 --- a/icu4c/packaging/build-android.sh +++ b/icu4c/packaging/build-android.sh @@ -25,7 +25,7 @@ udata_setCommonData). Shared libraries are linked with unversioned SONAMEs so APK packaging of lib*.so alone works with the Android loader. Options: - --arch=LIST Comma-separated ABIs: x86_64, arm64-v8a (default: x86_64) + --arch=LIST Comma-separated ABIs: x86_64, arm64-v8a, armeabi-v7a (default: x86_64) --api=LEVEL Minimum Android API (default: 21) --clean Remove all generated Android ICU output --clean-arch=LIST Remove generated output for the listed ABI(s) @@ -90,11 +90,23 @@ resolve_ndk() { die "Android NDK not found. Set ANDROID_NDK_HOME or ANDROID_HOME." } +# Clang/configure triple for the ABI. arch_to_target() { case "$1" in x86_64) echo x86_64-linux-android ;; arm64-v8a) echo aarch64-linux-android ;; - *) die "Unsupported ABI '$1'. Supported ABIs: x86_64, arm64-v8a" ;; + armeabi-v7a) echo armv7a-linux-androideabi ;; + *) die "Unsupported ABI '$1'. Supported ABIs: x86_64, arm64-v8a, armeabi-v7a" ;; + esac +} + +# NDK sysroot usr/lib/ name (differs from the clang triple for 32-bit ARM). +arch_to_sysroot_lib() { + case "$1" in + x86_64) echo x86_64-linux-android ;; + arm64-v8a) echo aarch64-linux-android ;; + armeabi-v7a) echo arm-linux-androideabi ;; + *) die "Unsupported ABI '$1'. Supported ABIs: x86_64, arm64-v8a, armeabi-v7a" ;; esac } @@ -108,8 +120,8 @@ parallel_jobs() { } copy_ndk_cpp_shared() { - local ndk="$1" host_tag="$2" target="$3" install_dir="$4" - local cxx_lib="$ndk/toolchains/llvm/prebuilt/$host_tag/sysroot/usr/lib/$target/libc++_shared.so" + local ndk="$1" host_tag="$2" sysroot_lib="$3" install_dir="$4" + local cxx_lib="$ndk/toolchains/llvm/prebuilt/$host_tag/sysroot/usr/lib/$sysroot_lib/libc++_shared.so" [[ -f "$cxx_lib" ]] || die "NDK libc++ not found: $cxx_lib" cp -f "$cxx_lib" "$install_dir/" } @@ -156,8 +168,9 @@ install_icu_data_file() { build_android_arch() { local abi="$1" ndk="$2" host_tag="$3" - local target build_dir install_dir toolchain + local target sysroot_lib build_dir install_dir toolchain target="$(arch_to_target "$abi")" + sysroot_lib="$(arch_to_sysroot_lib "$abi")" build_dir="$OUTPUT_DIR/build/android/$abi" install_dir="$OUTPUT_DIR/$abi" toolchain="$ndk/toolchains/llvm/prebuilt/$host_tag" @@ -195,7 +208,7 @@ build_android_arch() { else die "Missing stubdata directory in $build_dir" fi - copy_ndk_cpp_shared "$ndk" "$host_tag" "$target" "$install_dir" + copy_ndk_cpp_shared "$ndk" "$host_tag" "$sysroot_lib" "$install_dir" install_android_apk_libs "$install_dir" popd >/dev/null } diff --git a/icu4c/packaging/pack-android-nuget.sh b/icu4c/packaging/pack-android-nuget.sh index dd58aa8aa252..b4366d96927b 100644 --- a/icu4c/packaging/pack-android-nuget.sh +++ b/icu4c/packaging/pack-android-nuget.sh @@ -10,7 +10,7 @@ INPUT_DIR="${INPUT_DIR:-$ICU4C_DIR/out/android-icu}" ASSETS_DIR="$ICU4C_DIR/nugetpackage/assets/android" OUTPUT_DIR="${OUTPUT_DIR:-$ICU4C_DIR/nugetpackage}" PKG_VERSION="" -ABIS="${ABIS:-x86_64,arm64-v8a}" +ABIS="${ABIS:-x86_64,arm64-v8a,armeabi-v7a}" usage() { cat < "$STAGE_DIR/Icu4c.Android.Fw.Lib.csproj" <https://github.com/sillsdev/icu LICENSE native;android;maui - FieldWorks ICU4C shared libraries for Android (x86_64, arm64-v8a) for MAUI / .NET Android apps. The major version number corresponds to the ICU release. - $PKG_VERSION - FieldWorks ICU $ICU_MAJOR Android natives (x86_64, arm64-v8a) plus icudt${ICU_MAJOR}l.dat + FieldWorks ICU4C shared libraries for Android (x86_64, arm64-v8a, armeabi-v7a) for MAUI / .NET Android apps. The major version number corresponds to the ICU release. + $PKG_VERSION - FieldWorks ICU $ICU_MAJOR Android natives (x86_64, arm64-v8a, armeabi-v7a) plus icudt${ICU_MAJOR}l.dat false From cce148c1f566efb7fd5276950f7b7fd256285bf5 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 13 Aug 2026 09:34:32 +0700 Subject: [PATCH 10/12] Address PR #26 review feedback for Android build scripts - Remove unused build-android.ps1; run build-android.sh from Git Bash on Windows instead, and update README.android.md accordingly. - Rename ABIS/--abis to ARCHS/--arch in pack-android-nuget.sh to match build-android.sh. - Use realpath for SCRIPT_DIR/ICU4C_DIR in build-android.sh. - Drop confusing comment from Icu4c.Android.Fw.Lib.targets and reword the SONAME-override comment in build-android.sh. Co-Authored-By: Claude Opus 4.8 --- .../build/Icu4c.Android.Fw.Lib.targets | 4 -- icu4c/packaging/README.android.md | 19 ++++--- icu4c/packaging/build-android.ps1 | 55 ------------------- icu4c/packaging/build-android.sh | 11 ++-- icu4c/packaging/pack-android-nuget.sh | 14 ++--- 5 files changed, 24 insertions(+), 79 deletions(-) delete mode 100644 icu4c/packaging/build-android.ps1 diff --git a/icu4c/nugetpackage/assets/android/build/Icu4c.Android.Fw.Lib.targets b/icu4c/nugetpackage/assets/android/build/Icu4c.Android.Fw.Lib.targets index 5eb8df7e41c8..271502e09218 100644 --- a/icu4c/nugetpackage/assets/android/build/Icu4c.Android.Fw.Lib.targets +++ b/icu4c/nugetpackage/assets/android/build/Icu4c.Android.Fw.Lib.targets @@ -1,8 +1,4 @@ - -[CmdletBinding()] -param( - [string[]] $Arch = @('x86_64'), - [int] $ApiLevel = 21, - [switch] $Clean, - [string[]] $CleanArch, - [switch] $Help -) - -$ErrorActionPreference = 'Stop' -$bashScript = Join-Path $PSScriptRoot 'build-android.sh' - -function Find-GitBash { - $candidates = @( - 'C:\Program Files\Git\bin\bash.exe', - 'C:\Program Files (x86)\Git\bin\bash.exe' - ) - if ($env:ProgramFiles) { - $candidates += (Join-Path $env:ProgramFiles 'Git\bin\bash.exe') - } - if (${env:ProgramFiles(x86)}) { - $candidates += (Join-Path ${env:ProgramFiles(x86)} 'Git\bin\bash.exe') - } - $candidates | Where-Object { $_ -and (Test-Path $_) } | Select-Object -First 1 -} - -$bash = Find-GitBash -if (-not $bash) { - throw 'Git Bash was not found. Install Git for Windows, then re-run this command. For WSL, run ./icu4c/packaging/build-android.sh inside WSL with a Linux NDK installation.' -} - -$archList = ($Arch | ForEach-Object { "$_".Trim() } | Where-Object { $_ }) -join ',' -if (-not $archList) { - throw 'At least one -Arch value is required (x86_64, arm64-v8a, and/or armeabi-v7a).' -} - -$arguments = @($bashScript, "--arch=$archList", "--api=$ApiLevel") -if ($Clean) { $arguments += '--clean' } -if ($CleanArch -and $CleanArch.Count -gt 0) { - $cleanList = ($CleanArch | ForEach-Object { "$_".Trim() } | Where-Object { $_ }) -join ',' - if ($cleanList) { $arguments += "--clean-arch=$cleanList" } -} -if ($Help) { $arguments = @($bashScript, '--help') } -Write-Host "Running: $bash $($arguments -join ' ')" -& $bash @arguments -exit $LASTEXITCODE diff --git a/icu4c/packaging/build-android.sh b/icu4c/packaging/build-android.sh index becdeb88e8b6..b428b04dbeae 100644 --- a/icu4c/packaging/build-android.sh +++ b/icu4c/packaging/build-android.sh @@ -5,8 +5,8 @@ set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ICU4C_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +SCRIPT_DIR="$(realpath "$(dirname "${BASH_SOURCE[0]}")")" +ICU4C_DIR="$(realpath "$SCRIPT_DIR/..")" ICU_SRC_DIR="$ICU4C_DIR/source" OUTPUT_DIR="$ICU4C_DIR/out/android-icu" HOST_BUILD_DIR="$OUTPUT_DIR/build/host" @@ -192,8 +192,11 @@ build_android_arch() { --enable-samples=no --enable-extras=no --enable-draft=yes \ --with-data-packaging=archive fi - # Override mh-linux SONAME (MIDDLE_SO_TARGET / libicu*.so.N) so DT_NEEDED - # entries match the unversioned lib*.so names Android packages into the APK. + # By default ICU gives its shared libraries versioned SONAMEs (e.g. + # libicuuc.so.70) and records those versioned names in each library's + # dependency list. Android only packages the unversioned lib*.so files + # into the APK, so override the SONAME to the unversioned name; otherwise + # the loader looks for libicuuc.so.70 and fails. make -j"$(parallel_jobs)" \ 'LD_SONAME=-Wl,-soname -Wl,$(notdir $(SO_TARGET))' diff --git a/icu4c/packaging/pack-android-nuget.sh b/icu4c/packaging/pack-android-nuget.sh index b4366d96927b..1be3f77003b4 100644 --- a/icu4c/packaging/pack-android-nuget.sh +++ b/icu4c/packaging/pack-android-nuget.sh @@ -10,7 +10,7 @@ INPUT_DIR="${INPUT_DIR:-$ICU4C_DIR/out/android-icu}" ASSETS_DIR="$ICU4C_DIR/nugetpackage/assets/android" OUTPUT_DIR="${OUTPUT_DIR:-$ICU4C_DIR/nugetpackage}" PKG_VERSION="" -ABIS="${ABIS:-x86_64,arm64-v8a,armeabi-v7a}" +ARCHS="${ARCHS:-x86_64,arm64-v8a,armeabi-v7a}" usage() { cat <${ICU_MAJOR}" "$STAGE_DIR/build/Icu4c.Android.Fw.Lib.props" \ || die "Failed to stamp IcuFwAndroidMajorVersion=${ICU_MAJOR} into props" -IFS=',' read -ra ABI_LIST <<< "$ABIS" -for abi in "${ABI_LIST[@]}"; do +IFS=',' read -ra ARCH_LIST <<< "$ARCHS" +for abi in "${ARCH_LIST[@]}"; do abi="$(echo "$abi" | xargs)" [[ -z "$abi" ]] && continue src="$INPUT_DIR/$abi" @@ -91,7 +91,7 @@ pack_items="$(mktemp "${TMPDIR:-/tmp}/icu-android-nuget-items.XXXXXX")" echo ' ' echo ' ' echo ' ' - for abi in "${ABI_LIST[@]}"; do + for abi in "${ARCH_LIST[@]}"; do abi="$(echo "$abi" | xargs)" [[ -z "$abi" ]] && continue echo " " @@ -145,7 +145,7 @@ fail_verify() { [[ -f "$verify_dir/build/Icu4c.Android.Fw.Lib.props" ]] || fail_verify "nupkg missing build props" [[ -f "$verify_dir/build/Icu4c.Android.Fw.Lib.targets" ]] || fail_verify "nupkg missing build targets" [[ -f "$verify_dir/build/assets/icudt${ICU_MAJOR}l.dat" ]] || fail_verify "nupkg missing build/assets/icudt${ICU_MAJOR}l.dat" -for abi in "${ABI_LIST[@]}"; do +for abi in "${ARCH_LIST[@]}"; do abi="$(echo "$abi" | xargs)" [[ -z "$abi" ]] && continue for library in libc++_shared.so libicuuc.so libicui18n.so libicudata.so; do From 59f9cfd689461c78273111ab67da32ba02c854df Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 13 Aug 2026 09:52:38 +0700 Subject: [PATCH 11/12] Add NuGet badges to README and rename publish secret - Add a NuGet packages table to README.md with version badges for the fw packages (Icu4c.Win.Fw.Lib, Icu4c.Win.Fw.Bin, Icu4c.Android.Fw.Lib). - Rename the NuGet publish secret to SILLSDEV_PUBLISH_NUGET_ORG in fw_icu4c_ci.yml. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/fw_icu4c_ci.yml | 4 ++-- README.md | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/fw_icu4c_ci.yml b/.github/workflows/fw_icu4c_ci.yml index aa6b09d4bf7e..1c948f4d1712 100644 --- a/.github/workflows/fw_icu4c_ci.yml +++ b/.github/workflows/fw_icu4c_ci.yml @@ -98,7 +98,7 @@ jobs: - name: Publish nuget packages if: github.event_name == 'push' working-directory: icu4c - run: nuget push **/*.nupkg -Source 'https://api.nuget.org/v3/index.json' -ApiKey ${{ secrets.ICU_NUGET_API_KEY }} + run: nuget push **/*.nupkg -Source 'https://api.nuget.org/v3/index.json' -ApiKey ${{ secrets.SILLSDEV_PUBLISH_NUGET_ORG }} # Run ICU4C tests with SIL mod and stub data @@ -195,7 +195,7 @@ jobs: set -xeuo pipefail dotnet nuget push icu4c/nugetpackage/Icu4c.Android.Fw.Lib.*.nupkg \ --source 'https://api.nuget.org/v3/index.json' \ - --api-key '${{ secrets.ICU_NUGET_API_KEY }}' \ + --api-key '${{ secrets.SILLSDEV_PUBLISH_NUGET_ORG }}' \ --skip-duplicate debian-packaging: diff --git a/README.md b/README.md index 4920e941b4ef..a57965ccb3ec 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,14 @@ Build | Status ------|------- GitHub Actions | [![FW Branch CI](https://github.com/sillsdev/icu/actions/workflows/fw_icu4c_ci.yml/badge.svg?branch=fw)](https://github.com/sillsdev/icu/actions/workflows/fw_icu4c_ci.yml) +### NuGet packages (`fw` branch) + +Package | Version +--------|-------- +[Icu4c.Win.Fw.Lib](https://www.nuget.org/packages/Icu4c.Win.Fw.Lib/) | [![NuGet version](https://img.shields.io/nuget/v/Icu4c.Win.Fw.Lib.svg?style=flat-square)](https://www.nuget.org/packages/Icu4c.Win.Fw.Lib/) +[Icu4c.Win.Fw.Bin](https://www.nuget.org/packages/Icu4c.Win.Fw.Bin/) | [![NuGet version](https://img.shields.io/nuget/v/Icu4c.Win.Fw.Bin.svg?style=flat-square)](https://www.nuget.org/packages/Icu4c.Win.Fw.Bin/) +[Icu4c.Android.Fw.Lib](https://www.nuget.org/packages/Icu4c.Android.Fw.Lib/) | [![NuGet version](https://img.shields.io/nuget/v/Icu4c.Android.Fw.Lib.svg?style=flat-square)](https://www.nuget.org/packages/Icu4c.Android.Fw.Lib/) + # ICU Project info The ICU project is under the stewardship of [The Unicode Consortium](https://www.unicode.org). From dd5d9c41ee60a6d464a5f7e71bfeab446558056e Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 13 Aug 2026 10:21:29 +0700 Subject: [PATCH 12/12] Fix stale config.status on restored Android build cache When the host or per-ABI build tree is restored from the CI cache, git checkout rewrites configure/uvernum.h mtimes to now, so ICU's Makefile treats the cached config.status as stale and aborts. Refresh config.status mtime on the incremental (reuse) path so the build proceeds against the existing, identical configuration (the cache key already pins configure and uvernum.h, so reuse only happens when they are byte-identical). Co-Authored-By: Claude Opus 4.8 --- icu4c/packaging/build-android.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/icu4c/packaging/build-android.sh b/icu4c/packaging/build-android.sh index b428b04dbeae..68f9da700325 100644 --- a/icu4c/packaging/build-android.sh +++ b/icu4c/packaging/build-android.sh @@ -119,6 +119,16 @@ parallel_jobs() { nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4 } +# Refresh config.status in the current build dir when reusing a configured +# tree (e.g. restored from a CI cache). git checkout rewrites the mtimes of +# configure and uvernum.h to "now", making them newer than the cached +# config.status; ICU's Makefile then treats config.status as stale (see the +# `config.status:` rule in source/Makefile.in) and aborts. Touching it keeps +# the existing configuration and lets the incremental build proceed. +refresh_config_status() { + [[ -f config.status ]] && touch config.status +} + copy_ndk_cpp_shared() { local ndk="$1" host_tag="$2" sysroot_lib="$3" install_dir="$4" local cxx_lib="$ndk/toolchains/llvm/prebuilt/$host_tag/sysroot/usr/lib/$sysroot_lib/libc++_shared.so" @@ -150,6 +160,7 @@ build_host_icu() { --prefix="$HOST_BUILD_DIR/icu_build" else log "Host ICU already configured; rebuilding incrementally" + refresh_config_status fi make -j"$(parallel_jobs)" [[ -f config/icucross.mk ]] || die "Host build did not produce config/icucross.mk" @@ -191,6 +202,8 @@ build_android_arch() { --enable-static=no --enable-shared=yes --enable-tests=no \ --enable-samples=no --enable-extras=no --enable-draft=yes \ --with-data-packaging=archive + else + refresh_config_status fi # By default ICU gives its shared libraries versioned SONAMEs (e.g. # libicuuc.so.70) and records those versioned names in each library's