From cb44ee21c7a04523796e2b744c2a817bb547aba2 Mon Sep 17 00:00:00 2001 From: Matan Baruch Date: Wed, 2 Sep 2026 16:47:56 +0300 Subject: [PATCH 1/2] Replace preinstalled cmdline-tools older than revision 23 The install was gated on the absence of the cmdline-tools directory, so on GitHub-hosted runners that ship their own copy (ubuntu-24.04 ships 12.0) the bundled 23.0 was never downloaded and the stale revision went on PATH. Revision 12.0 cannot parse a dotted API level such as 37.1: avdmanager exits 0 but writes target=android-0 into the AVD ini, which mis-configures gfxstream and crash-loops the guest. Gate the install on Pkg.Revision instead, and remove the stale latest before extracting so the new package replaces it in place. --- __tests__/sdk-installer.test.ts | 26 ++++++++++++++++++++++++++ lib/sdk-installer.js | 23 +++++++++++++++++++++-- src/sdk-installer.ts | 22 ++++++++++++++++++++-- 3 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 __tests__/sdk-installer.test.ts diff --git a/__tests__/sdk-installer.test.ts b/__tests__/sdk-installer.test.ts new file mode 100644 index 000000000..b89176de4 --- /dev/null +++ b/__tests__/sdk-installer.test.ts @@ -0,0 +1,26 @@ +import { parseCmdlineToolsMajorRevision } from '../src/sdk-installer'; + +describe('cmdline-tools revision parser tests', () => { + it('Parses the major revision of the cmdline-tools preinstalled on ubuntu-24.04', () => { + const sourceProperties = ['Pkg.UserSrc=false', 'Pkg.Revision=12.0', 'Pkg.Path=cmdline-tools;12.0', 'Pkg.Desc=Android SDK Command-line Tools', ''].join('\n'); + expect(parseCmdlineToolsMajorRevision(sourceProperties)).toBe(12); + }); + + it('Parses the major revision of the cmdline-tools bundled with the action', () => { + const sourceProperties = ['Pkg.UserSrc=false', 'Pkg.Revision=23.0', 'Pkg.Path=cmdline-tools;23.0', 'Pkg.Desc=Android SDK Command-line Tools', ''].join('\n'); + expect(parseCmdlineToolsMajorRevision(sourceProperties)).toBe(23); + }); + + it('Returns null if the revision is missing', () => { + const sourceProperties = ['Pkg.UserSrc=false', 'Pkg.Desc=Android SDK Command-line Tools', ''].join('\n'); + expect(parseCmdlineToolsMajorRevision(sourceProperties)).toBeNull(); + }); + + it('Returns null if the revision is malformed', () => { + expect(parseCmdlineToolsMajorRevision('Pkg.Revision=unknown')).toBeNull(); + }); + + it('Returns null for empty source properties', () => { + expect(parseCmdlineToolsMajorRevision('')).toBeNull(); + }); +}); diff --git a/lib/sdk-installer.js b/lib/sdk-installer.js index d84759889..e17676cc4 100644 --- a/lib/sdk-installer.js +++ b/lib/sdk-installer.js @@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); +exports.parseCmdlineToolsMajorRevision = parseCmdlineToolsMajorRevision; exports.installAndroidSdk = installAndroidSdk; const core = __importStar(require("@actions/core")); const exec = __importStar(require("@actions/exec")); @@ -44,6 +45,15 @@ const BUILD_TOOLS_VERSION = '37.0.0'; const CMDLINE_TOOLS_VERSION = '16111833'; const CMDLINE_TOOLS_URL_MAC = `https://dl.google.com/android/repository/commandlinetools-mac_x86_64-${CMDLINE_TOOLS_VERSION}_latest.zip`; const CMDLINE_TOOLS_URL_LINUX = `https://dl.google.com/android/repository/commandlinetools-linux-${CMDLINE_TOOLS_VERSION}_latest.zip`; +// keep in sync with CMDLINE_TOOLS_VERSION +const CMDLINE_TOOLS_MIN_MAJOR_REVISION = 23; +/** + * Returns the major revision of an installed cmdline-tools package from the content of its `source.properties`, or `null` if the revision cannot be determined. + */ +function parseCmdlineToolsMajorRevision(sourceProperties) { + const match = /^Pkg\.Revision=(\d+)/m.exec(sourceProperties); + return match ? Number(match[1]) : null; +} /** * Installs & updates the Android SDK for the macOS platform, including SDK platform for the chosen API level, latest build tools, platform tools, Android Emulator, * and the system image for the chosen API level, CPU arch, and target. @@ -54,10 +64,19 @@ async function installAndroidSdk(apiLevel, systemImageApiLevel, target, arch, ch const isOnMac = process.platform === 'darwin'; const isArm = process.arch === 'arm64'; const cmdlineToolsPath = `${process.env.ANDROID_HOME}/cmdline-tools`; - if (!fs.existsSync(cmdlineToolsPath)) { - console.log('Installing new cmdline-tools.'); + const sourcePropertiesPath = `${cmdlineToolsPath}/latest/source.properties`; + // a preinstalled cmdline-tools too old to parse minor API levels such as android-37.1 must be replaced, not kept + const installedRevision = fs.existsSync(sourcePropertiesPath) ? parseCmdlineToolsMajorRevision(fs.readFileSync(sourcePropertiesPath, 'utf8')) : null; + if (installedRevision === null || installedRevision < CMDLINE_TOOLS_MIN_MAJOR_REVISION) { + if (installedRevision === null) { + console.log('Installing new cmdline-tools.'); + } + else { + console.log(`Replacing cmdline-tools revision ${installedRevision} with revision ${CMDLINE_TOOLS_MIN_MAJOR_REVISION}.`); + } const sdkUrl = isOnMac ? CMDLINE_TOOLS_URL_MAC : CMDLINE_TOOLS_URL_LINUX; const downloadPath = await tc.downloadTool(sdkUrl); + await io.rmRF(`${cmdlineToolsPath}/latest`); await tc.extractZip(downloadPath, cmdlineToolsPath); await io.mv(`${cmdlineToolsPath}/cmdline-tools`, `${cmdlineToolsPath}/latest`); } diff --git a/src/sdk-installer.ts b/src/sdk-installer.ts index b284269d4..044156385 100644 --- a/src/sdk-installer.ts +++ b/src/sdk-installer.ts @@ -9,6 +9,16 @@ const BUILD_TOOLS_VERSION = '37.0.0'; const CMDLINE_TOOLS_VERSION = '16111833'; const CMDLINE_TOOLS_URL_MAC = `https://dl.google.com/android/repository/commandlinetools-mac_x86_64-${CMDLINE_TOOLS_VERSION}_latest.zip`; const CMDLINE_TOOLS_URL_LINUX = `https://dl.google.com/android/repository/commandlinetools-linux-${CMDLINE_TOOLS_VERSION}_latest.zip`; +// keep in sync with CMDLINE_TOOLS_VERSION +const CMDLINE_TOOLS_MIN_MAJOR_REVISION = 23; + +/** + * Returns the major revision of an installed cmdline-tools package from the content of its `source.properties`, or `null` if the revision cannot be determined. + */ +export function parseCmdlineToolsMajorRevision(sourceProperties: string): number | null { + const match = /^Pkg\.Revision=(\d+)/m.exec(sourceProperties); + return match ? Number(match[1]) : null; +} /** * Installs & updates the Android SDK for the macOS platform, including SDK platform for the chosen API level, latest build tools, platform tools, Android Emulator, @@ -30,10 +40,18 @@ export async function installAndroidSdk( const isArm = process.arch === 'arm64'; const cmdlineToolsPath = `${process.env.ANDROID_HOME}/cmdline-tools`; - if (!fs.existsSync(cmdlineToolsPath)) { - console.log('Installing new cmdline-tools.'); + const sourcePropertiesPath = `${cmdlineToolsPath}/latest/source.properties`; + // a preinstalled cmdline-tools too old to parse minor API levels such as android-37.1 must be replaced, not kept + const installedRevision = fs.existsSync(sourcePropertiesPath) ? parseCmdlineToolsMajorRevision(fs.readFileSync(sourcePropertiesPath, 'utf8')) : null; + if (installedRevision === null || installedRevision < CMDLINE_TOOLS_MIN_MAJOR_REVISION) { + if (installedRevision === null) { + console.log('Installing new cmdline-tools.'); + } else { + console.log(`Replacing cmdline-tools revision ${installedRevision} with revision ${CMDLINE_TOOLS_MIN_MAJOR_REVISION}.`); + } const sdkUrl = isOnMac ? CMDLINE_TOOLS_URL_MAC : CMDLINE_TOOLS_URL_LINUX; const downloadPath = await tc.downloadTool(sdkUrl); + await io.rmRF(`${cmdlineToolsPath}/latest`); await tc.extractZip(downloadPath, cmdlineToolsPath); await io.mv(`${cmdlineToolsPath}/cmdline-tools`, `${cmdlineToolsPath}/latest`); } From 361c5a79e9260b6d80a8fe2d1d4e0fd5b8acd600 Mon Sep 17 00:00:00 2001 From: Matan Baruch Date: Fri, 4 Sep 2026 04:11:08 +0300 Subject: [PATCH 2/2] Give the macos-15-intel job 20 minutes The job fits 15 minutes only when the AVD snapshot cache is warm. On a cold cache it has to generate the snapshot, and the first boot of API 31 on that runner takes 3-5 minutes on top of a cold Gradle build and the SDK install, which put the last run at 15m17s and cancelled it mid-test. Keep 15 minutes everywhere else; the Linux jobs finish in about 5. --- .github/workflows/main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c98e8c79b..70b5080c3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,7 +15,7 @@ jobs: runs-on: ${{ matrix.os }} env: JAVA_TOOL_OPTIONS: -Xmx4g - timeout-minutes: 15 + timeout-minutes: ${{ matrix.timeout-minutes || 15 }} strategy: matrix: include: @@ -31,6 +31,7 @@ jobs: api-level: 31 target: default arch: x86_64 + timeout-minutes: 20 - os: ubuntu-24.04 api-level: 34 target: aosp_atd