From 85c958dc7cdbc7e94d4c04e446aad77a462a95a6 Mon Sep 17 00:00:00 2001 From: Martin Guillon Date: Tue, 18 Aug 2026 22:13:59 +0200 Subject: [PATCH] feat(android): build only the ABIs of the devices being deployed to A `ns run android` with a single arm64 device still builds every ABI. This narrows the native build down to the ABIs of the devices it is about to deploy to, and picks the matching package when installing. - `GradleBuildService` passes `-PabiFilters=` built from the devices the build targets (honouring `--device`/`--emulator`). The app's gradle configuration decides what to do with it - typically an `ndk.abiFilters` or `splits` block in `App_Resources/Android/app.gradle`. An explicit `-PabiFilters` in `--gradleArgs` always wins. - `--no-filter-devices-arch` turns the narrowing off. `ns build` never narrows, since its artifact is meant to be shipped, and neither does an app bundle build, which carries every ABI anyway. - `Mobile.IDeviceInfo` gained `abis`, read on android from `ro.product.cpu.abilist64`/`abilist32`, falling back to `ro.product.cpu.abi` on old devices. - `AndroidProjectService.checkForChanges` marks the native project as changed when a connected device has no package of its own in the build output - a device that joins later would otherwise never get one, as the sources did not change. - `DeviceInstallAppService` installs the package matching the device's ABIs, falling back to the universal one and then to the newest package. - `copyLatestAppPackage` became `copyAppPackages`: a directory `--copy-to` target receives every package the build produced, a single file target receives the universal one. Co-Authored-By: Claude Opus 5 --- .../project/testing/debug-android.md | 1 + docs/man_pages/project/testing/run-android.md | 1 + lib/commands/build.ts | 7 +- lib/common/definitions/mobile.d.ts | 5 + lib/common/mobile/android/android-device.ts | 23 ++++ lib/controllers/build-controller.ts | 2 +- lib/data/build-data.ts | 4 + lib/declarations.d.ts | 6 + lib/definitions/build.d.ts | 3 +- lib/options.ts | 5 + lib/services/android-project-service.ts | 61 ++++++++- lib/services/android/gradle-build-service.ts | 46 +++++++ lib/services/build-artifacts-service.ts | 39 ++++-- .../device/device-install-app-service.ts | 46 ++++++- test/plugins-service.ts | 6 + test/services/android-project-service.ts | 6 + test/services/android/gradle-build-service.ts | 125 ++++++++++++++++++ 17 files changed, 368 insertions(+), 18 deletions(-) create mode 100644 test/services/android/gradle-build-service.ts diff --git a/docs/man_pages/project/testing/debug-android.md b/docs/man_pages/project/testing/debug-android.md index cd34d57c66..2d51cae53d 100644 --- a/docs/man_pages/project/testing/debug-android.md +++ b/docs/man_pages/project/testing/debug-android.md @@ -38,6 +38,7 @@ Attach the debug tools to a running app in the native emulator | `$ ns debug and * `--env.sourceMap` - creates inline source maps. * `--env.hiddenSourceMap` - creates sources maps in the root folder (useful for Crashlytics usage with bundled app in release). * `--aab` - Specifies that the command will produce and deploy an Android App Bundle. +* `--no-filter-devices-arch` - If set, builds every ABI instead of only the ones the connected devices report. The narrowing only applies when the app's gradle configuration acts on the `abiFilters` property, and `ns build` never narrows. * `--force` - If set, skips the application compatibility checks and forces `npm i` to ensure all dependencies are installed. Otherwise, the command will check the application compatibility with the current CLI version and could fail requiring `ns migrate`. <% if(isHtml) { %> diff --git a/docs/man_pages/project/testing/run-android.md b/docs/man_pages/project/testing/run-android.md index c895cd8103..e1ee430a8c 100644 --- a/docs/man_pages/project/testing/run-android.md +++ b/docs/man_pages/project/testing/run-android.md @@ -43,6 +43,7 @@ Start a default emulator if none are running, or run application on all connecte * `--env.sourceMap` - creates inline source maps. * `--env.hiddenSourceMap` - creates sources maps in the root folder (useful for Crashlytics usage with bundled app in release). * `--aab` - Specifies that the command will produce and deploy an Android App Bundle. +* `--no-filter-devices-arch` - If set, builds every ABI instead of only the ones the connected devices report. The narrowing only applies when the app's gradle configuration acts on the `abiFilters` property, and `ns build` never narrows. * `--force` - If set, skips the application compatibility checks and forces `npm i` to ensure all dependencies are installed. Otherwise, the command will check the application compatibility with the current CLI version and could fail requiring `ns migrate`. <% if(isHtml) { %> diff --git a/lib/commands/build.ts b/lib/commands/build.ts index 7216e8a3fc..4ce6c0ef72 100644 --- a/lib/commands/build.ts +++ b/lib/commands/build.ts @@ -53,7 +53,12 @@ export abstract class BuildCommandBase extends ValidatePlatformCommandBase { const buildData = this.$buildDataService.getBuildData( this.$projectData.projectDir, platform, - this.$options, + { + ...this.$options.argv, + // `ns build` produces an artifact meant to be shipped, so it must + // not be narrowed down to the ABIs of whatever is plugged in + filterDevicesArch: false, + }, ); const outputPath = await this.$buildController.prepareAndBuild(buildData); diff --git a/lib/common/definitions/mobile.d.ts b/lib/common/definitions/mobile.d.ts index a658dd0c17..c6d3c27b3a 100644 --- a/lib/common/definitions/mobile.d.ts +++ b/lib/common/definitions/mobile.d.ts @@ -100,6 +100,11 @@ declare global { * For iOS simulators - same as the identifier. */ imageIdentifier?: string; + /** + * Optional property listing the ABIs the device supports, most + * preferred first. Available for Android only. + */ + abis?: string[]; } interface IDeviceError extends Error, IDeviceIdentifier {} diff --git a/lib/common/mobile/android/android-device.ts b/lib/common/mobile/android/android-device.ts index b230e02827..ec17423c29 100644 --- a/lib/common/mobile/android/android-device.ts +++ b/lib/common/mobile/android/android-device.ts @@ -13,6 +13,9 @@ interface IAndroidDeviceDetails { name: string; release: string; brand: string; + "cpu.abi"?: string; + "cpu.abilist32"?: string; + "cpu.abilist64"?: string; } interface IAdbDeviceStatusInfo { @@ -96,6 +99,7 @@ export class AndroidDevice implements Mobile.IAndroidDevice { identifier: this.identifier, displayName: details.name, model: details.model, + abis: this.getAbis(details), version, vendor: details.brand, platform: this.$devicePlatformsConstants.Android, @@ -179,6 +183,25 @@ export class AndroidDevice implements Mobile.IAndroidDevice { return parsedDetails; } + // `ro.product.cpu.abilist64`/`abilist32` list every ABI the device supports, + // most preferred first. Old devices report neither and only have the single + // `ro.product.cpu.abi`. + private getAbis(details: IAndroidDeviceDetails): string[] { + const abis = [ + ...(details["cpu.abilist64"] || "").split(","), + ...(details["cpu.abilist32"] || "").split(",") + ] + .map((abi) => abi.trim()) + .filter((abi) => !!abi); + + if (abis.length) { + return abis; + } + + const abi = (details["cpu.abi"] || "").trim(); + return abi ? [abi] : []; + } + private getIsTablet(details: any): boolean { //version 3.x.x (also known as Honeycomb) is a tablet only version return ( diff --git a/lib/controllers/build-controller.ts b/lib/controllers/build-controller.ts index f5ebb1666c..291ed88f12 100644 --- a/lib/controllers/build-controller.ts +++ b/lib/controllers/build-controller.ts @@ -116,7 +116,7 @@ export class BuildController extends EventEmitter implements IBuildController { ); if (buildData.copyTo) { - this.$buildArtifactsService.copyLatestAppPackage( + this.$buildArtifactsService.copyAppPackages( buildData.copyTo, platformData, buildData diff --git a/lib/data/build-data.ts b/lib/data/build-data.ts index f6b2734174..d95f1e356a 100644 --- a/lib/data/build-data.ts +++ b/lib/data/build-data.ts @@ -51,6 +51,7 @@ export class AndroidBuildData extends BuildData { public keyStoreAliasPassword: string; public keyStorePassword: string; public androidBundle: boolean; + public buildFilterDevicesArch: boolean; public gradlePath: string; public gradleArgs: string; public hostProjectPath: string; @@ -63,6 +64,9 @@ export class AndroidBuildData extends BuildData { this.keyStoreAliasPassword = data.keyStoreAliasPassword; this.keyStorePassword = data.keyStorePassword; this.androidBundle = data.androidBundle || data.aab; + // an app bundle already carries every ABI, so there is nothing to filter + this.buildFilterDevicesArch = + !this.androidBundle && data.filterDevicesArch !== false; this.gradlePath = data.gradlePath; this.gradleArgs = data.gradleArgs; this.hostProjectPath = data.hostProjectPath; diff --git a/lib/declarations.d.ts b/lib/declarations.d.ts index 8e58d23875..1a494560d6 100644 --- a/lib/declarations.d.ts +++ b/lib/declarations.d.ts @@ -571,6 +571,12 @@ interface IEmbedOptions { } interface IAndroidOptions extends IEmbedOptions { + /** + * When true (the default) `ns run`/`ns debug` restrict the native build to + * the ABIs of the devices it is about to deploy to. Pass + * `--no-filter-devices-arch` to always build every ABI. + */ + filterDevicesArch: boolean; gradlePath: string; gradleArgs: string; } diff --git a/lib/definitions/build.d.ts b/lib/definitions/build.d.ts index e64a318c6d..ab9c5bf350 100644 --- a/lib/definitions/build.d.ts +++ b/lib/definitions/build.d.ts @@ -31,6 +31,7 @@ interface IAndroidBuildData extends IBuildData, IAndroidSigningData, IHasAndroidBundle { + buildFilterDevicesArch?: boolean; gradlePath?: string; gradleArgs?: string; } @@ -62,7 +63,7 @@ interface IBuildArtifactsService { platformData: IPlatformData, buildOutputOptions: IBuildOutputOptions ): Promise; - copyLatestAppPackage( + copyAppPackages( targetPath: string, platformData: IPlatformData, buildOutputOptions: IBuildOutputOptions diff --git a/lib/options.ts b/lib/options.ts index 64a4e644e7..1e95653dd2 100644 --- a/lib/options.ts +++ b/lib/options.ts @@ -219,6 +219,11 @@ export class Options { default: false, hasSensitiveValue: false, }, + filterDevicesArch: { + type: OptionType.Boolean, + default: true, + hasSensitiveValue: false, + }, gradlePath: { type: OptionType.String, hasSensitiveValue: false }, gradleArgs: { type: OptionType.String, hasSensitiveValue: false }, hostProjectPath: { type: OptionType.String, hasSensitiveValue: false }, diff --git a/lib/services/android-project-service.ts b/lib/services/android-project-service.ts index b0e53a53e5..851c305816 100644 --- a/lib/services/android-project-service.ts +++ b/lib/services/android-project-service.ts @@ -47,6 +47,8 @@ import { import { IInjector } from "../common/definitions/yok"; import { injector } from "../common/yok"; import { INotConfiguredEnvOptions } from "../common/definitions/commands"; +import { AndroidPrepareData } from "../data/prepare-data"; +import { IProjectChangesInfo } from "../definitions/project-changes"; interface NativeDependency { name: string; @@ -148,7 +150,9 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject private $androidPluginBuildService: IAndroidPluginBuildService, private $platformEnvironmentRequirements: IPlatformEnvironmentRequirements, private $androidResourcesMigrationService: IAndroidResourcesMigrationService, + private $devicesService: Mobile.IDevicesService, private $filesHashService: IFilesHashService, + private $liveSyncProcessDataService: ILiveSyncProcessDataService, private $gradleCommandService: IGradleCommandService, private $gradleBuildService: IGradleBuildService, private $analyticsService: IAnalyticsService @@ -835,8 +839,61 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject await adb.executeShellCommand(["rm", "-rf", deviceRootPath]); } - public async checkForChanges(): Promise { - // Nothing android specific to check yet. + /** + * When the native build is narrowed down to the ABIs of the connected + * devices, a device that joins later has no package of its own in the build + * output. Nothing else would trigger a native rebuild for it - the sources + * did not change - so flag it here. + */ + public async checkForChanges( + changesInfo: IProjectChangesInfo, + prepareData: AndroidPrepareData, + projectData: IProjectData + ): Promise { + if (changesInfo.nativeChanged) { + return; + } + + const platformData = this.getPlatformData(projectData); + const deviceDescriptors = this.$liveSyncProcessDataService.getDeviceDescriptors( + projectData.projectDir + ); + + for (const deviceDescriptor of deviceDescriptors) { + const buildData = deviceDescriptor.buildData; + if (!buildData || !buildData.buildFilterDevicesArch) { + continue; + } + + const packagesOutputPath = platformData.getBuildOutputPath(buildData); + if (!this.$fs.exists(packagesOutputPath)) { + continue; + } + + const builtPackages = this.$fs.readDirectory(packagesOutputPath); + // a universal package runs on every device, nothing to rebuild + if (_.some(builtPackages, (f) => f.indexOf("universal") !== -1)) { + continue; + } + + const device = _.find( + this.$devicesService.getDevicesForPlatform(buildData.platform), + (d) => d.deviceInfo.identifier === deviceDescriptor.identifier + ); + const abi = device && (device.deviceInfo.abis || [])[0]; + if (!abi) { + continue; + } + + const abiRegex = new RegExp(`${abi}.*\\.apk$`); + if (!_.some(builtPackages, (entry) => abiRegex.test(entry))) { + this.$logger.trace( + `No package was built for '${abi}', marking the native project as changed.` + ); + changesInfo.nativeChanged = true; + return; + } + } } public getDeploymentTarget(projectData: IProjectData): semver.SemVer { diff --git a/lib/services/android/gradle-build-service.ts b/lib/services/android/gradle-build-service.ts index 4ce97ab89a..820740e133 100644 --- a/lib/services/android/gradle-build-service.ts +++ b/lib/services/android/gradle-build-service.ts @@ -9,12 +9,14 @@ import { import { IAndroidBuildData } from "../../definitions/build"; import { IChildProcess } from "../../common/declarations"; import { injector } from "../../common/yok"; +import * as _ from "lodash"; export class GradleBuildService extends EventEmitter implements IGradleBuildService { constructor( private $childProcess: IChildProcess, + private $devicesService: Mobile.IDevicesService, private $gradleBuildArgsService: IGradleBuildArgsService, private $gradleCommandService: IGradleCommandService ) { @@ -28,6 +30,9 @@ export class GradleBuildService const buildTaskArgs = await this.$gradleBuildArgsService.getBuildTaskArgs( buildData ); + + this.applyDevicesAbiFilter(buildTaskArgs, buildData); + const spawnOptions = { emitOptions: { eventName: constants.BUILD_OUTPUT_EVENT_NAME }, throwError: true, @@ -51,6 +56,47 @@ export class GradleBuildService ); } + /** + * Narrows the native build down to the ABIs of the devices this build is + * about to be deployed to. The app's gradle configuration decides what to do + * with `abiFilters` - typically an `ndk.abiFilters`/`splits` block in + * `App_Resources/Android/app.gradle`. An explicitly passed `-PabiFilters` + * always wins. + */ + private applyDevicesAbiFilter( + buildTaskArgs: string[], + buildData: IAndroidBuildData + ): void { + if (!buildData.buildFilterDevicesArch) { + return; + } + + if (_.some(buildTaskArgs, (arg) => arg.startsWith("-PabiFilters"))) { + return; + } + + let devices = this.$devicesService.getDevicesForPlatform( + buildData.platform + ); + if (buildData.device) { + devices = devices.filter( + (d) => d.deviceInfo.identifier === buildData.device + ); + } else if (buildData.emulator) { + devices = devices.filter((d) => d.isEmulator); + } + + const abis = _.uniq( + devices + .map((d) => (d.deviceInfo.abis || [])[0]) + .filter((abi) => !!abi) + ); + + if (abis.length) { + buildTaskArgs.push(`-PabiFilters=${abis.join(",")}`); + } + } + public async cleanProject( projectRoot: string, buildData: IAndroidBuildData diff --git a/lib/services/build-artifacts-service.ts b/lib/services/build-artifacts-service.ts index 1a2bfc94af..04df64f8cf 100644 --- a/lib/services/build-artifacts-service.ts +++ b/lib/services/build-artifacts-service.ts @@ -75,7 +75,12 @@ export class BuildArtifactsService implements IBuildArtifactsService { return []; } - public copyLatestAppPackage( + /** + * Copies what the build produced to `targetPath`. A build can produce more + * than one package - an app split per ABI - so a directory target receives + * all of them, while a single file target receives the universal one. + */ + public copyAppPackages( targetPath: string, platformData: IPlatformData, buildOutputOptions: IBuildOutputOptions @@ -85,26 +90,36 @@ export class BuildArtifactsService implements IBuildArtifactsService { const outputPath = buildOutputOptions.outputPath || platformData.getBuildOutputPath(buildOutputOptions); - const applicationPackage = this.getLatestApplicationPackage( + const applicationPackages = this.getAllAppPackages( outputPath, platformData.getValidBuildOutputData(buildOutputOptions) ); - const packageFile = applicationPackage.packageName; this.$fs.ensureDirectoryExists(path.dirname(targetPath)); - if ( - this.$fs.exists(targetPath) && - this.$fs.getFsStats(targetPath).isDirectory() - ) { - const sourceFileName = path.basename(packageFile); + const targetIsDirectory = + (this.$fs.exists(targetPath) && + this.$fs.getFsStats(targetPath).isDirectory()) || + !path.extname(targetPath); + + let packagesToCopy = applicationPackages; + if (!targetIsDirectory && applicationPackages.length > 1) { this.$logger.trace( - `Specified target path: '${targetPath}' is directory. Same filename will be used: '${sourceFileName}'.` + `Specified target path: '${targetPath}' is a single file, but the build produced ${applicationPackages.length} packages. Only the universal one will be copied.` + ); + packagesToCopy = applicationPackages.filter((pack) => + path.basename(pack.packageName).includes("universal") ); - targetPath = path.join(targetPath, sourceFileName); } - this.$fs.copyFile(packageFile, targetPath); - this.$logger.info(`Copied file '${packageFile}' to '${targetPath}'.`); + + _.each(packagesToCopy, (pack) => { + const packageFile = pack.packageName; + const targetFilePath = targetIsDirectory + ? path.join(targetPath, path.basename(packageFile)) + : targetPath; + this.$fs.copyFile(packageFile, targetFilePath); + this.$logger.info(`Copied file '${packageFile}' to '${targetFilePath}'.`); + }); } private getLatestApplicationPackage( diff --git a/lib/services/device/device-install-app-service.ts b/lib/services/device/device-install-app-service.ts index 4c5d16b6f3..798917e892 100644 --- a/lib/services/device/device-install-app-service.ts +++ b/lib/services/device/device-install-app-service.ts @@ -51,7 +51,8 @@ export class DeviceInstallAppService { }); if (!packageFile) { - packageFile = await this.$buildArtifactsService.getLatestAppPackagePath( + packageFile = await this.getPackageForDevice( + device, platformData, buildData ); @@ -92,6 +93,49 @@ export class DeviceInstallAppService { ); } + /** + * A build narrowed down to the connected devices' ABIs produces one package + * per ABI, so the one this device can run has to be picked. Falls back to + * the universal package and then to the newest one, which is what a build + * that was not split produces anyway. + */ + private async getPackageForDevice( + device: Mobile.IDevice, + platformData: IPlatformData, + buildData: IBuildData + ): Promise { + const outputPath = + buildData.outputPath || platformData.getBuildOutputPath(buildData); + const packages = this.$buildArtifactsService.getAllAppPackages( + outputPath, + platformData.getValidBuildOutputData(buildData) + ); + + if (packages.length > 1) { + const abis = device.deviceInfo.abis || []; + for (const abi of abis) { + const match = packages.find((p) => + path.basename(p.packageName).includes(abi) + ); + if (match) { + return match.packageName; + } + } + + const universalPackage = packages.find((p) => + path.basename(p.packageName).includes("universal") + ); + if (universalPackage) { + return universalPackage.packageName; + } + } + + return this.$buildArtifactsService.getLatestAppPackagePath( + platformData, + buildData + ); + } + public async installOnDeviceIfNeeded( device: Mobile.IDevice, buildData: IBuildData, diff --git a/test/plugins-service.ts b/test/plugins-service.ts index 85bc40641d..4393a66817 100644 --- a/test/plugins-service.ts +++ b/test/plugins-service.ts @@ -185,6 +185,12 @@ function createTestInjector() { ); testInjector.register("platformEnvironmentRequirements", {}); + testInjector.register("devicesService", { + getDevicesForPlatform: (): Mobile.IDevice[] => [], + }); + testInjector.register("liveSyncProcessDataService", { + getDeviceDescriptors: (): ILiveSyncDeviceDescriptor[] => [], + }); testInjector.register("filesHashService", { hasChangesInShasums: ( oldPluginNativeHashes: IStringDictionary, diff --git a/test/services/android-project-service.ts b/test/services/android-project-service.ts index 1961548012..f775907e38 100644 --- a/test/services/android-project-service.ts +++ b/test/services/android-project-service.ts @@ -38,6 +38,12 @@ const createTestInjector = (): IInjector => { testInjector.register("filesHashService", { saveHashesForProject: () => ({}), }); + testInjector.register("devicesService", { + getDevicesForPlatform: (): Mobile.IDevice[] => [], + }); + testInjector.register("liveSyncProcessDataService", { + getDeviceDescriptors: (): ILiveSyncDeviceDescriptor[] => [], + }); testInjector.register("androidPluginBuildService", {}); testInjector.register("errors", stubs.ErrorsStub); testInjector.register("logger", stubs.LoggerStub); diff --git a/test/services/android/gradle-build-service.ts b/test/services/android/gradle-build-service.ts new file mode 100644 index 0000000000..d3185d9ba2 --- /dev/null +++ b/test/services/android/gradle-build-service.ts @@ -0,0 +1,125 @@ +import { Yok } from "../../../lib/common/yok"; +import { GradleBuildService } from "../../../lib/services/android/gradle-build-service"; +import { assert } from "chai"; +import { IInjector } from "../../../lib/common/definitions/yok"; +import { IAndroidBuildData } from "../../../lib/definitions/build"; + +const createDevice = ( + identifier: string, + abis: string[], + isEmulator = false, +): any => ({ + deviceInfo: { identifier, abis, platform: "android" }, + isEmulator, +}); + +function createTestInjector(devices: any[]): IInjector { + const injector = new Yok(); + injector.register("childProcess", { + on: (): void => undefined, + removeListener: (): void => undefined, + }); + injector.register("devicesService", { + getDevicesForPlatform: () => devices, + }); + injector.register("gradleBuildArgsService", { + getBuildTaskArgs: async () => ["assembleDebug"], + getCleanTaskArgs: () => ["clean"], + getBuildLoggingArgs: (): string[] => [], + }); + injector.register("gradleCommandService", { + executeCommand: async (args: string[]): Promise => { + executedArgs = args; + return null; + }, + }); + injector.register("gradleBuildService", GradleBuildService); + + return injector; +} + +let executedArgs: string[] = null; + +const buildProject = async ( + devices: any[], + buildData: Partial, +): Promise => { + executedArgs = null; + const injector = createTestInjector(devices); + const gradleBuildService = injector.resolve("gradleBuildService"); + await gradleBuildService.buildProject("projectRoot", { + platform: "android", + ...buildData, + }); + + return executedArgs; +}; + +describe("GradleBuildService", () => { + describe("abi filtering", () => { + it("passes the abis of the connected devices", async () => { + const args = await buildProject( + [ + createDevice("device1", ["arm64-v8a", "armeabi-v7a"]), + createDevice("emulator1", ["x86_64", "x86"], true), + ], + { buildFilterDevicesArch: true }, + ); + + assert.include(args, "-PabiFilters=arm64-v8a,x86_64"); + }); + + it("passes the abi of the selected device only", async () => { + const args = await buildProject( + [ + createDevice("device1", ["arm64-v8a"]), + createDevice("emulator1", ["x86_64"], true), + ], + { buildFilterDevicesArch: true, device: "device1" }, + ); + + assert.include(args, "-PabiFilters=arm64-v8a"); + }); + + it("passes the abis of the emulators only when --emulator is used", async () => { + const args = await buildProject( + [ + createDevice("device1", ["arm64-v8a"]), + createDevice("emulator1", ["x86_64"], true), + ], + { buildFilterDevicesArch: true, emulator: true }, + ); + + assert.include(args, "-PabiFilters=x86_64"); + }); + + it("deduplicates the abis", async () => { + const args = await buildProject( + [ + createDevice("device1", ["arm64-v8a"]), + createDevice("device2", ["arm64-v8a"]), + ], + { buildFilterDevicesArch: true }, + ); + + assert.include(args, "-PabiFilters=arm64-v8a"); + }); + + it("passes nothing when the filtering is off", async () => { + const args = await buildProject( + [createDevice("device1", ["arm64-v8a"])], + { buildFilterDevicesArch: false }, + ); + + assert.isUndefined(args.find((a) => a.startsWith("-PabiFilters"))); + }); + + it("passes nothing when no device reports its abis", async () => { + const args = await buildProject([createDevice("device1", [])], { + buildFilterDevicesArch: true, + }); + + assert.isUndefined(args.find((a) => a.startsWith("-PabiFilters"))); + }); + }); +});