diff --git a/docs/man_pages/project/testing/debug-android.md b/docs/man_pages/project/testing/debug-android.md index cd34d57c66..34f4bb201c 100644 --- a/docs/man_pages/project/testing/debug-android.md +++ b/docs/man_pages/project/testing/debug-android.md @@ -38,6 +38,8 @@ 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. +* `--filter-plugins-devices-arch` - If set, the ABIs of the connected devices are also passed to the gradle build of every plugin built from source. Nothing in the gradle files the CLI generates for a plugin acts on them - this is for a plugin whose own `include.gradle` reads the `abiFilters` property to shorten a long native build. * `--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..7d674dc96f 100644 --- a/docs/man_pages/project/testing/run-android.md +++ b/docs/man_pages/project/testing/run-android.md @@ -43,6 +43,8 @@ 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. +* `--filter-plugins-devices-arch` - If set, the ABIs of the connected devices are also passed to the gradle build of every plugin built from source. Nothing in the gradle files the CLI generates for a plugin acts on them - this is for a plugin whose own `include.gradle` reads the `abiFilters` property to shorten a long native build. * `--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..819e1f5eb0 100644 --- a/lib/declarations.d.ts +++ b/lib/declarations.d.ts @@ -571,6 +571,20 @@ 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; + + /** + * When true, the same ABIs are passed to the gradle build of every plugin + * that is built from source. Off by default - the CLI's own plugin gradle + * files ignore the property, only a plugin acting on it in its + * `include.gradle` gains anything from it. + */ + filterPluginsDevicesArch: boolean; gradlePath: string; gradleArgs: string; } diff --git a/lib/definitions/android-plugin-migrator.d.ts b/lib/definitions/android-plugin-migrator.d.ts index f5ad873307..646e8930a5 100644 --- a/lib/definitions/android-plugin-migrator.d.ts +++ b/lib/definitions/android-plugin-migrator.d.ts @@ -12,6 +12,7 @@ interface IAndroidBuildOptions { tempPluginDirPath: string; gradlePath?: string; gradleArgs?: string; + abiFilters?: string[]; } interface IAndroidPluginBuildService { @@ -49,4 +50,13 @@ interface IBuildAndroidPluginData extends Partial { * Optional custom Gradle arguments. */ gradleArgs?: string; + + /** + * The ABIs the build this plugin is prepared for is about to deploy to, + * passed to the plugin build as `-PabiFilters`. Nothing in the gradle files + * the CLI generates for a plugin acts on it - it is there for a plugin whose + * own `include.gradle` reads the property to skip the ABIs the build does + * not need. + */ + abiFilters?: 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..9a6669eee2 100644 --- a/lib/options.ts +++ b/lib/options.ts @@ -219,6 +219,16 @@ export class Options { default: false, hasSensitiveValue: false, }, + filterDevicesArch: { + type: OptionType.Boolean, + default: true, + hasSensitiveValue: false, + }, + filterPluginsDevicesArch: { + type: OptionType.Boolean, + default: false, + 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-plugin-build-service.ts b/lib/services/android-plugin-build-service.ts index 88098d55b0..33ab22e8a1 100644 --- a/lib/services/android-plugin-build-service.ts +++ b/lib/services/android-plugin-build-service.ts @@ -61,6 +61,8 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { private $watchIgnoreListService: IWatchIgnoreListService, ) {} + private static ABI_FILTERS_BUILD_DATA_KEY = "__abiFilters"; + private static MANIFEST_ROOT = { $: { "xmlns:android": "http://schemas.android.com/apk/res/android", @@ -233,6 +235,16 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { shortPluginName, ); + // the aar of a plugin built for a subset of the ABIs is not the aar of the + // same sources built for another subset, so the ABIs take part in the + // decision to rebuild - the sources alone would not change when a device + // with another ABI joins the run. + if (options.abiFilters && options.abiFilters.length) { + pluginSourceFileHashesInfo[ + AndroidPluginBuildService.ABI_FILTERS_BUILD_DATA_KEY + ] = options.abiFilters.join(","); + } + const shouldBuildAar = await this.shouldBuildAar({ manifestFilePath, androidSourceDirectories, @@ -264,6 +276,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { await this.buildPlugin({ gradlePath: options.gradlePath, gradleArgs: options.gradleArgs, + abiFilters: options.abiFilters, pluginDir: pluginTempDir, pluginName: options.pluginName, projectDir: options.projectDir, @@ -821,6 +834,19 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { localArgs.push(pluginBuildSettings.gradleArgs); } + // nothing in the gradle files generated here acts on `abiFilters` - it is + // passed for a plugin whose own include.gradle reads it to narrow a long + // native build down. An explicit `-PabiFilters` in the gradle args wins. + if ( + pluginBuildSettings.abiFilters && + pluginBuildSettings.abiFilters.length && + (pluginBuildSettings.gradleArgs || "").indexOf("-PabiFilters") === -1 + ) { + localArgs.push( + `-PabiFilters=${pluginBuildSettings.abiFilters.join(",")}` + ); + } + if (this.$logger.getLevel() === "INFO") { localArgs.push("--quiet"); } diff --git a/lib/services/android-project-service.ts b/lib/services/android-project-service.ts index b0e53a53e5..b4218ea66b 100644 --- a/lib/services/android-project-service.ts +++ b/lib/services/android-project-service.ts @@ -47,6 +47,9 @@ 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"; +import { getDevicesAbis } from "./android/devices-abis"; interface NativeDependency { name: string; @@ -148,7 +151,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 @@ -691,6 +696,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject const options: IPluginBuildOptions = { gradlePath: this.$options.gradlePath, gradleArgs: this.$options.gradleArgs, + abiFilters: this.getPluginsAbiFilters(), projectDir: projectData.projectDir, pluginName: pluginData.name, platformsAndroidDirPath: pluginPlatformsFolderPath, @@ -706,6 +712,27 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject } } + /** + * The ABIs passed to the gradle build of a plugin built from source. Opt-in + * (`--filter-plugins-devices-arch`): nothing in the gradle files the CLI + * generates for a plugin acts on `abiFilters`, so this is only useful for a + * plugin whose own `include.gradle` reads the property - a long native build + * can then skip the ABIs this run is not going to deploy to. + */ + private getPluginsAbiFilters(): string[] { + if (!this.$options.filterPluginsDevicesArch) { + return null; + } + + const abis = getDevicesAbis( + this.$devicesService, + this.$devicePlatformsConstants.Android, + { device: this.$options.device, emulator: this.$options.emulator } + ); + + return abis.length ? abis : null; + } + public async processConfigurationFilesFromAppResources(): Promise { return; } @@ -835,8 +862,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/devices-abis.ts b/lib/services/android/devices-abis.ts new file mode 100644 index 0000000000..f26f7ef4cc --- /dev/null +++ b/lib/services/android/devices-abis.ts @@ -0,0 +1,23 @@ +import * as _ from "lodash"; + +/** + * The ABIs of the devices a build is about to be deployed to - the first (most + * preferred) ABI of every device, deduplicated. `device`/`emulator` narrow the + * set down the same way they narrow the run itself. + */ +export function getDevicesAbis( + $devicesService: Mobile.IDevicesService, + platform: string, + filter: { device?: string; emulator?: boolean } = {} +): string[] { + let devices = $devicesService.getDevicesForPlatform(platform); + if (filter.device) { + devices = devices.filter((d) => d.deviceInfo.identifier === filter.device); + } else if (filter.emulator) { + devices = devices.filter((d) => d.isEmulator); + } + + return _.uniq( + devices.map((d) => (d.deviceInfo.abis || [])[0]).filter((abi) => !!abi) + ); +} diff --git a/lib/services/android/gradle-build-service.ts b/lib/services/android/gradle-build-service.ts index 4ce97ab89a..93b57c8e72 100644 --- a/lib/services/android/gradle-build-service.ts +++ b/lib/services/android/gradle-build-service.ts @@ -9,12 +9,15 @@ import { import { IAndroidBuildData } from "../../definitions/build"; import { IChildProcess } from "../../common/declarations"; import { injector } from "../../common/yok"; +import { getDevicesAbis } from "./devices-abis"; +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 +31,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 +57,35 @@ 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; + } + + const abis = getDevicesAbis(this.$devicesService, buildData.platform, { + device: buildData.device, + emulator: buildData.emulator, + }); + + 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..f84e413103 100644 --- a/test/services/android-project-service.ts +++ b/test/services/android-project-service.ts @@ -19,6 +19,7 @@ import { IFileSystem, IProjectDir, } from "../../lib/common/declarations"; +import { IPluginBuildOptions } from "../../lib/definitions/android-plugin-migrator"; const createTestInjector = (): IInjector => { const testInjector = new Yok(); @@ -38,6 +39,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); @@ -404,3 +411,91 @@ describe("androidProjectService", () => { }); }); }); + +describe("androidProjectService plugins abi filtering", () => { + const createDevice = ( + identifier: string, + abis: string[], + isEmulator = false + ): any => ({ + deviceInfo: { identifier, abis, platform: "android" }, + isEmulator, + }); + + const preparePluginNativeCode = async ( + options: any, + devices: any[] + ): Promise => { + const testInjector = createTestInjector(); + let pluginBuildOptions: IPluginBuildOptions = null; + testInjector.register("androidPluginBuildService", { + buildAar: async (opts: IPluginBuildOptions): Promise => { + pluginBuildOptions = opts; + return false; + }, + migrateIncludeGradle: (): boolean => false, + }); + testInjector.register("options", { + hostProjectModuleName: "app", + ...options, + }); + testInjector.register("devicesService", { + getDevicesForPlatform: (): any[] => devices, + }); + testInjector.register("devicePlatformsConstants", { Android: "Android" }); + + const androidProjectService: IPlatformProjectService = testInjector.resolve( + "androidProjectService" + ); + await androidProjectService.preparePluginNativeCode( + { + name: "my-plugin", + pluginPlatformsFolderPath: (): string => "pluginPlatformsDir", + }, + { projectDir: "projectDir", platformsDir: "platformsDir" } + ); + + return pluginBuildOptions; + }; + + it("passes the abis of the connected devices when the option is set", async () => { + const options = await preparePluginNativeCode( + { filterPluginsDevicesArch: true }, + [ + createDevice("device1", ["arm64-v8a", "armeabi-v7a"]), + createDevice("emulator1", ["x86_64", "x86"], true), + ] + ); + + assert.deepStrictEqual(options.abiFilters, ["arm64-v8a", "x86_64"]); + }); + + it("passes the abi of the selected device only", async () => { + const options = await preparePluginNativeCode( + { filterPluginsDevicesArch: true, device: "device1" }, + [ + createDevice("device1", ["arm64-v8a"]), + createDevice("emulator1", ["x86_64"], true), + ] + ); + + assert.deepStrictEqual(options.abiFilters, ["arm64-v8a"]); + }); + + it("passes no abis when the option is not set", async () => { + const options = await preparePluginNativeCode({}, [ + createDevice("device1", ["arm64-v8a"]), + ]); + + assert.isNull(options.abiFilters); + }); + + it("passes no abis when no device reports its abis", async () => { + const options = await preparePluginNativeCode( + { filterPluginsDevicesArch: true }, + [createDevice("device1", [])] + ); + + assert.isNull(options.abiFilters); + }); +}); 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"))); + }); + }); +});