Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/scripts/install-android-packages.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail

# Runner images and restored caches may already contain some or all required packages.
# Use the local SDK metadata instead of making sdkmanager refresh remote indexes every run.
: "${ANDROID_HOME:?ANDROID_HOME must point to the Android SDK}"
if (( $# == 0 )); then
echo 'Pass at least one SDK package ID.' >&2
exit 1
fi

missing=()
for package in "$@"; do
directory="${ANDROID_HOME}/${package//;/\/}"
if [[ -f "$directory/package.xml" || -f "$directory/source.properties" ]]; then
echo "Using installed SDK package: $package"
else
missing+=("$package")
fi
done

if (( ${#missing[@]} )); then
sdkmanager --sdk_root="$ANDROID_HOME" --install "${missing[@]}"
fi
107 changes: 98 additions & 9 deletions .github/workflows/debug-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,70 @@ on:
- '**.md'
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

env:
ANDROID_API_LEVEL: '36'
ANDROID_BUILD_TOOLS: '36.0.0'
ANDROID_NDK_VERSION: '28.2.13676358'
ANDROID_CMAKE_VERSION: '3.22.1'

jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04

steps:
- uses: actions/checkout@v4

- name: Set up Node.js 22
uses: actions/setup-node@v4
with:
node-version: '22'

- name: Set up JDK 25
uses: actions/setup-java@v4
with:
java-version: '25'
distribution: temurin
cache: gradle

- name: Set up Android SDK
uses: android-actions/setup-android@v4
- name: Set up Gradle cache
uses: gradle/actions/setup-gradle@v5
with:
packages: platform-tools
cache-read-only: ${{ github.event_name == 'pull_request' }}

- name: Install Android build tools
run: sdkmanager 'platforms;android-36' 'build-tools;36.0.0' 'ndk;28.2.13676358' 'cmake;3.22.1'
- name: Locate preinstalled Android SDK
id: android-sdk
run: |
test -d "$ANDROID_HOME/cmdline-tools/latest/bin"
echo "$ANDROID_HOME/cmdline-tools/latest/bin" >> "$GITHUB_PATH"
echo "$ANDROID_HOME/platform-tools" >> "$GITHUB_PATH"
echo "root=$ANDROID_HOME" >> "$GITHUB_OUTPUT"

- name: Cache required Android toolchain
uses: actions/cache@v4
with:
path: |
${{ steps.android-sdk.outputs.root }}/platforms/android-${{ env.ANDROID_API_LEVEL }}
${{ steps.android-sdk.outputs.root }}/build-tools/${{ env.ANDROID_BUILD_TOOLS }}
${{ steps.android-sdk.outputs.root }}/ndk/${{ env.ANDROID_NDK_VERSION }}
${{ steps.android-sdk.outputs.root }}/cmake/${{ env.ANDROID_CMAKE_VERSION }}
key: android-toolchain-v1-ubuntu24-${{ runner.arch }}-api${{ env.ANDROID_API_LEVEL }}-bt${{ env.ANDROID_BUILD_TOOLS }}-ndk${{ env.ANDROID_NDK_VERSION }}-cmake${{ env.ANDROID_CMAKE_VERSION }}

- name: Verify SDK installation helper
run: node --test test/ci-android-toolchain.test.js

- name: Install missing Android packages
run: |
bash .github/scripts/install-android-packages.sh \
"platforms;android-$ANDROID_API_LEVEL" \
"build-tools;$ANDROID_BUILD_TOOLS" \
"ndk;$ANDROID_NDK_VERSION" \
"cmake;$ANDROID_CMAKE_VERSION"

- name: Build and verify Debug
run: ./gradlew --no-daemon :shared:testDebugUnitTest :common:testDebugUnitTest :common:assembleDebugAndroidTest :mobile:lintDebug :mobile:assembleDebug
run: ./gradlew --no-daemon --build-cache :shared:testDebugUnitTest :common:testDebugUnitTest :common:assembleDebugAndroidTest :mobile:lintDebug :mobile:assembleDebug

- name: Upload failed unit test reports
if: failure()
Expand Down Expand Up @@ -72,13 +112,33 @@ jobs:
browser-runtime:
name: Browser HTTP on Android ${{ matrix.api-level }}
needs: build
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
api-level: [28, 36]
steps:
- name: Locate emulator cache directories
id: emulator-cache
run: |
echo "sdk=$ANDROID_HOME" >> "$GITHUB_OUTPUT"
echo "image-version=$ImageVersion" >> "$GITHUB_OUTPUT"

# Cache only this API's image and clean AVD. The runner image version also
# invalidates snapshots when the preinstalled emulator/host software changes.
- name: Restore Android emulator and clean snapshot
id: avd-cache
uses: actions/cache/restore@v4
with:
path: |
${{ steps.emulator-cache.outputs.sdk }}/emulator
${{ steps.emulator-cache.outputs.sdk }}/platforms/android-${{ matrix.api-level }}
${{ steps.emulator-cache.outputs.sdk }}/system-images/android-${{ matrix.api-level }}/google_apis/x86_64
~/.android/avd
~/.android/adb*
key: android-avd-v1-ubuntu24-${{ runner.arch }}-${{ steps.emulator-cache.outputs.image-version }}-api${{ matrix.api-level }}-google_apis-x86_64

- name: Download browser runtime test APK
uses: actions/download-artifact@v4
with:
Expand All @@ -88,12 +148,41 @@ jobs:
- name: Enable KVM
run: sudo chmod a+rw /dev/kvm

- name: Create clean emulator snapshot on cache miss
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
arch: x86_64
target: google_apis
avd-name: browser-runtime
force-avd-creation: false
disable-animations: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
script: echo 'Clean emulator snapshot created.'

# Save before installing or running tests so later jobs restore a clean device.
- name: Save clean emulator snapshot
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: |
${{ steps.emulator-cache.outputs.sdk }}/emulator
${{ steps.emulator-cache.outputs.sdk }}/platforms/android-${{ matrix.api-level }}
${{ steps.emulator-cache.outputs.sdk }}/system-images/android-${{ matrix.api-level }}/google_apis/x86_64
~/.android/avd
~/.android/adb*
key: ${{ steps.avd-cache.outputs.cache-primary-key }}

- name: Verify localized pages on Android ICU
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
arch: x86_64
target: google_apis
avd-name: browser-runtime
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: |
adb install -r runtime-tests/common-debug-androidTest.apk
Expand Down
79 changes: 79 additions & 0 deletions test/ci-android-toolchain.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';

const script = fileURLToPath(new URL('../.github/scripts/install-android-packages.sh', import.meta.url));
const packages = ['platforms;android-36', 'build-tools;36.0.0', 'ndk;28.2.13676358', 'cmake;3.22.1'];

function sdkFixture(t) {
// Include spaces to check SDK paths and package arguments are kept intact.
const directory = mkdtempSync(join(tmpdir(), 'wheelplay sdk '));
t.after(() => rmSync(directory, { recursive: true, force: true }));
const sdk = join(directory, 'Android SDK'), bin = join(directory, 'bin'), log = join(directory, 'sdkmanager.log');
mkdirSync(sdk); mkdirSync(bin);
writeFileSync(join(bin, 'sdkmanager'), `#!/usr/bin/env bash
set -euo pipefail
printf '%s\\n' "$@" > "$SDK_CALL_LOG"
if [[ "\${MOCK_SDKMANAGER_FAIL:-0}" == 1 ]]; then exit 7; fi
shift 2
for package in "$@"; do
directory="$ANDROID_HOME/\${package//;/\\/}"
mkdir -p "$directory"
printf 'installed' > "$directory/package.xml"
done
`, { mode: 0o755 });
return {
sdk, log,
installed(packageId) {
const directory = join(sdk, ...packageId.split(';'));
mkdirSync(directory, { recursive: true });
writeFileSync(join(directory, 'package.xml'), 'preinstalled');
},
run(fail = false) {
return spawnSync('bash', [script, ...packages], {
encoding: 'utf8',
env: { ...process.env, PATH: `${bin}:${process.env.PATH}`, ANDROID_HOME: sdk,
SDK_CALL_LOG: log, MOCK_SDKMANAGER_FAIL: fail ? '1' : '0' },
});
},
calls() { return readFileSync(log, 'utf8').trimEnd().split('\n'); },
};
}

test('SDK cold start installs all packages and a warm run skips sdkmanager entirely', t => {
const f = sdkFixture(t);
assert.equal(f.run().status, 0);
assert.deepEqual(f.calls(), [`--sdk_root=${f.sdk}`, '--install', ...packages]);
rmSync(f.log);
assert.equal(f.run().status, 0);
assert.equal(existsSync(f.log), false);
});

test('a partial SDK cache installs only missing packages and preserves preinstalled metadata', t => {
const f = sdkFixture(t);
f.installed(packages[0]); f.installed(packages[1]);
assert.equal(f.run().status, 0);
assert.deepEqual(f.calls(), [`--sdk_root=${f.sdk}`, '--install', ...packages.slice(2)]);
assert.equal(readFileSync(join(f.sdk, 'platforms', 'android-36', 'package.xml'), 'utf8'), 'preinstalled');
});

test('SDK packages unpacked from official archives do not require sdkmanager metadata', t => {
const f = sdkFixture(t);
for (const packageId of packages) {
const directory = join(f.sdk, ...packageId.split(';'));
mkdirSync(directory, { recursive: true });
writeFileSync(join(directory, 'source.properties'), 'Pkg.Revision = 1');
}
assert.equal(f.run().status, 0);
assert.equal(existsSync(f.log), false);
});

test('sdkmanager failures fail the setup step without marking missing packages installed', t => {
const f = sdkFixture(t);
assert.equal(f.run(true).status, 7);
assert.equal(existsSync(join(f.sdk, 'ndk', '28.2.13676358', 'package.xml')), false);
});
Loading