Skip to content

Commit 5ecb7c0

Browse files
farfromrefugclaude
andcommitted
feat(android): optionally pass the device ABIs to plugin builds
`--filter-plugins-devices-arch` passes the same `-PabiFilters` the app build gets to the gradle build of every plugin built from source. Nothing in the gradle files the CLI generates for a plugin acts on the property, and this deliberately does not add such a block: what a plugin's native sources need per ABI is the plugin's business. It is there for a plugin whose own `include.gradle` reads `abiFilters` - a plugin with a long native build (an NDK/CMake one, say) can then build only the ABIs this run is about to deploy to instead of all four. Off by default. A narrowed aar is a partial artifact and the aar cache is keyed by the plugin sources, which do not change when a device with another ABI joins, so the ABIs are now part of the plugin build data the rebuild decision reads. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 85c958d commit 5ecb7c0

10 files changed

Lines changed: 191 additions & 16 deletions

File tree

docs/man_pages/project/testing/debug-android.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Attach the debug tools to a running app in the native emulator | `$ ns debug and
3939
* `--env.hiddenSourceMap` - creates sources maps in the root folder (useful for Crashlytics usage with bundled app in release).
4040
* `--aab` - Specifies that the command will produce and deploy an Android App Bundle.
4141
* `--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.
42+
* `--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.
4243
* `--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`.
4344

4445
<% if(isHtml) { %>

docs/man_pages/project/testing/run-android.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Start a default emulator if none are running, or run application on all connecte
4444
* `--env.hiddenSourceMap` - creates sources maps in the root folder (useful for Crashlytics usage with bundled app in release).
4545
* `--aab` - Specifies that the command will produce and deploy an Android App Bundle.
4646
* `--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.
47+
* `--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.
4748
* `--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`.
4849

4950
<% if(isHtml) { %>

lib/declarations.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,14 @@ interface IAndroidOptions extends IEmbedOptions {
577577
* `--no-filter-devices-arch` to always build every ABI.
578578
*/
579579
filterDevicesArch: boolean;
580+
581+
/**
582+
* When true, the same ABIs are passed to the gradle build of every plugin
583+
* that is built from source. Off by default - the CLI's own plugin gradle
584+
* files ignore the property, only a plugin acting on it in its
585+
* `include.gradle` gains anything from it.
586+
*/
587+
filterPluginsDevicesArch: boolean;
580588
gradlePath: string;
581589
gradleArgs: string;
582590
}

lib/definitions/android-plugin-migrator.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ interface IAndroidBuildOptions {
1212
tempPluginDirPath: string;
1313
gradlePath?: string;
1414
gradleArgs?: string;
15+
abiFilters?: string[];
1516
}
1617

1718
interface IAndroidPluginBuildService {
@@ -49,4 +50,13 @@ interface IBuildAndroidPluginData extends Partial<IProjectDir> {
4950
* Optional custom Gradle arguments.
5051
*/
5152
gradleArgs?: string;
53+
54+
/**
55+
* The ABIs the build this plugin is prepared for is about to deploy to,
56+
* passed to the plugin build as `-PabiFilters`. Nothing in the gradle files
57+
* the CLI generates for a plugin acts on it - it is there for a plugin whose
58+
* own `include.gradle` reads the property to skip the ABIs the build does
59+
* not need.
60+
*/
61+
abiFilters?: string[];
5262
}

lib/options.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,11 @@ export class Options {
224224
default: true,
225225
hasSensitiveValue: false,
226226
},
227+
filterPluginsDevicesArch: {
228+
type: OptionType.Boolean,
229+
default: false,
230+
hasSensitiveValue: false,
231+
},
227232
gradlePath: { type: OptionType.String, hasSensitiveValue: false },
228233
gradleArgs: { type: OptionType.String, hasSensitiveValue: false },
229234
hostProjectPath: { type: OptionType.String, hasSensitiveValue: false },

lib/services/android-plugin-build-service.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService {
6161
private $watchIgnoreListService: IWatchIgnoreListService,
6262
) {}
6363

64+
private static ABI_FILTERS_BUILD_DATA_KEY = "__abiFilters";
65+
6466
private static MANIFEST_ROOT = {
6567
$: {
6668
"xmlns:android": "http://schemas.android.com/apk/res/android",
@@ -233,6 +235,16 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService {
233235
shortPluginName,
234236
);
235237

238+
// the aar of a plugin built for a subset of the ABIs is not the aar of the
239+
// same sources built for another subset, so the ABIs take part in the
240+
// decision to rebuild - the sources alone would not change when a device
241+
// with another ABI joins the run.
242+
if (options.abiFilters && options.abiFilters.length) {
243+
pluginSourceFileHashesInfo[
244+
AndroidPluginBuildService.ABI_FILTERS_BUILD_DATA_KEY
245+
] = options.abiFilters.join(",");
246+
}
247+
236248
const shouldBuildAar = await this.shouldBuildAar({
237249
manifestFilePath,
238250
androidSourceDirectories,
@@ -264,6 +276,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService {
264276
await this.buildPlugin({
265277
gradlePath: options.gradlePath,
266278
gradleArgs: options.gradleArgs,
279+
abiFilters: options.abiFilters,
267280
pluginDir: pluginTempDir,
268281
pluginName: options.pluginName,
269282
projectDir: options.projectDir,
@@ -821,6 +834,19 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService {
821834
localArgs.push(pluginBuildSettings.gradleArgs);
822835
}
823836

837+
// nothing in the gradle files generated here acts on `abiFilters` - it is
838+
// passed for a plugin whose own include.gradle reads it to narrow a long
839+
// native build down. An explicit `-PabiFilters` in the gradle args wins.
840+
if (
841+
pluginBuildSettings.abiFilters &&
842+
pluginBuildSettings.abiFilters.length &&
843+
(pluginBuildSettings.gradleArgs || "").indexOf("-PabiFilters") === -1
844+
) {
845+
localArgs.push(
846+
`-PabiFilters=${pluginBuildSettings.abiFilters.join(",")}`
847+
);
848+
}
849+
824850
if (this.$logger.getLevel() === "INFO") {
825851
localArgs.push("--quiet");
826852
}

lib/services/android-project-service.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import { injector } from "../common/yok";
4949
import { INotConfiguredEnvOptions } from "../common/definitions/commands";
5050
import { AndroidPrepareData } from "../data/prepare-data";
5151
import { IProjectChangesInfo } from "../definitions/project-changes";
52+
import { getDevicesAbis } from "./android/devices-abis";
5253

5354
interface NativeDependency {
5455
name: string;
@@ -695,6 +696,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
695696
const options: IPluginBuildOptions = {
696697
gradlePath: this.$options.gradlePath,
697698
gradleArgs: this.$options.gradleArgs,
699+
abiFilters: this.getPluginsAbiFilters(),
698700
projectDir: projectData.projectDir,
699701
pluginName: pluginData.name,
700702
platformsAndroidDirPath: pluginPlatformsFolderPath,
@@ -710,6 +712,27 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
710712
}
711713
}
712714

715+
/**
716+
* The ABIs passed to the gradle build of a plugin built from source. Opt-in
717+
* (`--filter-plugins-devices-arch`): nothing in the gradle files the CLI
718+
* generates for a plugin acts on `abiFilters`, so this is only useful for a
719+
* plugin whose own `include.gradle` reads the property - a long native build
720+
* can then skip the ABIs this run is not going to deploy to.
721+
*/
722+
private getPluginsAbiFilters(): string[] {
723+
if (!this.$options.filterPluginsDevicesArch) {
724+
return null;
725+
}
726+
727+
const abis = getDevicesAbis(
728+
this.$devicesService,
729+
this.$devicePlatformsConstants.Android,
730+
{ device: this.$options.device, emulator: this.$options.emulator }
731+
);
732+
733+
return abis.length ? abis : null;
734+
}
735+
713736
public async processConfigurationFilesFromAppResources(): Promise<void> {
714737
return;
715738
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as _ from "lodash";
2+
3+
/**
4+
* The ABIs of the devices a build is about to be deployed to - the first (most
5+
* preferred) ABI of every device, deduplicated. `device`/`emulator` narrow the
6+
* set down the same way they narrow the run itself.
7+
*/
8+
export function getDevicesAbis(
9+
$devicesService: Mobile.IDevicesService,
10+
platform: string,
11+
filter: { device?: string; emulator?: boolean } = {}
12+
): string[] {
13+
let devices = $devicesService.getDevicesForPlatform(platform);
14+
if (filter.device) {
15+
devices = devices.filter((d) => d.deviceInfo.identifier === filter.device);
16+
} else if (filter.emulator) {
17+
devices = devices.filter((d) => d.isEmulator);
18+
}
19+
20+
return _.uniq(
21+
devices.map((d) => (d.deviceInfo.abis || [])[0]).filter((abi) => !!abi)
22+
);
23+
}

lib/services/android/gradle-build-service.ts

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import { IAndroidBuildData } from "../../definitions/build";
1010
import { IChildProcess } from "../../common/declarations";
1111
import { injector } from "../../common/yok";
12+
import { getDevicesAbis } from "./devices-abis";
1213
import * as _ from "lodash";
1314

1415
export class GradleBuildService
@@ -75,22 +76,10 @@ export class GradleBuildService
7576
return;
7677
}
7778

78-
let devices = this.$devicesService.getDevicesForPlatform(
79-
buildData.platform
80-
);
81-
if (buildData.device) {
82-
devices = devices.filter(
83-
(d) => d.deviceInfo.identifier === buildData.device
84-
);
85-
} else if (buildData.emulator) {
86-
devices = devices.filter((d) => d.isEmulator);
87-
}
88-
89-
const abis = _.uniq(
90-
devices
91-
.map((d) => (d.deviceInfo.abis || [])[0])
92-
.filter((abi) => !!abi)
93-
);
79+
const abis = getDevicesAbis(this.$devicesService, buildData.platform, {
80+
device: buildData.device,
81+
emulator: buildData.emulator,
82+
});
9483

9584
if (abis.length) {
9685
buildTaskArgs.push(`-PabiFilters=${abis.join(",")}`);

test/services/android-project-service.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
IFileSystem,
2020
IProjectDir,
2121
} from "../../lib/common/declarations";
22+
import { IPluginBuildOptions } from "../../lib/definitions/android-plugin-migrator";
2223

2324
const createTestInjector = (): IInjector => {
2425
const testInjector = new Yok();
@@ -410,3 +411,91 @@ describe("androidProjectService", () => {
410411
});
411412
});
412413
});
414+
415+
describe("androidProjectService plugins abi filtering", () => {
416+
const createDevice = (
417+
identifier: string,
418+
abis: string[],
419+
isEmulator = false
420+
): any => ({
421+
deviceInfo: { identifier, abis, platform: "android" },
422+
isEmulator,
423+
});
424+
425+
const preparePluginNativeCode = async (
426+
options: any,
427+
devices: any[]
428+
): Promise<IPluginBuildOptions> => {
429+
const testInjector = createTestInjector();
430+
let pluginBuildOptions: IPluginBuildOptions = null;
431+
testInjector.register("androidPluginBuildService", {
432+
buildAar: async (opts: IPluginBuildOptions): Promise<boolean> => {
433+
pluginBuildOptions = opts;
434+
return false;
435+
},
436+
migrateIncludeGradle: (): boolean => false,
437+
});
438+
testInjector.register("options", {
439+
hostProjectModuleName: "app",
440+
...options,
441+
});
442+
testInjector.register("devicesService", {
443+
getDevicesForPlatform: (): any[] => devices,
444+
});
445+
testInjector.register("devicePlatformsConstants", { Android: "Android" });
446+
447+
const androidProjectService: IPlatformProjectService = testInjector.resolve(
448+
"androidProjectService"
449+
);
450+
await androidProjectService.preparePluginNativeCode(
451+
<any>{
452+
name: "my-plugin",
453+
pluginPlatformsFolderPath: (): string => "pluginPlatformsDir",
454+
},
455+
<any>{ projectDir: "projectDir", platformsDir: "platformsDir" }
456+
);
457+
458+
return pluginBuildOptions;
459+
};
460+
461+
it("passes the abis of the connected devices when the option is set", async () => {
462+
const options = await preparePluginNativeCode(
463+
{ filterPluginsDevicesArch: true },
464+
[
465+
createDevice("device1", ["arm64-v8a", "armeabi-v7a"]),
466+
createDevice("emulator1", ["x86_64", "x86"], true),
467+
]
468+
);
469+
470+
assert.deepStrictEqual(options.abiFilters, ["arm64-v8a", "x86_64"]);
471+
});
472+
473+
it("passes the abi of the selected device only", async () => {
474+
const options = await preparePluginNativeCode(
475+
{ filterPluginsDevicesArch: true, device: "device1" },
476+
[
477+
createDevice("device1", ["arm64-v8a"]),
478+
createDevice("emulator1", ["x86_64"], true),
479+
]
480+
);
481+
482+
assert.deepStrictEqual(options.abiFilters, ["arm64-v8a"]);
483+
});
484+
485+
it("passes no abis when the option is not set", async () => {
486+
const options = await preparePluginNativeCode({}, [
487+
createDevice("device1", ["arm64-v8a"]),
488+
]);
489+
490+
assert.isNull(options.abiFilters);
491+
});
492+
493+
it("passes no abis when no device reports its abis", async () => {
494+
const options = await preparePluginNativeCode(
495+
{ filterPluginsDevicesArch: true },
496+
[createDevice("device1", [])]
497+
);
498+
499+
assert.isNull(options.abiFilters);
500+
});
501+
});

0 commit comments

Comments
 (0)