diff --git a/.github/workflows/fw_icu4c_ci.yml b/.github/workflows/fw_icu4c_ci.yml index 07042587af15..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 @@ -125,6 +125,79 @@ jobs: echo 'Running ICU4C cintltst' 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' + 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/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 --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 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 + 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@v7 + 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.SILLSDEV_PUBLISH_NUGET_ORG }}' \ + --skip-duplicate + debian-packaging: needs: set-version-number runs-on: ubuntu-latest diff --git a/README.md b/README.md index d46ccc544eda..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). @@ -30,6 +38,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/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..e875b61c3dda --- /dev/null +++ b/icu4c/nugetpackage/assets/android/build/Icu4c.Android.Fw.Lib.props @@ -0,0 +1,10 @@ + + + + 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..271502e09218 --- /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..7da058aaa6ed --- /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, 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, armeabi-v7a) plus icudt*l.dat + + + + + + + + diff --git a/icu4c/packaging/README.android.md b/icu4c/packaging/README.android.md new file mode 100644 index 000000000000..f25d202ba78f --- /dev/null +++ b/icu4c/packaging/README.android.md @@ -0,0 +1,137 @@ +# 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. + +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`, +`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,armeabi-v7a +``` + +On Windows, install Git for Windows and an Android NDK installed for Windows, +then run the script from Git Bash: + +```bash +export ANDROID_NDK_HOME='C:\Android\Sdk\ndk\27.0.12077973' +bash ./icu4c/packaging/build-android.sh --arch=x86_64,arm64-v8a,armeabi-v7a +``` + +Git Bash and WSL need different NDKs: a Windows NDK cannot be reused from WSL, +and a Linux NDK cannot be used by Git Bash. Under WSL, run the same +`bash ./icu4c/packaging/build-android.sh` command with an NDK installed for +Linux inside WSL. + +## Controls and cleanup + +```bash +# Default fast validation ABI +bash ./icu4c/packaging/build-android.sh + +# 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 + +# Delete all generated host tools, cross-build trees, data, and libraries +bash ./icu4c/packaging/build-android.sh --clean +``` + +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 + +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 + arm64-v8a/ + libc++_shared.so + 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 +**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. + +## 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`, `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 +`AndroidIcuBootstrap`). + +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 + +- 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. +- 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.sh b/icu4c/packaging/build-android.sh new file mode 100644 index 000000000000..68f9da700325 --- /dev/null +++ b/icu4c/packaging/build-android.sh @@ -0,0 +1,276 @@ +#!/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="$(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" +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." +} + +# 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 ;; + 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 +} + +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 +} + +# 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" + [[ -f "$cxx_lib" ]] || die "NDK libc++ not found: $cxx_lib" + 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" lib src + for lib in icuuc icui18n icudata; do + 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" + done +} + +build_host_icu() { + local host_plat + mkdir -p "$HOST_BUILD_DIR" + pushd "$HOST_BUILD_DIR" >/dev/null + 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" + refresh_config_status + fi + make -j"$(parallel_jobs)" + [[ -f config/icucross.mk ]] || die "Host build did not produce config/icucross.mk" + popd >/dev/null +} + +install_icu_data_file() { + # 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 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" + 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" + 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" + 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 + 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 + # 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))' + + 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" "$sysroot_lib" "$install_dir" + install_android_apk_libs "$install_dir" + 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" +done +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 new file mode 100644 index 000000000000..1be3f77003b4 --- /dev/null +++ b/icu4c/packaging/pack-android-nuget.sh @@ -0,0 +1,158 @@ +#!/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="" +ARCHS="${ARCHS:-x86_64,arm64-v8a,armeabi-v7a}" + +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#*=}" ;; + --arch=*) ARCHS="${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" +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"; } +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" +grep -q ">${ICU_MAJOR}" "$STAGE_DIR/build/Icu4c.Android.Fw.Lib.props" \ + || die "Failed to stamp IcuFwAndroidMajorVersion=${ICU_MAJOR} into props" + +IFS=',' read -ra ARCH_LIST <<< "$ARCHS" +for abi in "${ARCH_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 + +# 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 "${ARCH_LIST[@]}"; do + abi="$(echo "$abi" | xargs)" + [[ -z "$abi" ]] && continue + echo " " + done +} > "$pack_items" + +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, 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 + + +$(cat "$pack_items") + + +EOF +rm -f "$pack_items" + +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" + +verify_dir="$(mktemp -d "${TMPDIR:-/tmp}/icu-android-nuget-verify.XXXXXX")" +unzip -q "$nupkg" -d "$verify_dir" +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 "${ARCH_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" ]] \ + || fail_verify "nupkg missing build/native/$abi/$library" + done +done +rm -rf "$verify_dir" + +log "Created $nupkg"