From 7dedb573846cd645221d7bcb40fded067572861c Mon Sep 17 00:00:00 2001 From: Martin Guillon Date: Tue, 18 Aug 2026 21:54:29 +0200 Subject: [PATCH 1/3] feat(android): ship the app and plugin gradle files with the CLI The gradle build scripts used to live only in the android runtime, so any fix to them had to wait for a runtime release. This bundles the app-level gradle files in `vendor/gradle-app` and copies them over the ones the runtime lays down when the platform is added, the same way the plugin build already uses `vendor/gradle-plugin`. - `vendor/gradle-app` holds `build.gradle`, `settings.gradle`, `app/build.gradle`, `app/gradle.properties` and the `app/gradle-helpers`. They are copied on top of the runtime files in `createProject`, so the runtime keeps providing everything that is not part of the overlay. - `--no-override-runtime-gradle-files` opts out and keeps the runtime files. - The directory the files come from is resolved through `getGradleFilesPath`, which already understands an `android.gradleFilesPackageName` config key so the files can later be provided by an npm package instead of the bundled copy. - The CLI now interpolates `__PACKAGE__` (android namespace) and `USER_PROJECT_ROOT` in the copied files, and honours `android.gradleVersion` by rewriting the gradle wrapper. - `--gradleArgs` becomes an array option, so it can be passed several times, and a single value may hold several space separated arguments. Arguments listed in `android.gradleArgs` are passed too, before the command line ones. Both app and plugin builds go through the same merge. - Both app and plugin gradle invocations now get `-PcompileSdk`, `-PtargetSdk`, `-PbuildToolsVersion`, `-PgenerateTypings`, `-PprojectRoot` and `-PappBuildPath` (the last two also as `-D` so `settings.gradle` can read them before project properties exist). - A debug build is signed when the `--key-store-*` options are passed, which is needed for system app builds. Co-Authored-By: Claude Opus 5 --- .../project/testing/build-android.md | 2 + .../project/testing/debug-android.md | 2 + docs/man_pages/project/testing/run-android.md | 2 + lib/data/build-data.ts | 2 +- lib/declarations.d.ts | 8 +- lib/definitions/android-plugin-migrator.d.ts | 4 +- lib/definitions/build.d.ts | 2 +- lib/definitions/gradle.d.ts | 1 + lib/definitions/project.d.ts | 19 + lib/options.ts | 7 +- lib/services/android-plugin-build-service.ts | 43 +- lib/services/android-project-service.ts | 111 ++ .../android/gradle-build-args-service.ts | 58 +- test/services/android-plugin-build-service.ts | 3 + test/services/android-project-service.ts | 40 + .../android/gradle-build-args-service.ts | 46 + vendor/gradle-app/app/build.gradle | 1459 +++++++++++++++++ .../gradle-helpers/AnalyticsCollector.gradle | 48 + .../app/gradle-helpers/BuildToolTask.gradle | 50 + .../CustomExecutionLogger.gradle | 52 + vendor/gradle-app/app/gradle.properties | 45 + vendor/gradle-app/build.gradle | 170 ++ vendor/gradle-app/settings.gradle | 78 + vendor/gradle-plugin/build.gradle | 306 +++- vendor/gradle-plugin/gradle.properties | 20 +- vendor/gradle-plugin/settings.gradle | 34 +- 26 files changed, 2469 insertions(+), 143 deletions(-) create mode 100644 vendor/gradle-app/app/build.gradle create mode 100644 vendor/gradle-app/app/gradle-helpers/AnalyticsCollector.gradle create mode 100644 vendor/gradle-app/app/gradle-helpers/BuildToolTask.gradle create mode 100644 vendor/gradle-app/app/gradle-helpers/CustomExecutionLogger.gradle create mode 100644 vendor/gradle-app/app/gradle.properties create mode 100644 vendor/gradle-app/build.gradle create mode 100644 vendor/gradle-app/settings.gradle diff --git a/docs/man_pages/project/testing/build-android.md b/docs/man_pages/project/testing/build-android.md index d537fa081c..ef886ef482 100644 --- a/docs/man_pages/project/testing/build-android.md +++ b/docs/man_pages/project/testing/build-android.md @@ -35,6 +35,8 @@ General | `$ ns build android [--compileSdk ] [--key-store-path ` - Specifies the directory that contains the project. If not set, the project is searched for in the current directory and all directories above it. diff --git a/docs/man_pages/project/testing/debug-android.md b/docs/man_pages/project/testing/debug-android.md index 44116cc3be..c5f11292c4 100644 --- a/docs/man_pages/project/testing/debug-android.md +++ b/docs/man_pages/project/testing/debug-android.md @@ -39,6 +39,8 @@ Attach the debug tools to a running app in the native emulator | `$ ns debug and * `--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. * `--gradleFlavor` - Builds the given product flavor, when the app declares any. `--gradleFlavor foo` runs the `assembleFooDebug`/`assembleFooRelease` gradle task instead of `assembleDebug`/`assembleRelease`. +* `--gradleArgs` - Passes additional arguments to gradle. Can be passed multiple times, and a single value may hold several space separated arguments. Use the `=` form so the value is not mistaken for another flag, for example `--gradleArgs="-PsomeProperty=value"`. Arguments listed under `android.gradleArgs` in `nativescript.config` are passed too, before these ones. +* `--no-override-runtime-gradle-files` - If set, keeps the gradle files coming from the android runtime instead of the ones shipped with the CLI. * `--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 ab651eff42..9ea557e199 100644 --- a/docs/man_pages/project/testing/run-android.md +++ b/docs/man_pages/project/testing/run-android.md @@ -44,6 +44,8 @@ Start a default emulator if none are running, or run application on all connecte * `--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. * `--gradleFlavor` - Builds the given product flavor, when the app declares any. `--gradleFlavor foo` runs the `assembleFooDebug`/`assembleFooRelease` gradle task instead of `assembleDebug`/`assembleRelease`. +* `--gradleArgs` - Passes additional arguments to gradle. Can be passed multiple times, and a single value may hold several space separated arguments. Use the `=` form so the value is not mistaken for another flag, for example `--gradleArgs="-PsomeProperty=value"`. Arguments listed under `android.gradleArgs` in `nativescript.config` are passed too, before these ones. +* `--no-override-runtime-gradle-files` - If set, keeps the gradle files coming from the android runtime instead of the ones shipped with the CLI. * `--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/data/build-data.ts b/lib/data/build-data.ts index 2dc9ba1546..c8747f5e4a 100644 --- a/lib/data/build-data.ts +++ b/lib/data/build-data.ts @@ -53,7 +53,7 @@ export class AndroidBuildData extends BuildData { public androidBundle: boolean; public gradleFlavor: string; public gradlePath: string; - public gradleArgs: string; + public gradleArgs: string[]; public hostProjectPath: string; constructor(projectDir: string, platform: string, data: any) { diff --git a/lib/declarations.d.ts b/lib/declarations.d.ts index 79f4559d76..8b52720845 100644 --- a/lib/declarations.d.ts +++ b/lib/declarations.d.ts @@ -578,7 +578,13 @@ interface IAndroidOptions extends IEmbedOptions { */ gradleFlavor: string; gradlePath: string; - gradleArgs: string; + gradleArgs: string[]; + /** + * When true (the default) the gradle files bundled with the CLI are copied + * over the ones shipped by the android runtime. Pass `--no-override-runtime-gradle-files` + * to keep the runtime files untouched. + */ + overrideRuntimeGradleFiles: boolean; } interface IIOSOptions extends IEmbedOptions {} diff --git a/lib/definitions/android-plugin-migrator.d.ts b/lib/definitions/android-plugin-migrator.d.ts index f5ad873307..2f756dbb50 100644 --- a/lib/definitions/android-plugin-migrator.d.ts +++ b/lib/definitions/android-plugin-migrator.d.ts @@ -11,7 +11,7 @@ interface IAndroidBuildOptions { aarOutputDir: string; tempPluginDirPath: string; gradlePath?: string; - gradleArgs?: string; + gradleArgs?: string[]; } interface IAndroidPluginBuildService { @@ -48,5 +48,5 @@ interface IBuildAndroidPluginData extends Partial { /** * Optional custom Gradle arguments. */ - gradleArgs?: string; + gradleArgs?: string[]; } diff --git a/lib/definitions/build.d.ts b/lib/definitions/build.d.ts index 391cb672c1..1076a79899 100644 --- a/lib/definitions/build.d.ts +++ b/lib/definitions/build.d.ts @@ -33,7 +33,7 @@ interface IAndroidBuildData IHasAndroidBundle { gradleFlavor?: string; gradlePath?: string; - gradleArgs?: string; + gradleArgs?: string[]; } interface IAndroidSigningData { diff --git a/lib/definitions/gradle.d.ts b/lib/definitions/gradle.d.ts index 7b9e8652c5..33bae4b35d 100644 --- a/lib/definitions/gradle.d.ts +++ b/lib/definitions/gradle.d.ts @@ -30,4 +30,5 @@ interface IGradleBuildService { interface IGradleBuildArgsService { getBuildTaskArgs(buildData: IAndroidBuildData): Promise; getCleanTaskArgs(buildData: IAndroidBuildData): string[]; + getBuildLoggingArgs(): string[]; } diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index cd2a931617..f94d8c10bf 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -179,6 +179,25 @@ interface INsConfigAndroid extends INsConfigPlaform { * Custom runtime package name */ runtimePackageName?: string; + + /** + * Pin the gradle wrapper to a specific gradle version, overriding the one + * shipped by the android runtime. + */ + gradleVersion?: string; + + /** + * Additional arguments passed to every gradle invocation (app and plugin + * builds). Merged with the ones passed on the command line through + * `--gradleArgs`. + */ + gradleArgs?: string[]; + + /** + * Package providing the gradle files copied over the ones shipped by the + * android runtime. Defaults to the files bundled with the CLI. + */ + gradleFilesPackageName?: string; } interface INsConfigHooks { diff --git a/lib/options.ts b/lib/options.ts index df6ce62f4d..700d3c9709 100644 --- a/lib/options.ts +++ b/lib/options.ts @@ -221,7 +221,12 @@ export class Options { }, gradleFlavor: { type: OptionType.String, hasSensitiveValue: false }, gradlePath: { type: OptionType.String, hasSensitiveValue: false }, - gradleArgs: { type: OptionType.String, hasSensitiveValue: false }, + gradleArgs: { type: OptionType.Array, hasSensitiveValue: false }, + overrideRuntimeGradleFiles: { + type: OptionType.Boolean, + default: true, + hasSensitiveValue: false, + }, hostProjectPath: { type: OptionType.String, hasSensitiveValue: false }, hostProjectModuleName: { type: OptionType.String, diff --git a/lib/services/android-plugin-build-service.ts b/lib/services/android-plugin-build-service.ts index 88098d55b0..76e0b41167 100644 --- a/lib/services/android-plugin-build-service.ts +++ b/lib/services/android-plugin-build-service.ts @@ -38,6 +38,8 @@ import { injector } from "../common/yok"; import * as _ from "lodash"; import { resolvePackageJSONPath } from "@rigor789/resolve-package-path"; import { cwd } from "process"; +import { IAndroidToolsInfo } from "../declarations"; +import { IGradleBuildArgsService } from "../definitions/gradle"; export class AndroidPluginBuildService implements IAndroidPluginBuildService { private get $platformsDataService(): IPlatformsDataService { @@ -46,7 +48,9 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { constructor( private $fs: IFileSystem, + private $androidToolsInfo: IAndroidToolsInfo, private $childProcess: IChildProcess, + private $gradleBuildArgsService: IGradleBuildArgsService, private $hostInfo: IHostInfo, private $options: IOptions, private $logger: ILogger, @@ -263,7 +267,9 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { ); await this.buildPlugin({ gradlePath: options.gradlePath, - gradleArgs: options.gradleArgs, + gradleArgs: ( + this.$projectData.nsConfig?.android?.gradleArgs ?? [] + ).concat(options.gradleArgs ?? []), pluginDir: pluginTempDir, pluginName: options.pluginName, projectDir: options.projectDir, @@ -413,10 +419,11 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { this.addCompileDependencies(platformsAndroidDirPath, buildGradlePath); const runtimeGradleVersions = await this.getRuntimeGradleVersions(projectDir); - this.replaceGradleVersion( - pluginTempDir, - runtimeGradleVersions.gradleVersion, - ); + // a gradle version pinned in the project config wins over the runtime one + const gradleVersion = + this.$projectData.nsConfig?.android?.gradleVersion ?? + runtimeGradleVersions.gradleVersion; + this.replaceGradleVersion(pluginTempDir, gradleVersion); this.replaceGradleAndroidPluginVersion( buildGradlePath, runtimeGradleVersions.gradleAndroidPluginVersion, @@ -808,22 +815,38 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { pluginBuildSettings.gradlePath ?? (this.$hostInfo.isWindows ? "gradlew.bat" : "./gradlew"); + const toolsInfo = this.$androidToolsInfo.getToolsInfo({ + projectDir: this.$projectData.projectDir, + }); + const localArgs = [ "-p", pluginBuildSettings.pluginDir, "assembleRelease", `-PtempBuild=true`, + `-PcompileSdk=${toolsInfo.compileSdkVersion}`, + `-PtargetSdk=${toolsInfo.targetSdkVersion}`, + `-PbuildToolsVersion=${toolsInfo.buildToolsVersion}`, + `-PprojectRoot=${this.$projectData.projectDir}`, + // settings.gradle runs before the project properties are available, + // so the same values have to be passed as system properties too + `-DprojectRoot=${this.$projectData.projectDir}`, + `-PappBuildPath=${this.$projectData.getBuildRelativeDirectoryPath()}`, + `-DappBuildPath=${this.$projectData.getBuildRelativeDirectoryPath()}`, `-PappPath=${this.$projectData.getAppDirectoryPath()}`, `-PappResourcesPath=${this.$projectData.getAppResourcesDirectoryPath()}`, ]; - if (pluginBuildSettings.gradleArgs) { - localArgs.push(pluginBuildSettings.gradleArgs); + for (const gradleArg of pluginBuildSettings.gradleArgs ?? []) { + localArgs.push( + ...gradleArg + .split(" ") + .map((arg) => arg.trim()) + .filter((arg) => !!arg), + ); } - if (this.$logger.getLevel() === "INFO") { - localArgs.push("--quiet"); - } + localArgs.push(...this.$gradleBuildArgsService.getBuildLoggingArgs()); const opts: any = { cwd: pluginBuildSettings.pluginDir, diff --git a/lib/services/android-project-service.ts b/lib/services/android-project-service.ts index d4ba573a08..501d11db3e 100644 --- a/lib/services/android-project-service.ts +++ b/lib/services/android-project-service.ts @@ -47,6 +47,7 @@ import { import { IInjector } from "../common/definitions/yok"; import { injector } from "../common/yok"; import { INotConfiguredEnvOptions } from "../common/definitions/commands"; +import { resolvePackageJSONPath } from "@rigor789/resolve-package-path"; interface NativeDependency { name: string; @@ -363,10 +364,57 @@ export class AndroidProjectService "-R", ); + this.overrideRuntimeGradleFiles(projectData); + // TODO: Check if we actually need this and if it should be targetSdk or compileSdk this.cleanResValues(targetSdkVersion, projectData); } + /** + * Copies the gradle files shipped with the CLI over the ones the android + * runtime just laid down, so the build scripts can be fixed without waiting + * for a runtime release. Opt out with `--no-override-runtime-gradle-files`. + */ + private overrideRuntimeGradleFiles(projectData: IProjectData): void { + if (!this.$options.overrideRuntimeGradleFiles) { + this.$logger.trace( + "Skipping the gradle files bundled with the CLI - the ones from the android runtime are kept." + ); + return; + } + + const gradleFilesPath = this.getGradleFilesPath(projectData); + this.$logger.trace(`Applying gradle files from '${gradleFilesPath}'.`); + this.$fs.copyFile( + path.join(gradleFilesPath, "*"), + this.getPlatformData(projectData).projectRoot + ); + } + + /** + * Resolves the directory holding the gradle files copied over the runtime + * ones. Defaults to the copy bundled with the CLI, but a project may point + * `android.gradleFilesPackageName` at an npm package shipping its own. + */ + private getGradleFilesPath(projectData: IProjectData): string { + const packageName = projectData.nsConfig?.android?.gradleFilesPackageName; + if (!packageName) { + return path.resolve(path.join(__dirname, "../../vendor/gradle-app")); + } + + const packageJsonPath = resolvePackageJSONPath(packageName, { + paths: [projectData.projectDir], + }); + + if (!packageJsonPath) { + this.$errors.fail( + `Unable to resolve '${packageName}', configured as 'android.gradleFilesPackageName'. Make sure it is installed in the project.` + ); + } + + return path.dirname(packageJsonPath); + } + private getResDestinationDir(projectData: IProjectData): string { const appResourcesDirStructureHasMigrated = this.$androidResourcesMigrationService.hasMigrated( @@ -468,6 +516,28 @@ export class AndroidProjectService this.getProjectNameFromId(projectData), gradleSettingsFilePath, ); + this.sedFile( + gradleSettingsFilePath, + /def USER_PROJECT_ROOT = "\$rootDir\/\.\.\/\.\.\/"/, + `def USER_PROJECT_ROOT = "$rootDir/${this.getProjectRootRelativePath( + projectData + )}"` + ); + + const gradleVersion = projectData.nsConfig?.android?.gradleVersion; + if (gradleVersion) { + // the project pinned a gradle version - point the wrapper at it + this.sedFile( + path.join( + this.getPlatformData(projectData).projectRoot, + "gradle", + "wrapper", + "gradle-wrapper.properties" + ), + /gradle-([0-9.]+)-bin.zip/, + `gradle-${gradleVersion}-bin.zip` + ); + } try { // will replace applicationId in app/App_Resources/Android/app.gradle if it has not been edited by the user @@ -497,6 +567,47 @@ export class AndroidProjectService projectData.projectIdentifiers.android, manifestPath, ); + + const projectRoot = this.getPlatformData(projectData).projectRoot; + // the gradle files bundled with the CLI declare the android namespace and + // resolve the project root themselves - both are placeholders until now + this.sedFile( + path.join(projectRoot, "app", "build.gradle"), + /__PACKAGE__/, + projectData.projectIdentifiers.android + ); + this.sedFile( + path.join(projectRoot, "build.gradle"), + /project\.ext\.USER_PROJECT_ROOT = "\$rootDir\/\.\.\/\.\."/, + `project.ext.USER_PROJECT_ROOT = "$rootDir/${this.getProjectRootRelativePath( + projectData + )}"` + ); + } + + private sedFile( + filePath: string, + pattern: RegExp, + replacement: string + ): void { + if (!this.$fs.exists(filePath)) { + return; + } + + shell.sed("-i", pattern, replacement, filePath); + } + + /** + * Path from the generated android project back to the project root. Not + * always `../..` - the platforms directory can live outside the project. + */ + private getProjectRootRelativePath(projectData: IProjectData): string { + return path + .relative( + this.getPlatformData(projectData).projectRoot, + projectData.projectDir + ) + .replace(/\\/g, "/"); } private getProjectNameFromId(projectData: IProjectData): string { diff --git a/lib/services/android/gradle-build-args-service.ts b/lib/services/android/gradle-build-args-service.ts index afe2449c62..d8c6568e93 100644 --- a/lib/services/android/gradle-build-args-service.ts +++ b/lib/services/android/gradle-build-args-service.ts @@ -1,13 +1,16 @@ import * as path from "path"; import { Configurations } from "../../common/constants"; import { IGradleBuildArgsService } from "../../definitions/gradle"; +import { IAndroidToolsInfo } from "../../declarations"; import { IAndroidBuildData } from "../../definitions/build"; import { IHooksService, IAnalyticsService } from "../../common/declarations"; import { injector } from "../../common/yok"; import { IProjectData } from "../../definitions/project"; +import { LoggerLevel } from "../../constants"; export class GradleBuildArgsService implements IGradleBuildArgsService { constructor( + private $androidToolsInfo: IAndroidToolsInfo, private $hooksService: IHooksService, private $analyticsService: IAnalyticsService, private $staticConfig: Config.IStaticConfig, @@ -50,17 +53,34 @@ export class GradleBuildArgsService implements IGradleBuildArgsService { // ensure we initialize project data this.$projectData.initializeProjectData(buildData.projectDir); + const toolsInfo = this.$androidToolsInfo.getToolsInfo({ + projectDir: buildData.projectDir, + }); + args.push( + `-PcompileSdk=${toolsInfo.compileSdkVersion}`, + `-PtargetSdk=${toolsInfo.targetSdkVersion}`, + `-PbuildToolsVersion=${toolsInfo.buildToolsVersion}`, + `-PgenerateTypings=${toolsInfo.generateTypings}`, + `-PprojectRoot=${this.$projectData.projectDir}`, + // settings.gradle runs before the project properties are available, + // so the same values have to be passed as system properties too + `-DprojectRoot=${this.$projectData.projectDir}`, + `-PappBuildPath=${this.$projectData.getBuildRelativeDirectoryPath()}`, + `-DappBuildPath=${this.$projectData.getBuildRelativeDirectoryPath()}`, `-PappPath=${this.$projectData.getAppDirectoryPath()}`, `-PappResourcesPath=${this.$projectData.getAppResourcesDirectoryPath()}` ); - if (buildData.gradleArgs) { - args.push(buildData.gradleArgs); - } + + args.push(...this.getUserDefinedGradleArgs(buildData.gradleArgs)); if (buildData.release) { + args.push("-Prelease"); + } + + // a debug build can be signed too - for example when building a system app + if (buildData.keyStorePath) { args.push( - "-Prelease", `-PksPath=${path.resolve(buildData.keyStorePath)}`, `-Palias=${buildData.keyStoreAlias}`, `-Ppassword=${buildData.keyStoreAliasPassword}`, @@ -71,13 +91,37 @@ export class GradleBuildArgsService implements IGradleBuildArgsService { return args; } - private getBuildLoggingArgs(): string[] { + /** + * Gradle args coming from `android.gradleArgs` in the project config, followed + * by the ones passed on the command line through `--gradleArgs`. A single + * value may hold several space separated args. + */ + private getUserDefinedGradleArgs(commandLineArgs: string[]): string[] { + const gradleArgs = ( + this.$projectData.nsConfig?.android?.gradleArgs ?? [] + ).concat(commandLineArgs ?? []); + + return gradleArgs.reduce( + (args, arg) => + args.concat( + arg + .split(" ") + .map((a) => a.trim()) + .filter((a) => !!a) + ), + [], + ); + } + + public getBuildLoggingArgs(): string[] { const args = []; const logLevel = this.$logger.getLevel(); - if (logLevel === "TRACE") { + if (logLevel === LoggerLevel.TRACE) { args.push("--stacktrace", "--debug"); - } else if (logLevel === "INFO") { + } else if (logLevel === LoggerLevel.DEBUG) { + args.push("--stacktrace", "--info"); + } else if (logLevel === LoggerLevel.INFO) { args.push("--quiet"); } diff --git a/test/services/android-plugin-build-service.ts b/test/services/android-plugin-build-service.ts index c2107cf4c1..0935235006 100644 --- a/test/services/android-plugin-build-service.ts +++ b/test/services/android-plugin-build-service.ts @@ -109,6 +109,9 @@ describe("androidPluginBuildService", () => { testInjector.register("watchIgnoreListService", { addFileToIgnoreList: () => ({}), }); + testInjector.register("gradleBuildArgsService", { + getBuildLoggingArgs: (): string[] => [], + }); fs = testInjector.resolve("fs"); androidBuildPluginService = testInjector.resolve( diff --git a/test/services/android-project-service.ts b/test/services/android-project-service.ts index 1961548012..ddfad54754 100644 --- a/test/services/android-project-service.ts +++ b/test/services/android-project-service.ts @@ -4,6 +4,7 @@ import * as stubs from "../stubs"; import { assert } from "chai"; import * as sinon from "sinon"; import * as path from "path"; +import { existsSync, readFileSync } from "fs"; import { GradleCommandService } from "../../lib/services/android/gradle-command-service"; import { GradleBuildService } from "../../lib/services/android/gradle-build-service"; import { GradleBuildArgsService } from "../../lib/services/android/gradle-build-args-service"; @@ -78,6 +79,45 @@ const getDefautlBuildConfig = (): IBuildConfig => { }; }; +describe("bundled gradle files", () => { + // the gradle files shipped with the CLI are copied over the ones coming from + // the android runtime - they have to be part of the published package + const gradleAppDir = path.join( + __dirname, + "..", + "..", + "vendor", + "gradle-app", + ); + + const expectedFiles = [ + "build.gradle", + "settings.gradle", + path.join("app", "build.gradle"), + path.join("app", "gradle.properties"), + ]; + + for (const expectedFile of expectedFiles) { + it(`ships vendor/gradle-app/${expectedFile}`, () => { + assert.isTrue( + existsSync(path.join(gradleAppDir, expectedFile)), + `${expectedFile} is missing from vendor/gradle-app`, + ); + }); + } + + it("keeps the placeholders the CLI interpolates", () => { + assert.include( + readFileSync(path.join(gradleAppDir, "settings.gradle"), "utf8"), + "__PROJECT_NAME__", + ); + assert.include( + readFileSync(path.join(gradleAppDir, "app", "build.gradle"), "utf8"), + "__PACKAGE__", + ); + }); +}); + describe("androidProjectService", () => { let injector: IInjector; let androidProjectService: IPlatformProjectService; diff --git a/test/services/android/gradle-build-args-service.ts b/test/services/android/gradle-build-args-service.ts index 8851f19e67..baadc68684 100644 --- a/test/services/android/gradle-build-args-service.ts +++ b/test/services/android/gradle-build-args-service.ts @@ -62,7 +62,16 @@ const ksDir = mkdtempSync(path.join(tmpdir(), "ksPath-")); const ksPath = path.join(ksDir, "keystore.jks"); const expectedInfoLoggingArgs = ["--quiet"]; const expectedTraceLoggingArgs = ["--stacktrace", "--debug"]; +const projectDir = "/path/to/projectDir".replace(/\//g, path.sep); const expectedDebugBuildArgs = [ + "-PcompileSdk=28", + "-PtargetSdk=26", + "-PbuildToolsVersion=my-build-tools-version", + "-PgenerateTypings=true", + `-PprojectRoot=${projectDir}`, + `-DprojectRoot=${projectDir}`, + "-PappBuildPath=platforms", + "-DappBuildPath=platforms", "-PappPath=/path/to/projectDir/app".replace(/\//g, path.sep), "-PappResourcesPath=/path/to/projectDir/app/App_Resources".replace( /\//g, @@ -213,6 +222,43 @@ describe("GradleBuildArgsService", () => { } }); + describe("gradle args", () => { + it("appends the ones passed through --gradleArgs", async () => { + const injector = createTestInjector(); + injector.resolve("logger").getLevel = () => "WARN"; + const gradleBuildArgsService = injector.resolve("gradleBuildArgsService"); + + const args = await gradleBuildArgsService.getBuildTaskArgs({ + release: false, + gradleArgs: ["-PsomeArg=1", "-PotherArg=2 -PthirdArg=3"], + }); + + assert.deepStrictEqual(args.slice(-3), [ + "-PsomeArg=1", + "-PotherArg=2", + "-PthirdArg=3", + ]); + }); + + it("appends the ones from the project config before the command line ones", async () => { + const injector = createTestInjector(); + injector.resolve("logger").getLevel = () => "WARN"; + const projectData = injector.resolve("projectData"); + projectData.nsConfig = { android: { gradleArgs: ["-PfromConfig=1"] } }; + const gradleBuildArgsService = injector.resolve("gradleBuildArgsService"); + + const args = await gradleBuildArgsService.getBuildTaskArgs({ + release: false, + gradleArgs: ["-PfromCommandLine=1"], + }); + + assert.deepStrictEqual(args.slice(-2), [ + "-PfromConfig=1", + "-PfromCommandLine=1", + ]); + }); + }); + describe("getCleanTaskArgs", async () => { const testCases = [ { diff --git a/vendor/gradle-app/app/build.gradle b/vendor/gradle-app/app/build.gradle new file mode 100644 index 0000000000..adac695f5c --- /dev/null +++ b/vendor/gradle-app/app/build.gradle @@ -0,0 +1,1459 @@ +/* +* Script builds apk in release or debug mode +* To run: +* gradle assembleRelease -Prelease (release mode) +* gradle assembleDebug (debug mode -> default) +* Options: +* -Prelease //this flag will run build in release mode +* -PksPath=[path_to_keystore_file] +* -PksPassword=[password_for_keystore_file] +* -Palias=[alias_to_use_from_keystore_file] +* -Ppassword=[password_for_alias] +* +* -PtargetSdk=[target_sdk] +* -PbuildToolsVersion=[build_tools_version] +* -PcompileSdk=[compile_sdk_version] +* -PandroidXLegacy=[androidx_legacy_version] +* -PandroidXAppCompat=[androidx_appcompat_version] +* -PandroidXMaterial=[androidx_material_version] +* -PappPath=[app_path] +* -PappResourcesPath=[app_resources_path] +*/ + +import groovy.io.FileType +import groovy.json.JsonSlurper +import groovy.xml.XmlSlurper +import org.apache.commons.io.FileUtils + +import javax.inject.Inject +import java.nio.file.Files +import java.nio.file.Paths +import java.nio.file.StandardCopyOption +import java.security.MessageDigest +import java.util.jar.JarEntry +import java.util.jar.JarFile + +import static org.gradle.internal.logging.text.StyledTextOutput.Style +import java.util.stream.Collectors; +import java.util.stream.Stream; + + +apply plugin: "com.android.application" +apply from: "gradle-helpers/BuildToolTask.gradle" +apply from: "gradle-helpers/CustomExecutionLogger.gradle" +apply from: "gradle-helpers/AnalyticsCollector.gradle" +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-parcelize' + +def onlyX86 = project.hasProperty("onlyX86") +if (onlyX86) { + outLogger.withStyle(Style.Info).println "OnlyX86 build triggered." +} + +//common +def BUILD_TOOLS_PATH = "$rootDir/build-tools" +def PASSED_TYPINGS_PATH = System.getenv("TNS_TYPESCRIPT_DECLARATIONS_PATH") +def TYPINGS_PATH = "$BUILD_TOOLS_PATH/typings" +if (PASSED_TYPINGS_PATH != null) { + TYPINGS_PATH = PASSED_TYPINGS_PATH +} + +def PACKAGE_JSON = "package.json" + +//static binding generator +def SBG_JAVA_DEPENDENCIES = "sbg-java-dependencies.txt" +def SBG_INPUT_FILE = "sbg-input-file.txt" +def SBG_OUTPUT_FILE = "sbg-output-file.txt" +def SBG_JS_PARSED_FILES = "sbg-js-parsed-files.txt" +def SBG_BINDINGS_NAME = "sbg-bindings.txt" +def SBG_INTERFACE_NAMES = "sbg-interface-names.txt" +def INPUT_JS_DIR = "$projectDir/src/main/assets/app" +def OUTPUT_JAVA_DIR = "$projectDir/src/main/java" + +//metadata generator +def MDG_OUTPUT_DIR = "mdg-output-dir.txt" +def MDG_JAVA_DEPENDENCIES = "mdg-java-dependencies.txt" +def METADATA_OUT_PATH = "$projectDir/src/main/assets/metadata" +def METADATA_JAVA_OUT = "mdg-java-out.txt" + +// paths to jar libraries +def pluginsJarLibraries = new LinkedList() +def allJarLibraries = new LinkedList() + +def computeKotlinVersion = { -> project.hasProperty("kotlinVersion") ? kotlinVersion : "${ns_default_kotlin_version}" } +def computeCompileSdkVersion = { -> project.hasProperty("compileSdk") ? compileSdk as int : NS_DEFAULT_COMPILE_SDK_VERSION as int } +def computeTargetSdkVersion = { -> project.hasProperty("targetSdk") ? targetSdk as int : NS_DEFAULT_COMPILE_SDK_VERSION as int } +def computeMinSdkVersion = { -> project.hasProperty("minSdk") ? minSdk : NS_DEFAULT_MIN_SDK_VERSION as int } +def computeBuildToolsVersion = { -> + project.hasProperty("buildToolsVersion") ? buildToolsVersion : NS_DEFAULT_BUILD_TOOLS_VERSION as String +} + +def enableAnalytics = (project.hasProperty("gatherAnalyticsData") && project.gatherAnalyticsData == "true") +def enableVerboseMDG = project.gradle.startParameter.logLevel.name() == 'DEBUG' +def analyticsFilePath = "$rootDir/analytics/build-statistics.json" +def analyticsCollector = project.ext.AnalyticsCollector.withOutputPath(analyticsFilePath) +if (enableAnalytics) { + analyticsCollector.markUseKotlinPropertyInApp(true) + analyticsCollector.writeAnalyticsFile() +} + +project.ext.selectedBuildType = project.hasProperty("release") ? "release" : "debug" + +buildscript { + def applyBuildScriptConfigurations = { -> + def absolutePathToAppResources = getAppResourcesPath() + def pathToBuildScriptGradle = "$absolutePathToAppResources/Android/buildscript.gradle" + def buildScriptGradle = file(pathToBuildScriptGradle) + if (buildScriptGradle.exists()) { + outLogger.withStyle(Style.SuccessHeader).println "\t + applying user-defined buildscript from ${buildScriptGradle}" + apply from: pathToBuildScriptGradle, to: buildscript + } + + nativescriptDependencies.each { dep -> + def pathToPluginBuildScriptGradle = "$rootDir/${dep.directory}/$PLATFORMS_ANDROID/buildscript.gradle" + def pluginBuildScriptGradle = file(pathToPluginBuildScriptGradle) + if (pluginBuildScriptGradle.exists()) { + outLogger.withStyle(Style.SuccessHeader).println "\t + applying user-defined buildscript from dependency ${pluginBuildScriptGradle}" + apply from: pathToPluginBuildScriptGradle, to: buildscript + } + } + } + applyBuildScriptConfigurations() +} +//////////////////////////////////////////////////////////////////////////////////// +///////////////////////////// CONFIGURATIONS /////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////// + +def applyBeforePluginGradleConfiguration = { -> + def appResourcesPath = getAppResourcesPath() + def pathToBeforePluginGradle = "$appResourcesPath/Android/before-plugins.gradle" + def beforePluginGradle = file(pathToBeforePluginGradle) + if (beforePluginGradle.exists()) { + outLogger.withStyle(Style.SuccessHeader).println "\t + applying user-defined configuration from ${beforePluginGradle}" + apply from: pathToBeforePluginGradle + } +} + +def applyAppGradleConfiguration = { -> + def appResourcesPath = getAppResourcesPath() + def pathToAppGradle = "$appResourcesPath/Android/app.gradle" + def appGradle = file(pathToAppGradle) + if (appGradle.exists()) { + outLogger.withStyle(Style.SuccessHeader).println "\t + applying user-defined configuration from ${appGradle}" + apply from: pathToAppGradle + } else { + outLogger.withStyle(Style.Info).println "\t + couldn't load user-defined configuration from ${appGradle}. File doesn't exist." + } +} + +def applyPluginGradleConfigurations = { -> + nativescriptDependencies.each { dep -> + def includeGradlePath = "$rootDir/${dep.directory}/$PLATFORMS_ANDROID/include.gradle" + if (file(includeGradlePath).exists()) { + apply from: includeGradlePath + } + } +} + +def getAppIdentifier = { packageJsonMap -> + def appIdentifier = "" + if (packageJsonMap && packageJsonMap.nativescript) { + appIdentifier = packageJsonMap.nativescript.id + if (!(appIdentifier instanceof String)) { + appIdentifier = appIdentifier.android + } + } + + return appIdentifier +} + +def setAppIdentifier = { -> + outLogger.withStyle(Style.SuccessHeader).println "\t + setting applicationId" + File packageJsonFile = new File("$USER_PROJECT_ROOT/$PACKAGE_JSON") + + if (packageJsonFile.exists()) { + def content = packageJsonFile.getText("UTF-8") + def jsonSlurper = new JsonSlurper() + def packageJsonMap = jsonSlurper.parseText(content) + def appIdentifier = getAppIdentifier(packageJsonMap) + + if (appIdentifier) { + project.ext.nsApplicationIdentifier = appIdentifier + android.defaultConfig.applicationId = appIdentifier + android.namespace = appIdentifier + } + } +} + +android { + namespace "__PACKAGE__" + + applyBeforePluginGradleConfiguration() + + kotlin { + jvmToolchain(17) + } + + compileSdk computeCompileSdkVersion() + buildToolsVersion = computeBuildToolsVersion() + + defaultConfig { + def manifest = new XmlSlurper().parse(file(android.sourceSets.main.manifest.srcFile)) + def minSdkVer = manifest."uses-sdk"."@android:minSdkVersion".text() ?: computeMinSdkVersion() + minSdkVersion minSdkVer + targetSdkVersion computeTargetSdkVersion() + ndk { + if (onlyX86) { + abiFilters 'x86' + } else { + abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' + } + } + } + + if (project.hasProperty("ndkVersion")) { + ndkVersion project.ndkVersion + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + sourceSets.main { + jniLibs.srcDirs = ["$projectDir/libs/jni", "$projectDir/snapshot-build/build/ndk-build/libs"] + } + + signingConfigs { + release { + if (project.hasProperty("ksPath") && + project.hasProperty("ksPassword") && + project.hasProperty("alias") && + project.hasProperty("password")) { + + storeFile file(ksPath) + storePassword ksPassword + keyAlias alias + keyPassword password + } + } + } + buildTypes { + debug { + if (project.hasProperty("ksPath") && + project.hasProperty("ksPassword") && + project.hasProperty("alias") && + project.hasProperty("password")) { + signingConfig signingConfigs.release + } + } + release { + signingConfig signingConfigs.release + } + } + + setAppIdentifier() + applyPluginGradleConfigurations() + applyAppGradleConfiguration() + + def initializeMergedAssetsOutputPath = { -> + android.applicationVariants.configureEach { variant -> + if (variant.buildType.name == project.selectedBuildType) { + def task + if (variant.metaClass.respondsTo(variant, "getMergeAssetsProvider")) { + def provider = variant.getMergeAssetsProvider() + task = provider.get() + } else { + // fallback for older android gradle plugin versions + task = variant.getMergeAssets() + } + for (File file : task.getOutputs().getFiles()) { + if (!file.getPath().contains("${File.separator}incremental${File.separator}")) { + project.ext.mergedAssetsOutputPath = file.getPath() + break + } + } + } + } + } + + initializeMergedAssetsOutputPath() +} + +def externalRuntimeExists = !findProject(':runtime').is(null) +def pluginDependencies + +repositories { + // used for local *.AAR files + pluginDependencies = nativescriptDependencies.collect { + "$rootDir/${it.directory}/$PLATFORMS_ANDROID" + } + + // some plugins may have their android dependencies in a /libs subdirectory + pluginDependencies.addAll(nativescriptDependencies.collect { + "$rootDir/${it.directory}/$PLATFORMS_ANDROID/libs" + }) + + if (!externalRuntimeExists) { + pluginDependencies.add("libs/runtime-libs") + } + + def appResourcesPath = getAppResourcesPath() + def localAppResourcesLibraries = "$appResourcesPath/Android/libs" + + pluginDependencies.add(localAppResourcesLibraries) + + if (pluginDependencies.size() > 0) { + flatDir { + dirs pluginDependencies + } + } + + mavenCentral() +} + +dependencies { + // println "\t ~ [DEBUG][app] build.gradle - ns_default_androidx_appcompat_version = ${ns_default_androidx_appcompat_version}..." + + def androidXAppCompatVersion = "${ns_default_androidx_appcompat_version}" + if (project.hasProperty("androidXAppCompat")) { + androidXAppCompatVersion = androidXAppCompat + outLogger.withStyle(Style.SuccessHeader).println "\t + using android X library androidx.appcompat:appcompat:$androidXAppCompatVersion" + } + + def androidXMaterialVersion = "${ns_default_androidx_material_version}" + if (project.hasProperty("androidXMaterial")) { + androidXMaterialVersion = androidXMaterial + outLogger.withStyle(Style.SuccessHeader).println "\t + using android X library com.google.android.material:material:$androidXMaterialVersion" + } + + def androidXExifInterfaceVersion = "${ns_default_androidx_exifinterface_version}" + if (project.hasProperty("androidXExifInterface")) { + androidXExifInterfaceVersion = androidXExifInterface + outLogger.withStyle(Style.SuccessHeader).println "\t + using android X library androidx.exifinterface:exifinterface:$androidXExifInterfaceVersion" + } + + def androidXViewPagerVersion = "${ns_default_androidx_viewpager_version}" + if (project.hasProperty("androidXViewPager")) { + androidXViewPagerVersion = androidXViewPager + outLogger.withStyle(Style.SuccessHeader).println "\t + using android X library androidx.viewpager2:viewpager2:$androidXViewPagerVersion" + } + + def androidXFragmentVersion = "${ns_default_androidx_fragment_version}" + if (project.hasProperty("androidXFragment")) { + androidXFragmentVersion = androidXFragment + outLogger.withStyle(Style.SuccessHeader).println "\t + using android X library androidx.fragment:fragment:$androidXFragmentVersion" + } + + def androidXTransitionVersion = "${ns_default_androidx_transition_version}" + if (project.hasProperty("androidXTransition")) { + androidXTransitionVersion = androidXTransition + outLogger.withStyle(Style.SuccessHeader).println "\t + using android X library androidx.transition:transition:$androidXTransitionVersion" + } + + def androidXMultidexVersion = "${ns_default_androidx_multidex_version}" + if (project.hasProperty("androidXMultidex")) { + androidXMultidexVersion = androidXMultidex + outLogger.withStyle(Style.SuccessHeader).println "\t + using android X library androidx.multidex:multidex:$androidXMultidexVersion" + } + + implementation "androidx.multidex:multidex:$androidXMultidexVersion" + implementation "androidx.appcompat:appcompat:$androidXAppCompatVersion" + debugImplementation "com.google.android.material:material:$androidXMaterialVersion" + implementation "androidx.exifinterface:exifinterface:$androidXExifInterfaceVersion" + implementation "androidx.viewpager2:viewpager2:$androidXViewPagerVersion" + //noinspection KtxExtensionAvailable + implementation "androidx.fragment:fragment:$androidXFragmentVersion" + implementation "androidx.transition:transition:$androidXTransitionVersion" + + def useV8Symbols = false + + def appPackageJsonFile = file("${getAppPath()}/$PACKAGE_JSON") + if (appPackageJsonFile.exists()) { + def appPackageJson = new JsonSlurper().parseText(appPackageJsonFile.text) + useV8Symbols = appPackageJson.android && appPackageJson.android.useV8Symbols + } + + if (!useV8Symbols) { + // check whether any of the dependencies require v8 symbols + useV8Symbols = nativescriptDependencies.any { + def packageJsonFile = file("$rootDir/${it.directory}/$PACKAGE_JSON") + def packageJson = new JsonSlurper().parseText(packageJsonFile.text) + return packageJson.nativescript && packageJson.nativescript.useV8Symbols + } + } + + if (!externalRuntimeExists) { + def runtime = "nativescript-optimized-with-inspector" + + if (project.gradle.startParameter.taskNames.any { it.toLowerCase().contains('release') }) { + runtime = "nativescript-optimized" + } + + if (useV8Symbols) { + runtime = "nativescript-regular" + } + + outLogger.withStyle(Style.SuccessHeader).println "\t + adding nativescript runtime package dependency: $runtime" + project.dependencies.add("implementation", [name: runtime, ext: "aar"]) + } else { + implementation project(':runtime') + } + +} + +//////////////////////////////////////////////////////////////////////////////////// +///////////////////////////// CONFIGURATION PHASE ////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////// + +task 'addDependenciesFromNativeScriptPlugins' { + nativescriptDependencies.each { dep -> + def aarFiles = fileTree(dir: file("$rootDir/${dep.directory}/$PLATFORMS_ANDROID")).matching { + include "**/*.aar" + exclude "cpp/**" + exclude project.hasProperty("aarIgnoreFilter") ? project.findProperty('aarIgnoreFilter').split(',').collect{it as String} : [] + } + aarFiles.each { aarFile -> + def length = aarFile.name.length() - 4 + def fileName = aarFile.name[0.. + def jarFileAbsolutePath = jarFile.getAbsolutePath() + outLogger.withStyle(Style.SuccessHeader).println "\t + adding jar plugin dependency: $jarFileAbsolutePath" + pluginsJarLibraries.add(jarFile.getAbsolutePath()) + } + + project.dependencies.add("implementation", jarFiles) + } +} + +task 'addDependenciesFromAppResourcesLibraries' { + def appResourcesPath = getAppResourcesPath() + def appResourcesLibraries = file("$appResourcesPath/Android/libs") + if (appResourcesLibraries.exists()) { + def aarFiles = fileTree(dir: appResourcesLibraries, include: ["**/*.aar"]) + aarFiles.each { aarFile -> + def length = aarFile.name.length() - 4 + def fileName = aarFile.name[0.. + def jarFileAbsolutePath = jarFile.getAbsolutePath() + outLogger.withStyle(Style.SuccessHeader).println "\t + adding jar plugin dependency: $jarFileAbsolutePath" + pluginsJarLibraries.add(jarFile.getAbsolutePath()) + } + + project.dependencies.add("implementation", jarFiles) + } +} + +if (failOnCompilationWarningsEnabled()) { + tasks.withType(JavaCompile).configureEach { + options.compilerArgs << '-Xlint:all' << "-Werror" + options.deprecation = true + } +} + + +//////////////////////////////////////////////////////////////////////////////////// +///////////////////////////// EXECUTION PHASE ///////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////// + +task runSbg(type: BuildToolTask) { + dependsOn "collectAllJars" + def rootPath = "" + if (!findProject(':static-binding-generator').is(null)) { + rootPath = Paths.get(project(':static-binding-generator').projectDir.path, "build/libs").toString() + dependsOn ':static-binding-generator:jar' + } + + outputs.dir("$OUTPUT_JAVA_DIR/com/tns/gen") + inputs.dir(INPUT_JS_DIR) + inputs.dir(extractedDependenciesDir) + + workingDir "$BUILD_TOOLS_PATH" + mainClass = "-jar" + + def paramz = new ArrayList() + paramz.add(Paths.get(rootPath, "static-binding-generator.jar")) + + if (failOnCompilationWarningsEnabled()) { + paramz.add("-show-deprecation-warnings") + } + + setOutputs outLogger + + args paramz + + doFirst { + new File("$OUTPUT_JAVA_DIR/com/tns/gen").deleteDir() + } +} + +def failOnCompilationWarningsEnabled() { + return project.hasProperty("failOnCompilationWarnings") && (failOnCompilationWarnings || failOnCompilationWarnings.toBoolean()) +} + +def explodeAar(File compileDependency, File outputDir) { + logger.info("explodeAar: Extracting ${compileDependency.path} -> ${outputDir.path}") + + if (compileDependency.name.endsWith(".aar")) { + JarFile jar = new JarFile(compileDependency) + Enumeration enumEntries = jar.entries() + while (enumEntries.hasMoreElements()) { + JarEntry file = (JarEntry) enumEntries.nextElement() + if (file.isDirectory()) { + continue + } + if (file.name.endsWith(".jar")) { + def targetFile = new File(outputDir, file.name) + InputStream inputStream = jar.getInputStream(file) + new File(targetFile.parent).mkdirs() + Files.copy(inputStream, targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + } + } + jar.close() + } else if (compileDependency.name.endsWith(".jar")) { + copy { + from compileDependency.absolutePath + into outputDir + } + } +} + +static def md5(String string) { + MessageDigest digest = MessageDigest.getInstance("MD5") + digest.update(string.bytes) + return new BigInteger(1, digest.digest()).toString(16).padLeft(32, '0') +} + +class WorkerTask extends DefaultTask { + @Inject + WorkerExecutor getWorkerExecutor() { + throw new UnsupportedOperationException() + } +} + +class EmptyRunnable implements Runnable { + void run() { + } +} + +def getMergedAssetsOutputPath() { + if (!project.hasProperty("mergedAssetsOutputPath")) { + // mergedAssetsOutputPath not found fallback to the default value for android gradle plugin 3.5.1 + project.ext.mergedAssetsOutputPath = "$projectDir/build/intermediates/merged_assets/" + project.selectedBuildType + "/out" + } + return project.ext.mergedAssetsOutputPath +} + +// Discover all jars and dynamically create tasks for the extraction of each of them +project.ext.allJars = [] +allprojects { + afterEvaluate { project -> + def buildType = project.selectedBuildType + def jars = [] + def artifactType = Attribute.of('artifactType', String) + android.applicationVariants.configureEach { variant -> + if (variant.buildType.name == buildType) { + variant.getCompileClasspath(null).each { fileDependency -> + processJar(fileDependency, jars) + } + } + } + } +} + +def processJar(File jar, jars) { + if (!jars.contains(jar)) { + jars.add(jar) + def destDir = md5(jar.path) + def outputDir = new File(Paths.get(extractedDependenciesDir, destDir).normalize().toString()) + + def taskName = "extract_${jar.name}_to_${destDir}" + logger.debug("Creating dynamic task ${taskName}") + + // Add discovered jars as dependencies of cleanupAllJars. + // This is crucial for cloud builds because they are different + // on each incremental build (as each time the gradle user home + // directory is a randomly generated string) + cleanupAllJars.inputs.files jar + + task "${taskName}"(type: WorkerTask) { + dependsOn cleanupAllJars + extractAllJars.dependsOn it + + // This dependency seems redundant but probably due to some Gradle issue with workers, + // without it `runSbg` sporadically starts before all extraction tasks have finished and + // fails due to missing JARs + runSbg.dependsOn it + + inputs.files jar + outputs.dir outputDir + + doLast { + // Runing in parallel no longer seems to bring any benefit. + // It mattered only when we were extracting JARs from AARs. + // To try it simply remove the following comments. + // workerExecutor.submit(EmptyRunnable.class) { + explodeAar(jar, outputDir) + // } + } + } + project.ext.allJars.add([file: jar, outputDir: outputDir]) + } +} + +task 'cleanupAllJars' { + // We depend on the list of libs directories that might contain aar or jar files + // and on the list of all discovered jars + inputs.files(pluginDependencies) + + outputs.files cleanupAllJarsTimestamp + + doLast { + def allDests = project.ext.allJars*.outputDir*.name + def dir = new File(extractedDependenciesDir) + if (dir.exists()) { + dir.eachDir { + // An old directory which is no longer a dependency (e.g. orphaned by a deleted plugin) + if (!allDests.contains(it.name)) { + logger.info("Task cleanupAllJars: Deleting orphaned ${it.path}") + FileUtils.deleteDirectory(it) + } + } + } + new File(cleanupAllJarsTimestamp).write "" + } +} + + +// Placeholder task which depends on all dynamically generated extraction tasks +task 'extractAllJars' { + dependsOn cleanupAllJars + outputs.files extractAllJarsTimestamp + + doLast { + new File(cleanupAllJarsTimestamp).write "" + } +} + +task 'collectAllJars' { + dependsOn extractAllJars + description "gathers all paths to jar dependencies before building metadata with them" + + def sdkPath = android.sdkDirectory.getAbsolutePath() + def androidJar = sdkPath + "/platforms/" + android.compileSdkVersion + "/android.jar" + + doFirst { + def allJarPaths = new LinkedList() + allJarPaths.add(androidJar) + allJarPaths.addAll(pluginsJarLibraries) + def ft = fileTree(dir: extractedDependenciesDir, include: "**/*.jar") + ft.each { currentJarFile -> + allJarPaths.add(currentJarFile.getAbsolutePath()) + } + + new File("$BUILD_TOOLS_PATH/$SBG_JAVA_DEPENDENCIES").withWriter { out -> + allJarPaths.each { out.println it } + } + new File("$BUILD_TOOLS_PATH/$MDG_JAVA_DEPENDENCIES").withWriter { out -> + allJarPaths.each { + if (it.endsWith(".jar")) { + out.println it + } + } + } + + new File("$BUILD_TOOLS_PATH/$SBG_INPUT_FILE").withWriter { out -> + out.println INPUT_JS_DIR + } + new File("$BUILD_TOOLS_PATH/$SBG_OUTPUT_FILE").withWriter { out -> + out.println OUTPUT_JAVA_DIR + } + + allJarLibraries.addAll(allJarPaths) + } +} + +task copyMetadataFilters { + outputs.files("$BUILD_TOOLS_PATH/whitelist.mdg", "$BUILD_TOOLS_PATH/blacklist.mdg") + // use an explicit copy task here because the copy task itselfs marks the whole built-tools as an output! + copy { + from file("$rootDir/whitelist.mdg"), file("$rootDir/blacklist.mdg") + into "$BUILD_TOOLS_PATH" + } +} + +task 'copyMetadata' { + doLast { + copy { + from "$projectDir/src/main/assets/metadata" + into getMergedAssetsOutputPath() + "/metadata" + } + } +} + +def listf(String directoryName, ArrayList store) { + def directory = new File(directoryName) + + def resultList = new ArrayList() + + def fList = directory.listFiles() + resultList.addAll(Arrays.asList(fList)) + for (File file : fList) { + if (file.isFile()) { + store.add(file) + } else if (file.isDirectory()) { + resultList.addAll(listf(file.getAbsolutePath(), store)) + } + } + return resultList +} + +task buildMetadata(type: BuildToolTask) { + def rootPath = "" + if (!findProject(':android-metadata-generator').is(null)) { + rootPath = Paths.get(project(':android-metadata-generator').projectDir.path, "build/libs").toString() + dependsOn ':android-metadata-generator:jar' + } + + + + android.applicationVariants.all { variant -> + def buildTypeName = variant.buildType.name.capitalize() + def mergeShadersTaskName = "merge${buildTypeName}Shaders" + def mergeShadersTask = tasks.findByName(mergeShadersTaskName) + + if (mergeShadersTask) { + dependsOn mergeShadersTask + } + + def compileJavaWithJavacTaskName = "compile${buildTypeName}JavaWithJavac" + def compileJavaWithJavacTask = tasks.findByName(compileJavaWithJavacTaskName) + + + if (compileJavaWithJavacTask) { + dependsOn compileJavaWithJavacTask + } + + def compileKotlinTaskName = "compile${buildTypeName}Kotlin" + def compileKotlinTask = tasks.findByName(compileKotlinTaskName) + + + if (compileKotlinTask) { + dependsOn compileKotlinTask + } + + + def mergeDexTaskName = "mergeDex${buildTypeName}" + def mergeDexTask = tasks.findByName(mergeDexTaskName) + + if (mergeDexTask) { + dependsOn mergeDexTask + } + + def checkDuplicateClassesTaskName = "check${buildTypeName}DuplicateClasses" + def checkDuplicateClassesTask = tasks.findByName(checkDuplicateClassesTaskName) + + if (checkDuplicateClassesTask) { + dependsOn checkDuplicateClassesTask + } + + def generateBuildConfigTaskName = "generate${buildTypeName}BuildConfig" + def generateBuildConfigTask = tasks.findByName(generateBuildConfigTaskName) + + if (generateBuildConfigTask) { + dependsOn generateBuildConfigTask + } + + def dexBuilderTaskName = "dexBuilder${buildTypeName}" + def dexBuilderTask = tasks.findByName(dexBuilderTaskName) + + if (dexBuilderTask) { + dependsOn dexBuilderTask + } + + + def mergeExtDexTaskName = "mergeExtDex${buildTypeName}" + def mergeExtDexTask = tasks.findByName(mergeExtDexTaskName) + + if (mergeExtDexTask) { + dependsOn mergeExtDexTask + } + + def mergeLibDexTaskName = "mergeLibDex${buildTypeName}" + def mergeLibDexTask = tasks.findByName(mergeLibDexTaskName) + + if (mergeLibDexTask) { + dependsOn mergeLibDexTask + } + + def mergeProjectDexTaskName = "mergeProjectDex${buildTypeName}" + def mergeProjectDexTask = tasks.findByName(mergeProjectDexTaskName) + + if (mergeProjectDexTask) { + dependsOn mergeProjectDexTask + } + + def syncLibJarsTaskName = "sync${buildTypeName}LibJars" + def syncLibJarsTask = tasks.findByName(syncLibJarsTaskName) + + if (syncLibJarsTask) { + dependsOn syncLibJarsTask + } + + def mergeJavaResourceTaskName = "merge${buildTypeName}JavaResource" + def mergeJavaResourceTask = tasks.findByName(mergeJavaResourceTaskName) + + if (mergeJavaResourceTask) { + dependsOn mergeJavaResourceTask + } + + def mergeJniLibFoldersTaskName = "merge${buildTypeName}JniLibFolders" + def mergeJniLibFoldersTask = tasks.findByName(mergeJniLibFoldersTaskName) + + if (mergeJniLibFoldersTask) { + dependsOn mergeJniLibFoldersTask + } + + def mergeNativeLibsTaskName = "merge${buildTypeName}NativeLibs" + def mergeNativeLibsTask = tasks.findByName(mergeNativeLibsTaskName) + + if (mergeNativeLibsTask) { + dependsOn mergeNativeLibsTask + } + + def stripDebugSymbolsTaskName = "strip${buildTypeName}DebugSymbols" + def stripDebugSymbolsTask = tasks.findByName(stripDebugSymbolsTaskName) + + if (stripDebugSymbolsTask) { + dependsOn stripDebugSymbolsTask + } + + def validateSigningTaskName = "validateSigning${buildTypeName}" + def validateSigningTask = tasks.findByName(validateSigningTaskName) + + if (validateSigningTask) { + dependsOn validateSigningTask + } + + + def extractProguardFilesTaskName = "extractProguardFiles" + def extractProguardFilesTask = tasks.findByName(extractProguardFilesTaskName) + + if (extractProguardFilesTask) { + dependsOn extractProguardFilesTask + } + + + // def compileArtProfileTaskName = "compile${buildTypeName}ArtProfile" + // def compileArtProfileTask = tasks.findByName(compileArtProfileTaskName) + + // if (compileArtProfileTask) { + // dependsOn compileArtProfileTask + // } + + + def extractNativeSymbolTablesTaskName = "extract${buildTypeName}NativeSymbolTables" + def extractNativeSymbolTablesTask = tasks.findByName(extractNativeSymbolTablesTaskName) + + if (extractNativeSymbolTablesTask) { + dependsOn extractNativeSymbolTablesTask + } + + + // def optimizeResourcesTaskName = "optimize${buildTypeName}Resources" + // def optimizeResourcesTask = tasks.findByName(optimizeResourcesTaskName) + + // if (optimizeResourcesTask) { + // dependsOn optimizeResourcesTask + // } + + def bundleResourcesTaskName = "bundle${buildTypeName}Resources" + def bundleResourcesTask = tasks.findByName(bundleResourcesTaskName) + + if (bundleResourcesTask) { + dependsOn bundleResourcesTask + } + + } + + dependsOn copyMetadataFilters + + // As some external gradle plugins can reorder the execution order of the tasks it may happen that buildMetadata is executed after merge{Debug/Release}Assets + // in that case the metadata won't be included in the result apk and it will crash, so to avoid this we are adding the copyMetadata task which will manually copy + // the metadata files in the merge assets folder and they will be added to the result apk + + // The next line is added to avoid adding another copyData implementation from the firebase plugin - https://github.com/EddyVerbruggen/nativescript-plugin-firebase/blob/3943bb9147f43c41599e801d026378eba93d3f3a/publish/scripts/installer.js#L1105 + //buildMetadata.finalizedBy(copyMetadata) + finalizedBy copyMetadata + + description "builds metadata with provided jar dependencies" + + inputs.files("$MDG_JAVA_DEPENDENCIES") + + // make MDG aware of whitelist.mdg and blacklist.mdg files + // inputs.files(project.fileTree(dir: "$rootDir", include: "**/*.mdg")) + // use explicit inputs as the above makes the whole build-tools directory an input! + inputs.files("$BUILD_TOOLS_PATH/whitelist.mdg", "$BUILD_TOOLS_PATH/blacklist.mdg") + + def classesDir = layout.buildDirectory.dir("intermediates/javac").get().asFile + if (classesDir.exists()) { + inputs.dir(classesDir) + } + + def kotlinClassesDir = layout.buildDirectory.dir("tmp/kotlin-classes").get().asFile + if (kotlinClassesDir.exists()) { + inputs.dir(kotlinClassesDir) + } + + outputs.files("$METADATA_OUT_PATH/treeNodeStream.dat", "$METADATA_OUT_PATH/treeStringsStream.dat", "$METADATA_OUT_PATH/treeValueStream.dat") + + workingDir "$BUILD_TOOLS_PATH" + mainClass = "-jar" + + doFirst { + // get compiled classes to pass to metadata generator + // these need to be called after the classes have compiled + new File(getMergedAssetsOutputPath() + "/metadata").deleteDir() + + def classesSubDirs = [] + def kotlinClassesSubDirs = [] + def selectedBuildType = project.ext.selectedBuildType + + rootProject.subprojects { + + def projectClassesDir = it.layout.buildDirectory.dir("intermediates/javac").get().asFile + def projectKotlinClassesDir = it.layout.buildDirectory.dir("tmp/kotlin-classes").get().asFile + + if (projectClassesDir.exists()) { + def projectClassesSubDirs = projectClassesDir.listFiles() + for (File subDir : projectClassesSubDirs) { + if (!classesSubDirs.contains(subDir)) { + classesSubDirs.add(subDir) + } + } + } + + if (projectKotlinClassesDir.exists()) { + def projectKotlinClassesSubDirs = projectKotlinClassesDir.listFiles() + for (File subDir : projectKotlinClassesSubDirs) { + if (!kotlinClassesSubDirs.contains(subDir)) { + kotlinClassesSubDirs.add(subDir) + } + } + } + } + + def generatedClasses = new LinkedList() + for (File subDir : classesSubDirs) { + if (subDir.getName() == selectedBuildType) { + generatedClasses.add(subDir.getAbsolutePath()) + } + } + + for (File subDir : kotlinClassesSubDirs) { + if (subDir.getName() == selectedBuildType) { + generatedClasses.add(subDir.getAbsolutePath()) + } + } + + def store = new ArrayList() + for (String dir : generatedClasses) { + listf(dir, store) + } + + + new File("$BUILD_TOOLS_PATH/$METADATA_JAVA_OUT").withWriter { out -> + store.each { + out.println it.absolutePath + } + } + + + new File("$BUILD_TOOLS_PATH/$MDG_OUTPUT_DIR").withWriter { out -> + out.println "$METADATA_OUT_PATH" + } + + new File("$BUILD_TOOLS_PATH/$MDG_JAVA_DEPENDENCIES").withWriterAppend { out -> + generatedClasses.each { out.println it } + } + + setOutputs outLogger + + def paramz = new ArrayList() + paramz.add(Paths.get(rootPath, "android-metadata-generator.jar")) + + if (enableAnalytics) { + paramz.add("analyticsFilePath=$analyticsFilePath") + } + + if (enableVerboseMDG) { + paramz.add("verbose") + } + + args paramz.toArray() + } +} + +task generateTypescriptDefinitions(type: BuildToolTask) { + if (!findProject(':dts-generator').is(null)) { + dependsOn ':dts-generator:jar' + } + + def paramz = new ArrayList() + def includeDirs = ["com.android.support", "/platforms/" + android.compileSdkVersion] + + workingDir "$BUILD_TOOLS_PATH" + mainClass = "-jar" + + doFirst { + delete "$TYPINGS_PATH" + + paramz.add("dts-generator.jar") + paramz.add("-input") + + for (String jarPath : allJarLibraries) { + // don't generate typings for runtime jars and classes + if (shouldIncludeDirForTypings(jarPath, includeDirs)) { + paramz.add(jarPath) + } + } + + paramz.add("-output") + paramz.add("$TYPINGS_PATH") + + new File("$TYPINGS_PATH").mkdirs() + + logger.info("Task generateTypescriptDefinitions: Call dts-generator.jar with arguments: " + paramz.toString().replaceAll(',', '')) + outLogger.withStyle(Style.SuccessHeader).println "Task generateTypescriptDefinitions: Call dts-generator.jar with arguments: " + paramz.toString().replaceAll(',', '') + + setOutputs outLogger + + args paramz.toArray() + } +} + +generateTypescriptDefinitions.onlyIf { + (project.hasProperty("generateTypings") && Boolean.parseBoolean(project.generateTypings)) || PASSED_TYPINGS_PATH != null +} + +collectAllJars.finalizedBy(generateTypescriptDefinitions) + +static def shouldIncludeDirForTypings(path, includeDirs) { + for (String p : includeDirs) { + if (path.indexOf(p) > -1) { + return true + } + } + + return false +} + +task 'copyTypings' { + doLast { + outLogger.withStyle(Style.Info).println "Copied generated typings to application root level. Make sure to import android.d.ts in reference.d.ts" + + copy { + from "$TYPINGS_PATH" + into "$USER_PROJECT_ROOT" + } + } +} + +copyTypings.onlyIf { generateTypescriptDefinitions.didWork } +generateTypescriptDefinitions.finalizedBy(copyTypings) + +task 'validateAppIdMatch' { + doLast { + def lineSeparator = System.getProperty("line.separator") + + if (project.hasProperty("nsApplicationIdentifier") && !project.hasProperty("release")) { + if (project.nsApplicationIdentifier != android.defaultConfig.applicationId && android.namespace != appIdentifier) { + def errorMessage = "${lineSeparator}WARNING: The Application identifier is different from the one inside \"package.json\" file.$lineSeparator" + + "NativeScript CLI might not work properly.$lineSeparator" + + "Remove applicationId from app.gradle and update the \"nativescript.id\" in package.json.$lineSeparator" + + "Actual: ${android.defaultConfig.applicationId}$lineSeparator" + + "Expected(from \"package.json\"): ${project.nsApplicationIdentifier}$lineSeparator" + + logger.error(errorMessage) + } + } + } +} + + +//////////////////////////////////////////////////////////////////////////////////// +////////////////////////// COMPILE-TIME BYTECODE /////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////// +// +// For RELEASE builds, replace the plain-JS app bundle in the merged assets with +// pre-compiled bytecode for the active JS engine (Hermes today; QuickJS/QuickJS-NG/ +// PrimJS are wired but gated off until their compilers + runtime support land). +// The files keep their .js names, so module resolution is unaffected; the runtime +// detects the engine's bytecode magic at load time (see js_run_bytecode_file / +// ModuleInternal) and runs it directly instead of parsing + compiling source, +// which greatly improves TTI. +// +// The engine-generic driver decides whether the selected engine has a usable +// compiler for the build host, so this task simply runs it for every release +// build and lets it no-op when there's nothing to do. It only touches the +// merged-assets build intermediate, never the app source. +// +// Requires the runtime's gradle.properties to declare `ns_engine`; without it +// the task is a no-op (older runtimes ship no bytecode compiler). +// +// Disable for a release build with: -PnsBytecodeDisabled +// Force-enable for a debug build with: -PnsBytecodeForce +// Enable source maps with: -PnsBytecodeSourceMaps +// +// Overridable paths: +// -PbytecodeToolsDir= — the bytecode-compiler tools folder +// -PbytecodeCompilerBinary= — force a specific compiler binary +// -PnodePath= — the node executable + +def bytecodeDisabled = project.hasProperty("nsBytecodeDisabled") || project.hasProperty("bytecodeDisabled") +// Force bytecode compilation even for debug builds. Bytecode is a release-only +// feature by default; this opt-in flag makes debug builds compile to bytecode +// too, which is handy for testing/debugging the bytecode path itself. +def bytecodeForced = project.hasProperty("nsBytecodeForce") || project.hasProperty("bytecodeForce") +// A release build is not reliably signalled by -Prelease (the CLI runs +// assembleRelease/bundleRelease without it), so detect it from the requested +// task names — the same heuristic the rest of this file uses. +def isReleaseBuild = project.hasProperty("release") || + project.gradle.startParameter.taskNames.any { it.toLowerCase().contains("release") } +// The engine name comes from the runtime's gradle.properties (ns_engine). Older +// runtimes don't declare it and have no bytecode compiler at all, so the whole +// feature stays off unless the property is present. +def bytecodeEngine = project.hasProperty("ns_engine") ? ns_engine as String : null +project.ext.bytecodeEnabled = bytecodeEngine != null && (isReleaseBuild || bytecodeForced) && !bytecodeDisabled + +// Announce the bytecode status once at configuration time so it's obvious in the +// build log whether ahead-of-time bytecode will be produced. +if (project.ext.bytecodeEnabled) { + def buildKind = isReleaseBuild ? "release build" : "debug build (forced via -PnsBytecodeForce)" + outLogger.withStyle(Style.SuccessHeader).println "\t + [bytecode] ENABLED for ${buildKind} (engine: ${bytecodeEngine})" + outLogger.withStyle(Style.Info).println "\t ~ app JS will be compiled to bytecode after asset merge (if ${bytecodeEngine} has a compiler for this host)." + outLogger.withStyle(Style.Info).println "\t ~ disable with -PnsBytecodeDisabled" +} else if (bytecodeEngine != null) { + def bytecodeReason = bytecodeDisabled ? "disabled via -PnsBytecodeDisabled" + : "debug build (release-only feature; force with -PnsBytecodeForce)" + outLogger.withStyle(Style.Info).println "\t ~ [bytecode] DISABLED — ${bytecodeReason}; shipping plain JS. isReleaseBuid: ${isReleaseBuild};" +} + +// The compiler tooling ships in the framework template under build-tools; fall +// back to the in-repo location when building the runtime itself (test-app). +def resolveBytecodeToolsDir = { -> + if (project.hasProperty("bytecodeToolsDir")) return bytecodeToolsDir as String + def candidates = [ + "$rootDir/build-tools/bytecode-compiler" as String, + "$rootDir/../tools/bytecode-compiler" as String, + ] + return candidates.find { new File(it).exists() } ?: candidates[0] +} +def resolveNodePath = { -> + project.hasProperty("nodePath") ? nodePath as String : "node" +} + +task compileBytecode { + // No inputs/outputs declared on purpose: the task is cheap and idempotent + // (it skips files that are already bytecode) and must re-run whenever the + // merged assets are refreshed. + onlyIf { project.ext.bytecodeEnabled } + doLast { + def appDir = getMergedAssetsOutputPath() + "/app" + if (!new File(appDir).exists()) { + outLogger.withStyle(Style.Info).println "\t ~ [bytecode] no merged app assets at ${appDir}, skipping" + return + } + def toolsDir = resolveBytecodeToolsDir() + def script = "$toolsDir/compile-bytecode.js" + def node = resolveNodePath() + def sourceMaps = project.hasProperty("nsBytecodeSourceMaps") + // Resilient by default: a file that fails to compile is left as plain JS + // (the runtime loads source directly) and the build continues. Opt into + // fail-the-build behaviour with -PnsBytecodeStrict. + def strict = project.hasProperty("nsBytecodeStrict") + + outLogger.withStyle(Style.SuccessHeader).println "\t + [bytecode] compiling app JS → ${bytecodeEngine} bytecode" + outLogger.withStyle(Style.Info).println "\t ~ engine: ${bytecodeEngine}" + outLogger.withStyle(Style.Info).println "\t ~ app assets: ${appDir}" + outLogger.withStyle(Style.Info).println "\t ~ driver: ${script}" + if (project.hasProperty("bytecodeCompilerBinary")) { + outLogger.withStyle(Style.Info).println "\t ~ compiler: ${bytecodeCompilerBinary}" + } + outLogger.withStyle(Style.Info).println "\t ~ source maps: ${sourceMaps ? "on" : "off"}" + outLogger.withStyle(Style.Info).println "\t ~ on error: ${strict ? "fail the build (strict)" : "skip file, keep source"}" + + def cmd = [node, script, "--app", appDir, "--engine", bytecodeEngine] + if (project.hasProperty("bytecodeCompilerBinary")) { + cmd += ["--compiler", bytecodeCompilerBinary as String] + } + if (sourceMaps) { + cmd += ["--source-maps"] + } + if (!strict) { + cmd += ["--keep-going"] + } + // The driver prints its own "[bytecode] ... compiled N file(s)" summary to + // stdout, which surfaces in the build log below. + exec { + commandLine cmd + } + outLogger.withStyle(Style.SuccessHeader).println "\t + [bytecode] done." + } +} + + +//////////////////////////////////////////////////////////////////////////////////// +////////////////////////////// OPTIONAL TASKS ////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////// + +//////// custom clean /////////// +task cleanSbg(type: Delete) { + delete "$BUILD_TOOLS_PATH/$SBG_JS_PARSED_FILES", + "$BUILD_TOOLS_PATH/$SBG_JAVA_DEPENDENCIES", + "$BUILD_TOOLS_PATH/$SBG_INTERFACE_NAMES", + "$BUILD_TOOLS_PATH/$SBG_BINDINGS_NAME", + "$BUILD_TOOLS_PATH/$SBG_INPUT_FILE", + "$BUILD_TOOLS_PATH/$SBG_OUTPUT_FILE", + "$BUILD_TOOLS_PATH/$METADATA_JAVA_OUT", + "$OUTPUT_JAVA_DIR/com/tns/gen" +} + +task cleanMdg(type: Delete) { + delete "$BUILD_TOOLS_PATH/$MDG_OUTPUT_DIR", + "$BUILD_TOOLS_PATH/whitelist.mdg", + "$BUILD_TOOLS_PATH/blacklist.mdg", + "$BUILD_TOOLS_PATH/$MDG_JAVA_DEPENDENCIES", + "$METADATA_OUT_PATH" +} + +cleanSbg.dependsOn(cleanMdg) +clean.dependsOn(cleanSbg) + + +//dependsOn { +// pattern { +// include "merge*.Shaders" // Matches tasks starting with "merge" and ending with "Shaders" +// } +//} + +tasks.configureEach({ DefaultTask currentTask -> + // println "\t ~ [DEBUG][app] build.gradle - currentTask = ${currentTask.name} ..." + + if (currentTask =~ /compile.+JavaWithJavac/) { + currentTask.dependsOn(runSbg) + } + + if (currentTask =~ /mergeDex.+/) { + currentTask.dependsOn(runSbg) + } + + if (currentTask =~ /compile.+Kotlin.+/) { + currentTask.dependsOn(runSbg) + } + + if (currentTask =~ /merge.*Assets/) { + currentTask.dependsOn(buildMetadata) + // Compile the freshly-merged app JS to engine bytecode right after the + // assets are merged (the onlyIf guard makes this a no-op unless it's a + // release build with bytecode enabled). Packaging is made to wait for it below. + currentTask.finalizedBy(compileBytecode) + compileBytecode.mustRunAfter(currentTask) + + } + + // The asset pipeline is: merge*Assets -> compress*Assets -> package*. The + // compress task reads the merged assets and zips each file into a `.js.jar` + // that is what actually lands in the APK/App Bundle. It only depends on the + // merge task, so without this it can run BEFORE compileBytecode and end up + // compressing plain source — the compiled bytecode in the merged assets then + // never reaches the package. Force compression to wait for compileBytecode so + // the order is merge -> compileBytecode -> compress -> package. (No cycle: + // compileBytecode only mustRunAfter the merge task, never the compress task.) + // Debug's compress*Assets task is included so a force-enabled (-PnsBytecodeForce) + // debug build compresses the compiled bytecode rather than plain source. The + // task is a no-op via compileBytecode's onlyIf guard when bytecode is off. + if (currentTask.name == "compressReleaseAssets" || currentTask.name == "compressDebugAssets") { + currentTask.dependsOn(compileBytecode) + } + + // Ensure the final APK/App Bundle packaging picks up the compiled bytecode. + // Match the packaging tasks by exact name — a broad /package.*Release/ also + // matches packageReleaseResources, which is upstream of buildMetadata -> + // mergeReleaseAssets -> compileBytecode and creates a circular dependency. + // Debug packaging is wired too so a force-enabled debug build ships bytecode; + // compileBytecode's onlyIf keeps it a no-op for ordinary debug builds. + if (currentTask.name in ["packageRelease", "packageReleaseBundle", "packageDebug", "packageDebugBundle"]) { + currentTask.dependsOn(compileBytecode) + } + + // ensure buildMetadata is done before R8 to allow custom proguard from metadata + if (currentTask =~ /minify(Debug|Release)WithR8/) { + buildMetadata.finalizedBy(currentTask) + } + + if (currentTask =~ /assemble.*Debug/ || currentTask =~ /assemble.*Release/) { + currentTask.finalizedBy("validateAppIdMatch") + } + + if (currentTask =~ /process.+Resources/) { + cleanupAllJars.dependsOn(currentTask) + } + +// if (currentTask.name == "extractProguardFiles") { +// currentTask.finalizedBy(buildMetadata) +// } +// + if (currentTask =~ /generate.+LintVitalReportModel/) { + currentTask.dependsOn(buildMetadata) + } + + if (currentTask =~ /lintVitalAnalyze.+/) { + currentTask.dependsOn(buildMetadata) + } +// +// if (currentTask =~ /merge.+GlobalSynthetics/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /optimize.+Resources/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /buildCMake.*/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /configureCMake.*/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /validateSigning.*/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /generate.*LintReportModel/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /generate.*AndroidTestResValues/) { +// // buildMetadata.dependsOn(currentTask) +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /generate.*AndroidTestLintModel/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /generate.*UnitTestLintModel/) { +// buildMetadata.mustRunAfter(currentTask) +// } +// +// if (currentTask =~ /generate.*UnitTestLintModel/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// +// if (currentTask =~ /lintAnalyze.*UnitTest/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /process.*JavaRes/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /strip.*DebugSymbols/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /merge.*JavaResource/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /lintAnalyze.*/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /lintAnalyze.*AndroidTest/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /bundle.*Resources/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /compile.*ArtProfile/) { +// currentTask.mustRunAfter(buildMetadata) +// } +// +// if (currentTask =~ /check.*DuplicateClasses/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /check.*AarMetadata/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /create.*CompatibleScreenManifests/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /process.*Manifest/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /generate.*ResValues/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /merge.*Resources/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /package.*Resources/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /process.*Resources/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /desugar.*Dependencies/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /merge.*JniLibFolders/) { +// currentTask.finalizedBy(buildMetadata) +// } + +}) + +rootProject.subprojects.forEach { + it.tasks.configureEach({ DefaultTask currentTask -> + if (currentTask =~ /.+bundleLibCompileToJar.*/) { + currentTask.finalizedBy(cleanupAllJars) + } + + if (currentTask =~ /bundleLibRuntimeToDir.*/) { + currentTask.finalizedBy(buildMetadata) + } + + if (currentTask =~ /compile.*LibraryResources/) { + currentTask.finalizedBy(buildMetadata) + } + }) +} diff --git a/vendor/gradle-app/app/gradle-helpers/AnalyticsCollector.gradle b/vendor/gradle-app/app/gradle-helpers/AnalyticsCollector.gradle new file mode 100644 index 0000000000..72fa363a73 --- /dev/null +++ b/vendor/gradle-app/app/gradle-helpers/AnalyticsCollector.gradle @@ -0,0 +1,48 @@ +import groovy.json.JsonBuilder + +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Paths +import java.nio.file.Path + +class AnalyticsCollector{ + + private final String analyticsFilePath + private boolean hasUseKotlinPropertyInApp = false + private boolean hasKotlinRuntimeClasses = false + + private AnalyticsCollector(String analyticsFilePath){ + this.analyticsFilePath = analyticsFilePath + } + + static AnalyticsCollector withOutputPath(String analyticsFilePath){ + return new AnalyticsCollector(analyticsFilePath) + } + + void markUseKotlinPropertyInApp(boolean useKotlin) { + hasUseKotlinPropertyInApp = useKotlin + } + + void writeAnalyticsFile() { + def jsonBuilder = new JsonBuilder() + def kotlinUsageData = new Object() + kotlinUsageData.metaClass.hasUseKotlinPropertyInApp = hasUseKotlinPropertyInApp + kotlinUsageData.metaClass.hasKotlinRuntimeClasses = hasKotlinRuntimeClasses + jsonBuilder(kotlinUsage: kotlinUsageData) + def prettyJson = jsonBuilder.toPrettyString() + + + + Path statisticsFilePath = Paths.get(analyticsFilePath) + + if (Files.notExists(statisticsFilePath)) { + Files.createDirectories(statisticsFilePath.getParent()) + Files.createFile(statisticsFilePath) + } + + Files.write(statisticsFilePath, prettyJson.getBytes(StandardCharsets.UTF_8)) + + } +} + +ext.AnalyticsCollector = AnalyticsCollector diff --git a/vendor/gradle-app/app/gradle-helpers/BuildToolTask.gradle b/vendor/gradle-app/app/gradle-helpers/BuildToolTask.gradle new file mode 100644 index 0000000000..7b6052f4c2 --- /dev/null +++ b/vendor/gradle-app/app/gradle-helpers/BuildToolTask.gradle @@ -0,0 +1,50 @@ +import static org.gradle.internal.logging.text.StyledTextOutput.Style + +class BuildToolTask extends JavaExec { + void setOutputs(def logger) { + def logFile = new File("$workingDir/${name}.log") + if(logFile.exists()) { + logFile.delete() + } + standardOutput new FileOutputStream(logFile) + errorOutput new FailureOutputStream(logger, logFile) + } +} + +class FailureOutputStream extends OutputStream { + private logger + private File logFile + private currentLine = "" + private firstWrite = true + FailureOutputStream(inLogger, inLogFile) { + logger = inLogger + logFile = inLogFile + } + + @Override + void write(int i) throws IOException { + if(firstWrite) { + println "" + firstWrite = false + } + currentLine += String.valueOf((char) i) + } + + @Override + void flush() { + if(currentLine?.trim()) { + logger.withStyle(Style.Failure).println currentLine.trim() + currentLine = "" + } + } + + @Override + void close() { + if(!firstWrite && logFile.exists()) { + logger.withStyle(Style.Info).println "Detailed log here: ${logFile.getAbsolutePath()}\n" + } + super.close() + } +} + +ext.BuildToolTask = BuildToolTask \ No newline at end of file diff --git a/vendor/gradle-app/app/gradle-helpers/CustomExecutionLogger.gradle b/vendor/gradle-app/app/gradle-helpers/CustomExecutionLogger.gradle new file mode 100644 index 0000000000..ec8ea6c427 --- /dev/null +++ b/vendor/gradle-app/app/gradle-helpers/CustomExecutionLogger.gradle @@ -0,0 +1,52 @@ +import org.gradle.internal.logging.text.StyledTextOutputFactory + +import static org.gradle.internal.logging.text.StyledTextOutput.Style +def outLogger = services.get(StyledTextOutputFactory).create("colouredOutputLogger") + +class CustomExecutionLogger extends BuildAdapter implements TaskExecutionListener { + private logger + private failedTask + + CustomExecutionLogger(passedLogger) { + logger = passedLogger + } + + void buildStarted(Gradle gradle) { + failedTask = null + } + + void beforeExecute(Task task) { + } + + void afterExecute(Task task, TaskState state) { + def failure = state.getFailure() + if(failure) { + failedTask = task + } + } + + void buildFinished(BuildResult result) { + def failure = result.getFailure() + if(failure) { + if(failedTask && (failedTask.getClass().getName().contains("BuildToolTask"))) { + // the error from this task is already logged + return + } + + println "" + logger.withStyle(Style.FailureHeader).println failure.getMessage() + + def causeException = failure.getCause() + while (causeException != null) { + failure = causeException + causeException = failure.getCause() + } + if(failure != causeException) { + logger.withStyle(Style.Failure).println failure.getMessage() + } + println "" + } + } +} + +gradle.useLogger(new CustomExecutionLogger(outLogger)) \ No newline at end of file diff --git a/vendor/gradle-app/app/gradle.properties b/vendor/gradle-app/app/gradle.properties new file mode 100644 index 0000000000..9a6a4000a3 --- /dev/null +++ b/vendor/gradle-app/app/gradle.properties @@ -0,0 +1,45 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +#org.gradle.parallel=true + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx16384M +android.enableJetifier=true +android.useAndroidX=true + +# Default versions used throughout the gradle configurations +NS_DEFAULT_BUILD_TOOLS_VERSION=35.0.0 +NS_DEFAULT_COMPILE_SDK_VERSION=35 +NS_DEFAULT_MIN_SDK_VERSION=21 +NS_DEFAULT_ANDROID_BUILD_TOOLS_VERSION=8.12.1 + +ns_default_androidx_appcompat_version = 1.7.0 +ns_default_androidx_exifinterface_version = 1.3.7 +ns_default_androidx_fragment_version = 1.8.5 +ns_default_androidx_material_version = 1.8.0 +ns_default_androidx_multidex_version = 2.0.1 +ns_default_androidx_transition_version = 1.5.1 +ns_default_androidx_viewpager_version = 1.0.0 +ns_default_asm_util_version = 9.7 +ns_default_asm_version = 9.7 +ns_default_bcel_version = 6.8.2 +ns_default_commons_io_version = 2.6 +ns_default_google_java_format_version = 1.6 +ns_default_gson_version = 2.10.1 +ns_default_json_version = 20180813 +ns_default_junit_version = 4.13.2 +ns_default_kotlin_version = 2.2.20 +ns_default_kotlinx_metadata_jvm_version = 2.2.20 +ns_default_mockito_core_version = 3.0.0 +ns_default_spotbugs_version = 3.1.12 \ No newline at end of file diff --git a/vendor/gradle-app/build.gradle b/vendor/gradle-app/build.gradle new file mode 100644 index 0000000000..99a1643b20 --- /dev/null +++ b/vendor/gradle-app/build.gradle @@ -0,0 +1,170 @@ +import org.gradle.internal.logging.text.StyledTextOutputFactory + +import java.nio.file.Paths + +import org.gradle.internal.logging.text.StyledTextOutputFactory +import groovy.json.JsonSlurper +import static org.gradle.internal.logging.text.StyledTextOutput.Style + +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + + def initialize = { -> + // set up our logger + project.ext.outLogger = services.get(StyledTextOutputFactory).create("colouredOutputLogger") + def userDir = "${rootProject.projectDir}/../.." + apply from: "$rootDir/gradle-helpers/user_properties_reader.gradle" + apply from: "$rootDir/gradle-helpers/paths.gradle" + rootProject.ext.userDefinedGradleProperties = getUserProperties("${getAppResourcesPath(userDir)}/Android") + + loadPropertyFile("$rootDir/additional_gradle.properties") + + if (rootProject.hasProperty("userDefinedGradleProperties")) { + rootProject.ext.userDefinedGradleProperties.each { entry -> + def propertyName = entry.getKey() + def propertyValue = entry.getValue() + project.ext.set(propertyName, propertyValue) + } + } + + // the build script will not work with previous versions of the CLI (3.1 or earlier) + def dependenciesJson = file("$rootDir/dependencies.json") + if (!dependenciesJson.exists()) { + throw new BuildCancelledException(""" +'dependencies.json' file not found. Check whether the NativeScript CLI has prepared the project beforehand, +and that your NativeScript version is 3.3, or a more recent one. To build an android project with the current +version of the {N} CLI install a previous version of the runtime package - 'tns platform add android@3.2'. +""") + } + + project.ext.extractedDependenciesDir = "${project.layout.buildDirectory.dir("exploded-dependencies").get().asFile}" + project.ext.cleanupAllJarsTimestamp = "${project.layout.buildDirectory.file("cleanupAllJars.timestamp").get().asFile}" + project.ext.extractAllJarsTimestamp = "${project.layout.buildDirectory.file("extractAllJars.timestamp").get().asFile}" + + + project.ext.nativescriptDependencies = new JsonSlurper().parseText(dependenciesJson.text) + project.ext.PLATFORMS_ANDROID = "platforms/android" + project.ext.USER_PROJECT_ROOT = "$rootDir/../.." + + project.ext.getAppPath = { -> + def relativePathToApp = "app" + def nsConfigFile = file("$USER_PROJECT_ROOT/nsconfig.json") + def nsConfig + + if (nsConfigFile.exists()) { + nsConfig = new JsonSlurper().parseText(nsConfigFile.getText("UTF-8")) + } + + if (project.hasProperty("appPath")) { + // when appPath is passed through -PappPath=/path/to/app + // the path could be relative or absolute - either case will work + relativePathToApp = appPath + } else if (nsConfig != null && nsConfig.appPath != null) { + relativePathToApp = nsConfig.appPath + } + + project.ext.appPath = Paths.get(USER_PROJECT_ROOT).resolve(relativePathToApp).toAbsolutePath() + + return project.ext.appPath + } + + project.ext.getAppResourcesPath = { -> + def relativePathToAppResources + def absolutePathToAppResources + def nsConfigFile = file("$USER_PROJECT_ROOT/nsconfig.json") + def nsConfig + + if (nsConfigFile.exists()) { + nsConfig = new JsonSlurper().parseText(nsConfigFile.getText("UTF-8")) + } + + if (project.hasProperty("appResourcesPath")) { + // when appResourcesPath is passed through -PappResourcesPath=/path/to/App_Resources + // the path could be relative or absolute - either case will work + relativePathToAppResources = appResourcesPath + absolutePathToAppResources = Paths.get(USER_PROJECT_ROOT).resolve(relativePathToAppResources).toAbsolutePath() + } else if (nsConfig != null && nsConfig.appResourcesPath != null) { + relativePathToAppResources = nsConfig.appResourcesPath + absolutePathToAppResources = Paths.get(USER_PROJECT_ROOT).resolve(relativePathToAppResources).toAbsolutePath() + } else { + absolutePathToAppResources = "${getAppPath()}/App_Resources" + } + + project.ext.appResourcesPath = absolutePathToAppResources + + return absolutePathToAppResources + } + + + } + + def applyBeforePluginGradleConfiguration = { -> + def appResourcesPath = getAppResourcesPath() + def pathToBeforePluginGradle = "$appResourcesPath/Android/before-plugins.gradle" + def beforePluginGradle = file(pathToBeforePluginGradle) + if (beforePluginGradle.exists()) { + outLogger.withStyle(Style.SuccessHeader).println "\t + applying user-defined configuration from ${beforePluginGradle}" + apply from: pathToBeforePluginGradle + } + } + + def applyBuildScriptConfigurations = { -> + def absolutePathToAppResources = getAppResourcesPath() + def pathToBuildScriptGradle = "$absolutePathToAppResources/Android/rootbuildscript.gradle" + def buildScriptGradle = file(pathToBuildScriptGradle) + if (buildScriptGradle.exists()) { + outLogger.withStyle(Style.SuccessHeader).println "\t + applying user-defined root buildscript from ${buildScriptGradle}" + apply from: pathToBuildScriptGradle, to: buildscript + } + + nativescriptDependencies.each { dep -> + def pathToPluginBuildScriptGradle = "$rootDir/${dep.directory}/$PLATFORMS_ANDROID/rootbuildscript.gradle" + def pluginBuildScriptGradle = file(pathToPluginBuildScriptGradle) + if (pluginBuildScriptGradle.exists()) { + outLogger.withStyle(Style.SuccessHeader).println "\t + applying user-defined rootbuildscript from dependency ${pluginBuildScriptGradle}" + apply from: pathToPluginBuildScriptGradle, to: buildscript + } + } + } + + initialize() + applyBuildScriptConfigurations() + applyBeforePluginGradleConfiguration() + + def computeKotlinVersion = { -> project.hasProperty("kotlinVersion") ? kotlinVersion : "${ns_default_kotlin_version}" } + def computeBuildToolsVersion = { -> project.hasProperty("androidBuildToolsVersion") ? androidBuildToolsVersion : "${NS_DEFAULT_ANDROID_BUILD_TOOLS_VERSION}" } + def kotlinVersion = computeKotlinVersion() + def androidBuildToolsVersion = computeBuildToolsVersion() + + repositories { + google() + mavenCentral() + } + dependencies { + classpath "com.android.tools.build:gradle:$androidBuildToolsVersion" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" + classpath "org.apache.groovy:groovy-all:4.0.21" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } + beforeEvaluate { project -> + if (rootProject.hasProperty("userDefinedGradleProperties")) { + rootProject.ext.userDefinedGradleProperties.each { entry -> + def propertyName = entry.getKey() + def propertyValue = entry.getValue() + project.ext.set(propertyName, propertyValue) + } + } + + } +} + +task clean (type:Delete) { + delete rootProject.layout.buildDirectory.get().asFile +} \ No newline at end of file diff --git a/vendor/gradle-app/settings.gradle b/vendor/gradle-app/settings.gradle new file mode 100644 index 0000000000..e1226cf416 --- /dev/null +++ b/vendor/gradle-app/settings.gradle @@ -0,0 +1,78 @@ +rootProject.name = "__PROJECT_NAME__" +include ':app'//, ':runtime', ':runtime-binding-generator' + +//project(':runtime').projectDir = new File("${System.env.ANDROID_RUNTIME_HOME}/test-app/runtime") +//project(':runtime-binding-generator').projectDir = new File("${System.env.ANDROID_RUNTIME_HOME}/test-app/runtime-binding-generator") + +file("google-services.json").renameTo(file("./app/google-services.json")) + +import org.gradle.internal.logging.text.StyledTextOutputFactory + +import java.nio.file.Paths + +import org.gradle.internal.logging.text.StyledTextOutputFactory +import groovy.json.JsonSlurper +import static org.gradle.internal.logging.text.StyledTextOutput.Style + +def USER_PROJECT_ROOT = "$rootDir/../../" +def outLogger = services.get(StyledTextOutputFactory).create("colouredOutputLogger") +def ext = { + appResourcesPath = getProperty("appResourcesPath") + appPath = getProperty("appPath") +} + +def getAppPath = { -> + def relativePathToApp = "app" + def nsConfigFile = file("$USER_PROJECT_ROOT/nsconfig.json") + def nsConfig + + if (nsConfigFile.exists()) { + nsConfig = new JsonSlurper().parseText(nsConfigFile.getText("UTF-8")) + } + + if (ext.appPath) { + // when appPath is passed through -PappPath=/path/to/app + // the path could be relative or absolute - either case will work + relativePathToApp = ext.appPath + } else if (nsConfig != null && nsConfig.appPath != null) { + relativePathToApp = nsConfig.appPath + } + + return Paths.get(USER_PROJECT_ROOT).resolve(relativePathToApp).toAbsolutePath() + } + + +def getAppResourcesPath = { -> + def relativePathToAppResources + def absolutePathToAppResources + def nsConfigFile = file("$USER_PROJECT_ROOT/nsconfig.json") + def nsConfig + + if (nsConfigFile.exists()) { + nsConfig = new JsonSlurper().parseText(nsConfigFile.getText("UTF-8")) + } + if (ext.appResourcesPath) { + // when appResourcesPath is passed through -PappResourcesPath=/path/to/App_Resources + // the path could be relative or absolute - either case will work + relativePathToAppResources = ext.appResourcesPath + absolutePathToAppResources = Paths.get(USER_PROJECT_ROOT).resolve(relativePathToAppResources).toAbsolutePath() + } else if (nsConfig != null && nsConfig.appResourcesPath != null) { + relativePathToAppResources = nsConfig.appResourcesPath + absolutePathToAppResources = Paths.get(USER_PROJECT_ROOT).resolve(relativePathToAppResources).toAbsolutePath() + } else { + absolutePathToAppResources = "${getAppPath()}/App_Resources" + } + return absolutePathToAppResources +} + +def applySettingsGradleConfiguration = { -> + def appResourcesPath = getAppResourcesPath() + def pathToSettingsGradle = "$appResourcesPath/Android/settings.gradle" + def settingsGradle = file(pathToSettingsGradle) + if (settingsGradle.exists()) { + outLogger.withStyle(Style.SuccessHeader).println "\t + applying user-defined configuration from ${settingsGradle}" + apply from: pathToSettingsGradle + } +} + +applySettingsGradleConfiguration() diff --git a/vendor/gradle-plugin/build.gradle b/vendor/gradle-plugin/build.gradle index ee6de65efd..2b6e9f1f62 100644 --- a/vendor/gradle-plugin/build.gradle +++ b/vendor/gradle-plugin/build.gradle @@ -7,94 +7,130 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-parcelize' +def loadPropertyFile = { path -> + try { + if(project.hasProperty("loadedProperties_${path}")) { + logger.info "\t + gradle properties already loaded. SKIPPING" + } else { + logger.info "\t + trying to load gradle properties from \"$path\"" + + Properties properties = new Properties() + properties.load(new FileInputStream("$path")) + properties.each { prop -> + logger.info "\t + [$path] setting ${prop.key} = ${prop.value}" + project.ext.set(prop.key, prop.value) + } + project.ext.set("loadedProperties_${path}", true) + + outLogger.withStyle(Style.SuccessHeader).println "\t + loaded gradle properties from \"$path\"" + } + } catch(Exception ex) { + logger.warn "\t + failed to load gradle properties from \"$path\". Error is: ${ex.getMessage()}" + } +} + buildscript { - // project.ext.USER_PROJECT_ROOT = "$rootDir/../../.." - project.ext.PLATFORMS_ANDROID = "platforms/android" - project.ext.PLUGIN_NAME = "{{pluginName}}" + def GRADLE_PROPERTIES_FILENAME = "gradle.properties" - def USER_PROJECT_ROOT_FROM_ENV = System.getenv('USER_PROJECT_ROOT'); - if (USER_PROJECT_ROOT_FROM_ENV != null && !USER_PROJECT_ROOT_FROM_ENV.equals("")) { - project.ext.USER_PROJECT_ROOT = USER_PROJECT_ROOT_FROM_ENV; - } else { - project.ext.USER_PROJECT_ROOT = "$rootDir/../../../" + def getFile = { dir, filename -> + File file = new File("$dir$File.separator$filename") + file?.exists() ? file : null } - def USER_PROJECT_PLATFORMS_ANDROID_FROM_ENV = System.getenv('USER_PROJECT_PLATFORMS_ANDROID'); - if (USER_PROJECT_PLATFORMS_ANDROID_FROM_ENV != null && !USER_PROJECT_PLATFORMS_ANDROID_FROM_ENV.equals("")) { - project.ext.USER_PROJECT_PLATFORMS_ANDROID = USER_PROJECT_PLATFORMS_ANDROID_FROM_ENV; - } else { - project.ext.USER_PROJECT_PLATFORMS_ANDROID = project.ext.USER_PROJECT_ROOT + PLATFORMS_ANDROID + def getPropertyFile = { dir -> + return getFile(dir, GRADLE_PROPERTIES_FILENAME) } + def getUserProperties = { dir -> + def file = getPropertyFile(dir) + if (!file) { + return null + } + Properties properties = new Properties() + properties.load(file.newInputStream()) - def getDepPlatformDir = { dep -> - file("${project.ext.USER_PROJECT_PLATFORMS_ANDROID}/${dep.directory}/$PLATFORMS_ANDROID") - } - def computeKotlinVersion = { -> project.hasProperty("kotlinVersion") ? kotlinVersion : "2.2.20" } - def kotlinVersion = computeKotlinVersion() - repositories { - google() - mavenCentral() + return properties } - dependencies { - classpath 'com.android.tools.build:gradle:{{runtimeAndroidPluginVersion}}' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" - - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files + def loadPropertyFile = { path -> + try { + if(project.hasProperty("loadedProperties_${path}")) { + logger.info "\t + gradle properties already loaded. SKIPPING" + } else { + logger.info "\t + trying to load gradle properties from \"$path\"" + + Properties properties = new Properties() + properties.load(new FileInputStream("$path")) + properties.each { prop -> + logger.info "\t + [$path] setting ${prop.key} = ${prop.value}" + project.ext.set(prop.key, prop.value) + } + project.ext.set("loadedProperties_${path}", true) + + outLogger.withStyle(Style.SuccessHeader).println "\t + loaded gradle properties from \"$path\"" + } + } catch(Exception ex) { + logger.warn "\t + failed to load gradle properties from \"$path\". Error is: ${ex.getMessage()}" + } } + - // Set up styled logger - project.ext.getDepPlatformDir = getDepPlatformDir - project.ext.outLogger = services.get(StyledTextOutputFactory).create("colouredOutputLogger") - - // the build script will not work with previous versions of the CLI (3.1 or earlier) - def dependenciesJson = file("${project.ext.USER_PROJECT_PLATFORMS_ANDROID}/dependencies.json") - def appDependencies = new JsonSlurper().parseText(dependenciesJson.text) - def pluginData = appDependencies.find { it.name == project.ext.PLUGIN_NAME } - project.ext.nativescriptDependencies = appDependencies.findAll{pluginData.dependencies.contains(it.name)} project.ext.getAppPath = { -> - def relativePathToApp = "app" - def nsConfigFile = file("$USER_PROJECT_ROOT/nsconfig.json") - def nsConfig - - if (nsConfigFile.exists()) { - nsConfig = new JsonSlurper().parseText(nsConfigFile.getText("UTF-8")) - } + // def nsConfigFile = file("$USER_PROJECT_ROOT/nsconfig.json") + // def nsConfig + // if (nsConfigFile.exists()) { + // nsConfig = new JsonSlurper().parseText(nsConfigFile.getText("UTF-8")) + // } + def relativePathToApp = "app" if (project.hasProperty("appPath")) { // when appPath is passed through -PappPath=/path/to/app // the path could be relative or absolute - either case will work relativePathToApp = appPath - } else if (nsConfig != null && nsConfig.appPath != null) { - relativePathToApp = nsConfig.appPath + // } else if (nsConfig != null && nsConfig.appPath != null) { + // relativePathToApp = nsConfig.appPath } project.ext.appPath = Paths.get(USER_PROJECT_ROOT).resolve(relativePathToApp).toAbsolutePath() return project.ext.appPath } - + project.ext.getBuildPath = { -> + // def nsConfigFile = file("$USER_PROJECT_ROOT/nsconfig.json") + // def nsConfig + + // if (nsConfigFile.exists()) { + // nsConfig = new JsonSlurper().parseText(nsConfigFile.getText("UTF-8")) + // } + def relativeBuildToApp = "platforms" + if (project.hasProperty("appBuildPath")) { + relativeBuildToApp = appBuildPath + // } else if (nsConfig != null && nsConfig.buildPath != null) { + // relativeBuildToApp = nsConfig.buildPath + } + project.ext.relativeBuildPath = relativeBuildToApp + project.ext.buildPath = Paths.get(USER_PROJECT_ROOT).resolve(relativeBuildToApp).toAbsolutePath() + return project.ext.relativeBuildPath + } project.ext.getAppResourcesPath = { -> + // def nsConfigFile = file("$USER_PROJECT_ROOT/nsconfig.json") + // def nsConfig + + // if (nsConfigFile.exists()) { + // nsConfig = new JsonSlurper().parseText(nsConfigFile.getText("UTF-8")) + // } def relativePathToAppResources def absolutePathToAppResources - def nsConfigFile = file("$USER_PROJECT_ROOT/nsconfig.json") - def nsConfig - - if (nsConfigFile.exists()) { - nsConfig = new JsonSlurper().parseText(nsConfigFile.getText("UTF-8")) - } if (project.hasProperty("appResourcesPath")) { // when appResourcesPath is passed through -PappResourcesPath=/path/to/App_Resources // the path could be relative or absolute - either case will work relativePathToAppResources = appResourcesPath absolutePathToAppResources = Paths.get(USER_PROJECT_ROOT).resolve(relativePathToAppResources).toAbsolutePath() - } else if (nsConfig != null && nsConfig.appResourcesPath != null) { - relativePathToAppResources = nsConfig.appResourcesPath - absolutePathToAppResources = Paths.get(USER_PROJECT_ROOT).resolve(relativePathToAppResources).toAbsolutePath() + // } else if (nsConfig != null && nsConfig.appResourcesPath != null) { + // relativePathToAppResources = nsConfig.appResourcesPath + // absolutePathToAppResources = Paths.get(USER_PROJECT_ROOT).resolve(relativePathToAppResources).toAbsolutePath() } else { - absolutePathToAppResources = "${getAppPath()}/App_Resources" + absolutePathToAppResources = "${project.ext.getAppPath()}/App_Resources" } project.ext.appResourcesPath = absolutePathToAppResources @@ -102,8 +138,65 @@ buildscript { return absolutePathToAppResources } + project.ext.applyBeforePluginGradleConfiguration = { -> + def appResourcesPath = project.ext.getAppResourcesPath() + def pathToBeforePluginGradle = "$appResourcesPath/Android/before-plugins.gradle" + def beforePluginGradle = file(pathToBeforePluginGradle) + if (beforePluginGradle.exists()) { + outLogger.withStyle(Style.SuccessHeader).println "\t ~ applying user-defined configuration from ${beforePluginGradle}" + apply from: pathToBeforePluginGradle + } + } + + + def initialize = { -> + // set up our logger + project.ext.outLogger = services.get(StyledTextOutputFactory).create("colouredOutputLogger") + outLogger.withStyle(Style.SuccessHeader).println "\t ~initialize" + + + project.ext.USER_PROJECT_ROOT = "$rootDir/../.." + if (project.hasProperty('projectRoot')) { + project.ext.USER_PROJECT_ROOT = projectRoot + } + project.ext.PLATFORMS_ANDROID = project.ext.getBuildPath() + "/android" + project.ext.PLUGIN_NAME = "{{pluginName}}" + + def userDir = "$USER_PROJECT_ROOT" + rootProject.ext.userDefinedGradleProperties = getUserProperties("${project.ext.getAppResourcesPath()}/Android") + + loadPropertyFile("$USER_PROJECT_ROOT/${project.ext.PLATFORMS_ANDROID}/gradle.properties") + loadPropertyFile("$USER_PROJECT_ROOT/${project.ext.PLATFORMS_ANDROID}/additional_gradle.properties") + + if (rootProject.hasProperty("userDefinedGradleProperties")) { + rootProject.ext.userDefinedGradleProperties.each { entry -> + def propertyName = entry.getKey() + def propertyValue = entry.getValue() + project.ext.set(propertyName, propertyValue) + } + } + + def getDepPlatformDir = { dep -> + file("${project.ext.USER_PROJECT_ROOT}/${project.ext.PLATFORMS_ANDROID}/${dep.directory}/platforms/android") + } + + // Set up styled logger + project.ext.getDepPlatformDir = getDepPlatformDir + project.ext.outLogger = services.get(StyledTextOutputFactory).create("colouredOutputLogger") + + + // the build script will not work with previous versions of the CLI (3.1 or earlier) + def dependenciesJson = file("${project.ext.USER_PROJECT_ROOT}/${project.ext.PLATFORMS_ANDROID}/dependencies.json") + def appDependencies = new JsonSlurper().parseText(dependenciesJson.text) + def pluginData = appDependencies.find { it.name == project.ext.PLUGIN_NAME } + project.ext.nativescriptDependencies = appDependencies.findAll{pluginData.dependencies.contains(it.name)}.plus([pluginData]) + } + + initialize() + project.ext.applyBeforePluginGradleConfiguration() + def applyBuildScriptConfigurations = { -> - def absolutePathToAppResources = getAppResourcesPath() + def absolutePathToAppResources = project.ext.getAppResourcesPath() def pathToBuildScriptGradle = "$absolutePathToAppResources/Android/buildscript.gradle" def buildScriptGradle = file(pathToBuildScriptGradle) if (buildScriptGradle.exists()) { @@ -118,6 +211,12 @@ buildscript { outLogger.withStyle(Style.SuccessHeader).println "\t + applying user-defined buildscript from dependency ${pluginBuildScriptGradle}" apply from: pathToPluginBuildScriptGradle, to: buildscript } + // def pathToPluginProjectBuildScriptGradle = "${getDepPlatformDir(dep)}/project-buildscript.gradle" + // def pluginProjectBuildScriptGradle = file(pathToPluginProjectBuildScriptGradle) + // if (pluginProjectBuildScriptGradle.exists()) { + // outLogger.withStyle(Style.SuccessHeader).println "\t + applying user-defined project-buildscript from dependency ${pluginProjectBuildScriptGradle}" + // apply from: pathToPluginProjectBuildScriptGradle, to: project + // } } def pathToPluginBuildScriptGradle = "$rootDir/buildscript.gradle" @@ -127,8 +226,24 @@ buildscript { apply from: pathToPluginBuildScriptGradle, to: buildscript } } + applyBuildScriptConfigurations() + def computeKotlinVersion = { -> project.hasProperty("kotlinVersion") ? kotlinVersion : "${ns_default_kotlin_version}" } + def computeBuildToolsVersion = { -> project.hasProperty("androidBuildToolsVersion") ? androidBuildToolsVersion : "{{runtimeAndroidPluginVersion}}" } + def kotlinVersion = computeKotlinVersion() + def androidBuildToolsVersion = computeBuildToolsVersion() + + repositories { + google() + mavenCentral() + } + dependencies { + classpath "com.android.tools.build:gradle:$androidBuildToolsVersion" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" + classpath "org.codehaus.groovy:groovy-all:3.0.8" + } + } def pluginDependencies @@ -167,83 +282,92 @@ def computeBuildToolsVersion = { -> android { namespace "{{pluginNamespace}}" - - kotlinOptions { - jvmTarget = '17' - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - - - if (project.hasProperty("ndkVersion")) { - ndkVersion project.ndkVersion - } - def applyPluginGradleConfigurations = { -> nativescriptDependencies.each { dep -> def includeGradlePath = "${getDepPlatformDir(dep)}/include.gradle" if (file(includeGradlePath).exists()) { + outLogger.withStyle(Style.SuccessHeader).println "\t + applying plugin include.gradle from dependency ${includeGradlePath}" apply from: includeGradlePath } } } - applyBeforePluginGradleConfiguration() + + project.ext.applyBeforePluginGradleConfiguration() applyPluginGradleConfigurations() compileSdkVersion computeCompileSdkVersion() buildToolsVersion computeBuildToolsVersion() + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + if (project.hasProperty("ndkVersion")) { + ndkVersion project.ndkVersion + } + defaultConfig { targetSdkVersion computeTargetSdkVersion() versionCode 1 versionName "1.0" } -} - - -def applyBeforePluginGradleConfiguration() { - def appResourcesPath = getAppResourcesPath() - def pathToBeforePluginGradle = "$appResourcesPath/Android/before-plugins.gradle" - def beforePluginGradle = file(pathToBeforePluginGradle) - if (beforePluginGradle.exists()) { - outLogger.withStyle(Style.SuccessHeader).println "\t ~ applying user-defined configuration from ${beforePluginGradle}" - apply from: pathToBeforePluginGradle + lintOptions { + checkReleaseBuilds false + abortOnError false } } task addDependenciesFromNativeScriptPlugins { nativescriptDependencies.each { dep -> - def aarFiles = fileTree(dir: getDepPlatformDir(dep), include: ["**/*.aar"]) + def aarFiles = fileTree(dir: getDepPlatformDir(dep)).matching { + include "**/*.aar" + exclude "cpp/**" + exclude project.hasProperty("aarIgnoreFilter") ? project.findProperty('aarIgnoreFilter').split(',').collect{it as String} : [] + } + def currentDirname = file(project.buildscript.sourceFile).getParentFile().getName() aarFiles.each { aarFile -> def length = aarFile.name.length() - 4 def fileName = aarFile.name[0.. def jarFileAbsolutePath = jarFile.getAbsolutePath() outLogger.withStyle(Style.SuccessHeader).println "\t + adding jar plugin dependency: $jarFileAbsolutePath" - pluginsJarLibraries.add(jarFile.getAbsolutePath()) + // pluginsJarLibraries.add(jarFile.getAbsolutePath()) } project.dependencies.add("implementation", jarFiles) } } -tasks.whenTaskAdded({ DefaultTask currentTask -> - if (currentTask.name == 'bundleRelease' || currentTask.name == 'bundleDebug') { +// This wont work with gradle 8 + should be the same as the code just after +// afterEvaluate { +// def generateBuildConfig = project.hasProperty("generateBuildConfig") ? project.generateBuildConfig : false +// def generateR = project.hasProperty("generateR") ? project.generateR : false +// generateReleaseBuildConfig.enabled = generateBuildConfig +// generateDebugBuildConfig.enabled = generateBuildConfig +// generateReleaseResValues.enabled = generateR +// generateDebugResValues.enabled = generateR +// } +project.tasks.configureEach { + if (name == 'bundleRelease') { def generateBuildConfig = project.hasProperty("generateBuildConfig") ? project.generateBuildConfig : false def generateR = project.hasProperty("generateR") ? project.generateR : false if (!generateBuildConfig) { - currentTask.exclude '**/BuildConfig.class' + it.exclude '**/BuildConfig.class' } if (!generateR) { - currentTask.exclude '**/R.class', '**/R$*.class' + it.exclude '**/R.class', '**/R$*.class' } } -}) +} diff --git a/vendor/gradle-plugin/gradle.properties b/vendor/gradle-plugin/gradle.properties index 84a8b08c9c..5cccde2d62 100644 --- a/vendor/gradle-plugin/gradle.properties +++ b/vendor/gradle-plugin/gradle.properties @@ -1,22 +1,6 @@ -# Project-wide Gradle settings. - -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. - -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html - -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -#org.gradle.parallel=true - -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. +# Nativescript CLI plugin build gradle properties org.gradle.jvmargs=-Xmx16384M android.enableJetifier=true android.useAndroidX=true -android.nonTransitiveRClass=true -android.enableSeparateRClassCompilation=true \ No newline at end of file +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/vendor/gradle-plugin/settings.gradle b/vendor/gradle-plugin/settings.gradle index d8e66382d3..e119ac9c80 100644 --- a/vendor/gradle-plugin/settings.gradle +++ b/vendor/gradle-plugin/settings.gradle @@ -1,26 +1,38 @@ import groovy.json.JsonSlurper -// def USER_PROJECT_ROOT = "$rootDir/../../../" -def PLATFORMS_ANDROID = "platforms/android" -def PLUGIN_NAME = "{{pluginName}}" -def USER_PROJECT_PLATFORMS_ANDROID -def USER_PROJECT_PLATFORMS_ANDROID_FROM_ENV = System.getenv('USER_PROJECT_PLATFORMS_ANDROID'); -if (USER_PROJECT_PLATFORMS_ANDROID_FROM_ENV != null && !USER_PROJECT_PLATFORMS_ANDROID_FROM_ENV.equals("")) { - USER_PROJECT_PLATFORMS_ANDROID = USER_PROJECT_PLATFORMS_ANDROID_FROM_ENV; -} else { - USER_PROJECT_PLATFORMS_ANDROID = "$rootDir/../../../platforms/android" +def getProjectRoot = { -> + def projectRoot = "$rootDir/../../" + if (System.getProperties().projectRoot != null) { + projectRoot = System.getProperties().projectRoot + } + return projectRoot +} + +def getBuildPath = { -> + def relativeBuildToApp = "platforms" + if (System.getProperties().appBuildPath != null) { + relativeBuildToApp = System.getProperties().appBuildPath + } + return relativeBuildToApp } -def dependenciesJson = file("${USER_PROJECT_PLATFORMS_ANDROID}/dependencies.json") +def USER_PROJECT_ROOT = getProjectRoot() +def PLATFORMS_ANDROID = getBuildPath() + "/android" +def PLUGIN_NAME = "{{pluginName}}" + + + +def dependenciesJson = file("${USER_PROJECT_ROOT}/${PLATFORMS_ANDROID}/dependencies.json") def appDependencies = new JsonSlurper().parseText(dependenciesJson.text) def pluginData = appDependencies.find { it.name == PLUGIN_NAME } def nativescriptDependencies = appDependencies.findAll{pluginData.name == it.name} def getDepPlatformDir = { dep -> - file("$USER_PROJECT_PLATFORMS_ANDROID/${dep.directory}/$PLATFORMS_ANDROID") + file("$USER_PROJECT_ROOT/$PLATFORMS_ANDROID/${dep.directory}/$PLATFORMS_ANDROID") } + def applyIncludeSettingsGradlePlugin = { nativescriptDependencies.each { dep -> def includeSettingsGradlePath = "${getDepPlatformDir(dep)}/include-settings.gradle" From 77cfbb67ce2f5284d412c90d6a3e0c61f5f8dd3d Mon Sep 17 00:00:00 2001 From: Martin Guillon Date: Wed, 19 Aug 2026 11:34:19 +0200 Subject: [PATCH 2/3] feat(android): build only the requested abis in the bundled app build.gradle `-PabiFilters=[,]` now narrows the native build down to those abis instead of being ignored, and an apk debug build splits so each abi gets its own package. `-PsplitEnabled` forces the split on for any build type; `-PonlyX86` keeps its old meaning and disables splitting. The property is what the CLI passes for the devices a run is about to deploy to, so this needs #6130 to be fully functional - on its own it only makes the property meaningful for anyone passing it through `--gradleArgs`. Co-Authored-By: Claude Opus 5 (1M context) --- vendor/gradle-app/app/build.gradle | 34 +++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/vendor/gradle-app/app/build.gradle b/vendor/gradle-app/app/build.gradle index adac695f5c..01c5ee5064 100644 --- a/vendor/gradle-app/app/build.gradle +++ b/vendor/gradle-app/app/build.gradle @@ -18,6 +18,8 @@ * -PandroidXMaterial=[androidx_material_version] * -PappPath=[app_path] * -PappResourcesPath=[app_resources_path] +* -PabiFilters=[abi][,abi...] //build only these abis +* -PsplitEnabled //one apk per abi */ import groovy.io.FileType @@ -50,6 +52,23 @@ if (onlyX86) { outLogger.withStyle(Style.Info).println "OnlyX86 build triggered." } +def DEFAULT_ABIS = ["x86", "x86_64", "armeabi-v7a", "arm64-v8a"] +// -PabiFilters=[,] narrows the build down to those abis. The CLI passes the abis of the +// devices it is about to deploy to, unless --no-filter-devices-arch was given. +def requestedAbis = project.hasProperty("abiFilters") + ? project.findProperty("abiFilters").split(",").collect { it.trim() as String }.findAll { it } + : null +// splitting gives one apk per abi, so every device can be handed the one it needs. Only apk debug +// builds split: a release artifact, like an app bundle, is meant to be shipped as a whole. +def isDebugApkBuild = gradle.startParameter.taskNames.any { taskName -> + def name = taskName.toLowerCase() + name.startsWith("assemble") && name.endsWith("debug") +} +def splitEnabled = !onlyX86 && (project.hasProperty("splitEnabled") || (requestedAbis && isDebugApkBuild)) +if (requestedAbis) { + outLogger.withStyle(Style.Info).println "Building abis: ${requestedAbis.join(", ")}${splitEnabled ? " (one apk each)" : ""}" +} + //common def BUILD_TOOLS_PATH = "$rootDir/build-tools" def PASSED_TYPINGS_PATH = System.getenv("TNS_TYPESCRIPT_DECLARATIONS_PATH") @@ -205,12 +224,25 @@ android { ndk { if (onlyX86) { abiFilters 'x86' + } else if (splitEnabled) { + // the splits block below decides which abi ends up in which apk + } else if (requestedAbis) { + abiFilters.addAll(requestedAbis) } else { - abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' + abiFilters.addAll(DEFAULT_ABIS) } } } + splits { + abi { + enable splitEnabled + reset() + include(*(requestedAbis ?: DEFAULT_ABIS)) + universalApk false + } + } + if (project.hasProperty("ndkVersion")) { ndkVersion project.ndkVersion } From 108f3c2693ae8ffb84ffe75bedffad3d7eefa3db Mon Sep 17 00:00:00 2001 From: Martin Guillon Date: Wed, 19 Aug 2026 23:28:05 +0200 Subject: [PATCH 3/3] fix: app gradle supports all latest napi features --- vendor/gradle-app/app/build.gradle | 525 ++++++++++++++++++++++++++--- 1 file changed, 480 insertions(+), 45 deletions(-) diff --git a/vendor/gradle-app/app/build.gradle b/vendor/gradle-app/app/build.gradle index 01c5ee5064..463a9d4a0f 100644 --- a/vendor/gradle-app/app/build.gradle +++ b/vendor/gradle-app/app/build.gradle @@ -52,7 +52,7 @@ if (onlyX86) { outLogger.withStyle(Style.Info).println "OnlyX86 build triggered." } -def DEFAULT_ABIS = ["x86", "x86_64", "armeabi-v7a", "arm64-v8a"] +def DEFAULT_ABIS = ["x86_64", "armeabi-v7a", "arm64-v8a"] // -PabiFilters=[,] narrows the build down to those abis. The CLI passes the abis of the // devices it is about to deploy to, unless --no-filter-devices-arch was given. def requestedAbis = project.hasProperty("abiFilters") @@ -88,11 +88,13 @@ def SBG_BINDINGS_NAME = "sbg-bindings.txt" def SBG_INTERFACE_NAMES = "sbg-interface-names.txt" def INPUT_JS_DIR = "$projectDir/src/main/assets/app" def OUTPUT_JAVA_DIR = "$projectDir/src/main/java" +def APP_DIR = "$projectDir/src/main/assets/app" //metadata generator def MDG_OUTPUT_DIR = "mdg-output-dir.txt" def MDG_JAVA_DEPENDENCIES = "mdg-java-dependencies.txt" def METADATA_OUT_PATH = "$projectDir/src/main/assets/metadata" +def METADATA_KEEP_RULES = layout.buildDirectory.file("nativescript/metadata-keep-rules.pro").get().asFile.absolutePath def METADATA_JAVA_OUT = "mdg-java-out.txt" // paths to jar libraries @@ -109,15 +111,20 @@ def computeBuildToolsVersion = { -> def enableAnalytics = (project.hasProperty("gatherAnalyticsData") && project.gatherAnalyticsData == "true") def enableVerboseMDG = project.gradle.startParameter.logLevel.name() == 'DEBUG' + def analyticsFilePath = "$rootDir/analytics/build-statistics.json" def analyticsCollector = project.ext.AnalyticsCollector.withOutputPath(analyticsFilePath) if (enableAnalytics) { analyticsCollector.markUseKotlinPropertyInApp(true) analyticsCollector.writeAnalyticsFile() } - project.ext.selectedBuildType = project.hasProperty("release") ? "release" : "debug" +// The engine name comes from the runtime's gradle.properties (ns_engine). Older +// runtimes don't declare it and have no bytecode compiler at all, so the whole +// feature stays off unless the property is present. +def napiEngine = project.hasProperty("ns_engine") ? ns_engine as String : null + buildscript { def applyBuildScriptConfigurations = { -> def absolutePathToAppResources = getAppResourcesPath() @@ -256,6 +263,19 @@ android { jniLibs.srcDirs = ["$projectDir/libs/jni", "$projectDir/snapshot-build/build/ndk-build/libs"] } + packagingOptions { + // The Hermes engine build pulls in fbjni, which ships its own + // libc++_shared.so alongside the one the runtime module already + // packages. The runtime module resolves this for its own merge, but the + // app merges the runtime's jniLibs with fbjni's, so it needs the same + // rule or :app:mergeDebugNativeLibs fails with "2 files found with path + // lib//libc++_shared.so". Both copies are the NDK's, so either is + // fine. libfbjni.so collides the same way: the runtime module packages a + // copy from its jniLibs and the fbjni prefab supplies another. + pickFirst "**/libc++_shared.so" + pickFirst "**/libfbjni.so" + } + signingConfigs { release { if (project.hasProperty("ksPath") && @@ -281,6 +301,17 @@ android { } release { signingConfig signingConfigs.release + + // Shrink the dex with the same set the metadata was filtered to. + // Only together with -PnsFilterMetadata: without the generated keep + // rules R8 cannot see that JS reaches these classes at all, and + // would strip everything the app depends on. + if (project.hasProperty("nsFilterMetadata")) { + minifyEnabled true + proguardFiles getDefaultProguardFile("proguard-android.txt"), + METADATA_KEEP_RULES + } + } } @@ -502,8 +533,112 @@ if (failOnCompilationWarningsEnabled()) { ///////////////////////////// EXECUTION PHASE ///////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////// +// Compiles the app's own Java so the static binding generator can see it. +// +// Without this the generator detects an `extend` of an app class perfectly -- +// 48 rows for com.tns.tests.Button1 alone -- and then generates nothing for it, +// because it cannot load the base class to read its methods. Only classes from +// android.jar and the AAR dependencies got bindings; everything the app itself +// declared fell through to being dexed on the device at run time. +// +// It cannot simply depend on the normal javac task: that one consumes the +// generator's output, so the dependency would be circular. This compiles the +// same sources *minus* the generated bindings into a scratch directory nothing +// else reads, which breaks the cycle. +// +// Best-effort by design. A source that fails here (most often a Java class +// referring to a Kotlin one, which is not compiled at this point) simply is not +// on the generator's classpath, which is exactly today's behaviour for it. The +// real javac still compiles everything and still fails the build if it cannot. +task compileAppClassesForSbg { + dependsOn "collectAllJars" + + def outDir = layout.buildDirectory.dir("nativescript/sbg-app-classes").get().asFile + def sourceRoot = file(OUTPUT_JAVA_DIR) + + inputs.files(fileTree(sourceRoot) { include "**/*.java"; exclude "com/tns/gen/**" }) + outputs.dir(outDir) + + doLast { + def sources = [] + sourceRoot.eachFileRecurse { f -> + if (f.isFile() && f.name.endsWith(".java") + && !f.absolutePath.contains("${File.separator}com${File.separator}tns${File.separator}gen${File.separator}")) { + sources.add(f.absolutePath) + } + } + + if (sources.isEmpty()) { + return + } + + outDir.deleteDir() + outDir.mkdirs() + + // The classpath collectAllJars just wrote, plus whatever the runtime + // module has already produced -- com.tns.* is what the app's own + // classes extend. + def cp = [] + def depsFile = file("$BUILD_TOOLS_PATH/$SBG_JAVA_DEPENDENCIES") + if (depsFile.exists()) { + depsFile.eachLine { line -> if (line.trim()) cp.add(line.trim()) } + } + def runtimeProject = findProject(':runtime') + if (runtimeProject != null) { + runtimeProject.fileTree(runtimeProject.buildDir) { + include "intermediates/javac/**/classes" + include "tmp/kotlin-classes/**" + }.visit { d -> if (d.directory) cp.add(d.file.absolutePath) } + } + + def argsFile = new File(outDir.parentFile, "sbg-javac-args.txt") + argsFile.text = sources.collect { "\"${it.replace('\\', '/')}\"" }.join("\n") + + def cmd = ["javac", "-proc:none", "-nowarn", "-g", + "-source", "17", "-target", "17", + "-d", outDir.absolutePath] + if (!cp.isEmpty()) { + cmd.addAll(["-cp", cp.join(File.pathSeparator)]) + } + cmd.add("@${argsFile.absolutePath}".toString()) + + def process = new ProcessBuilder(cmd.collect { it.toString() }) + .redirectErrorStream(true).start() + def errors = 0 + process.inputStream.eachLine { line -> if (line.contains("error:")) errors++ } + process.waitFor() + + def produced = 0 + if (outDir.exists()) { + outDir.eachFileRecurse { f -> if (f.isFile() && f.name.endsWith(".class")) produced++ } + } + outLogger.withStyle(Style.Info).println( + "\t ~ [sbg] compiled ${produced} app class file(s) for binding generation" + + (errors > 0 ? " (${errors} source(s) skipped -- see above)" : "")) + } +} + +// Name generated bindings after what they contain rather than after the extend() +// call site. Off by default; an app opts in with -PuseContentBindings, or with +// +// project.ext.useContentBindings = true +// +// in App_Resources/Android/app.gradle or before-plugins.gradle. The value drives +// both halves of the scheme from one place: the static binding generator (which +// writes the names) and the runtime (which looks them up, via the app config +// written by writeRuntimeAppConfig). -Pcontent-keyed-bindings is the older +// spelling, kept because it is what a runtime build passes to :runtime. +def nsContentKeyedBindings = ["useContentBindings", "content-keyed-bindings"].any { name -> + project.hasProperty(name) && + (project.property(name).toString().isEmpty() + || Boolean.parseBoolean(project.property(name).toString())) +} + task runSbg(type: BuildToolTask) { dependsOn "collectAllJars" + if (napiEngine != null) { + dependsOn "compileAppClassesForSbg" + } def rootPath = "" if (!findProject(':static-binding-generator').is(null)) { rootPath = Paths.get(project(':static-binding-generator').projectDir.path, "build/libs").toString() @@ -523,6 +658,19 @@ task runSbg(type: BuildToolTask) { if (failOnCompilationWarningsEnabled()) { paramz.add("-show-deprecation-warnings") } + if (napiEngine != null) { + if (ns_engine == "PRIMJS") { + paramz.add("-line-column-primjs") + } + + // The generated names and the names the runtime looks up are two halves of + // one scheme, and half of it is a silent LookedUpClassNotFound. The other + // half is no longer a runtime rebuild: writeRuntimeAppConfig hands the same + // value to the runtime through the app's package.json. + if (nsContentKeyedBindings) { + paramz.add("-content-keyed-bindings") + } + } setOutputs outLogger @@ -699,6 +847,9 @@ task 'collectAllJars' { new File("$BUILD_TOOLS_PATH/$SBG_JAVA_DEPENDENCIES").withWriter { out -> allJarPaths.each { out.println it } + // The app's own classes, compiled by compileAppClassesForSbg. A + // directory rather than a jar; the generator reads both. + out.println layout.buildDirectory.dir("nativescript/sbg-app-classes").get().asFile.absolutePath } new File("$BUILD_TOOLS_PATH/$MDG_JAVA_DEPENDENCIES").withWriter { out -> allJarPaths.each { @@ -719,13 +870,172 @@ task 'collectAllJars' { } } +// Derives whitelist.mdg from the app's own JS, so metadata carries only what +// the app can reach. Opt-in: without -PnsFilterMetadata the seed is removed and +// the generator emits everything, which is the behaviour every existing build +// already has. +// +// The seed is only a starting point -- the generator closes over supertypes, +// nested classes and signature types before dropping anything, and fails the +// build if that closure turns out to be unsound. See docs/metadata-filtering.md. +def nsFilterMetadata = project.hasProperty("nsFilterMetadata") + +// Build-owned, and deliberately NOT $rootDir/whitelist.mdg: that path belongs to +// the app. An app may ship its own hand-written whitelist.mdg / blacklist.mdg +// and filter without -PnsFilterMetadata at all, exactly as it could before the +// generated seed existed -- writing the seed there overwrote that file, and +// turning filtering off deleted it. +def nsGeneratedSeed = file("$projectDir/build/nativescript/generated-whitelist.mdg") + +task generateMetadataSeed { + def seedScript = "$BUILD_TOOLS_PATH/metadata-filter/seed.js" + def generatedSeed = nsGeneratedSeed + + inputs.property("enabled", nsFilterMetadata) + inputs.dir(APP_DIR) + inputs.files(fileTree("$projectDir/src/main/res")) + inputs.files(fileTree("$BUILD_TOOLS_PATH/metadata-filter") { include "*.js" }) + outputs.file(generatedSeed) + + doLast { + if (!nsFilterMetadata) { + // Leaving a stale seed behind would keep filtering on for a build + // that did not ask for it -- the confusing direction to fail. Only + // the generated file is removed; an app's own whitelist.mdg is + // untouched and still filters. + if (generatedSeed.exists()) { + generatedSeed.delete() + outLogger.withStyle(Style.Info).println "\t ~ metadata filtering off; removed the generated seed" + } + return + } + + generatedSeed.parentFile.mkdirs() + + def cmd = ["node", seedScript, APP_DIR, + "--manifest", "$projectDir/src/main/AndroidManifest.xml", + "--out", generatedSeed.absolutePath] + + // Custom views, fragments and behaviours are named in resource XML and + // instantiated by the framework; nothing in the JS mentions them. + def resDir = file("$projectDir/src/main/res") + if (resDir.exists()) { + cmd.addAll(["--res", resDir.absolutePath]) + } + + def bindings = file("$BUILD_TOOLS_PATH/$SBG_BINDINGS_NAME") + if (bindings.exists()) { + cmd.addAll(["--sbg-bindings", bindings.absolutePath]) + } + + // This app ships files that are invalid on purpose, to test how the + // loader reports them. Anywhere else an unparseable file disables + // filtering for the whole build. + cmd.addAll(["--allow-unparseable", "shared/Require/SyntaxErrorInModule", + "--allow-unparseable", "shared/Workers/WorkerInvalidSyntax"]) + + // ProcessBuilder needs real Strings; interpolated paths are GStrings. + def process = new ProcessBuilder(cmd.collect { it.toString() }) + .redirectErrorStream(true).start() + process.inputStream.eachLine { outLogger.withStyle(Style.Info).println "\t ~ [metadata-seed] $it" } + if (process.waitFor() != 0) { + throw new GradleException("NativeScript: metadata seed generation failed.") + } + } +} + +// Device-free guard against the one part of the seed that is written by hand. +// Wired into `check` so it runs in CI: the runtime naming a new class from +// native code is otherwise invisible until an app built with filtering on hits +// the code path that needs it. +task checkMetadataRuntimeKeepList { + group = "verification" + description = "Verifies RUNTIME_KEEP covers every class the runtime resolves by name." + + def runtimeSources = ["$rootDir/../../../NativeScript/ffi/jni/jsi", + "$rootDir/../../../NativeScript/runtime/android/jsi"] + def script = "$BUILD_TOOLS_PATH/metadata-filter/check-runtime-keeplist.js" + + inputs.files(fileTree("$BUILD_TOOLS_PATH/metadata-filter") { include "*.js" }) + runtimeSources.each { dir -> + if (file(dir).exists()) { + inputs.files(fileTree(dir) { include "**/*.cpp", "**/*.h" }) + } + } + outputs.upToDateWhen { false } + + doLast { + def cmd = ["node", script] + runtimeSources.findAll { file(it).exists() } + def process = new ProcessBuilder(cmd.collect { it.toString() }) + .redirectErrorStream(true).start() + process.inputStream.eachLine { outLogger.withStyle(Style.Info).println "\t ~ [keeplist] $it" } + if (process.waitFor() != 0) { + throw new GradleException( + "NativeScript: the metadata runtime keep-list is out of date. " + + "Add the classes listed above to RUNTIME_KEEP in " + + "build-tools/metadata-filter/seed.js.") + } + } +} +if (napiEngine != null) { + tasks.matching { it.name == "check" }.configureEach { it.dependsOn(checkMetadataRuntimeKeepList) } +} + task copyMetadataFilters { - outputs.files("$BUILD_TOOLS_PATH/whitelist.mdg", "$BUILD_TOOLS_PATH/blacklist.mdg") - // use an explicit copy task here because the copy task itselfs marks the whole built-tools as an output! - copy { - from file("$rootDir/whitelist.mdg"), file("$rootDir/blacklist.mdg") - into "$BUILD_TOOLS_PATH" + + if (napiEngine != null) { + dependsOn generateMetadataSeed + + // Declared so the task re-runs when a filter file appears or disappears. + // Without inputs it has none to compare, stays up to date on the strength + // of its outputs alone, and leaves the previous build's filter in place -- + // which silently keeps filtering on for a build that turned it off. + inputs.files("$rootDir/whitelist.mdg", "$rootDir/blacklist.mdg", nsGeneratedSeed) + outputs.files("$BUILD_TOOLS_PATH/whitelist.mdg", "$BUILD_TOOLS_PATH/blacklist.mdg") + + // Copies at execution time, not configuration time: the seed does not exist + // until generateMetadataSeed has run. Files.copy rather than project.copy, + // which Gradle 9 no longer allows during execution. + doLast { + // The whitelist MDG reads is the app's own file, the generated seed, or + // both concatenated. Either alone is a complete filter: an app with a + // hand-written whitelist and no -PnsFilterMetadata filters by its list + // (and MDG leaves it alone -- see enforceClosure in buildMetadata), + // while a build with the flag and no app file filters by the seed. + def whitelistParts = [] + def appWhitelist = file("$rootDir/whitelist.mdg") + if (appWhitelist.exists()) { + whitelistParts.add(appWhitelist.text) + } + if (nsFilterMetadata && nsGeneratedSeed.exists()) { + whitelistParts.add(nsGeneratedSeed.text) + } + + def whitelistTarget = file("$BUILD_TOOLS_PATH/whitelist.mdg") + if (whitelistParts.isEmpty()) { + whitelistTarget.delete() + } else { + whitelistTarget.text = whitelistParts.join("\n") + } + + def blacklistSource = file("$rootDir/blacklist.mdg") + def blacklistTarget = file("$BUILD_TOOLS_PATH/blacklist.mdg") + if (blacklistSource.exists()) { + Files.copy(blacklistSource.toPath(), blacklistTarget.toPath(), + StandardCopyOption.REPLACE_EXISTING) + } else { + blacklistTarget.delete() + } + } + } else { + outputs.files("$BUILD_TOOLS_PATH/whitelist.mdg", "$BUILD_TOOLS_PATH/blacklist.mdg") + // use an explicit copy task here because the copy task itselfs marks the whole built-tools as an output! + copy { + from file("$rootDir/whitelist.mdg"), file("$rootDir/blacklist.mdg") + into "$BUILD_TOOLS_PATH" + } } + } task 'copyMetadata' { @@ -941,6 +1251,21 @@ task buildMetadata(type: BuildToolTask) { // use explicit inputs as the above makes the whole build-tools directory an input! inputs.files("$BUILD_TOOLS_PATH/whitelist.mdg", "$BUILD_TOOLS_PATH/blacklist.mdg") + outLogger.withStyle(Style.SuccessHeader).println "\t + buildMetadata" + if (napiEngine != null) { + + // The generator itself decides what the metadata contains, so a change to + // it has to invalidate the output. Without this the task stays up-to-date + // across a generator rebuild and silently ships the previous run's tree. + inputs.files(Paths.get(rootPath, "android-metadata-generator.jar").toString()) + + // R8 keep rules derived from the same retained set as the metadata. Build + // output, not an asset -- shipping it would put 100KB+ of build input in + // the APK. + outputs.file(METADATA_KEEP_RULES) + outLogger.withStyle(Style.SuccessHeader).println "\t + output ${METADATA_KEEP_RULES}" + } + def classesDir = layout.buildDirectory.dir("intermediates/javac").get().asFile if (classesDir.exists()) { inputs.dir(classesDir) @@ -1036,50 +1361,134 @@ task buildMetadata(type: BuildToolTask) { paramz.add("verbose") } + if (napiEngine != null) { + + paramz.add("proguardOut=$METADATA_KEEP_RULES") + + // Grow the whitelist into a closure, and fail the build if that closure + // is unsound, only when the seed is the one we generated. An app's own + // whitelist.mdg is filtered as authored -- see the comment on + // generateMetadataSeed and Builder.applyClosureIfRequested. + if (nsFilterMetadata) { + paramz.add("enforceClosure=true") + } + } + args paramz.toArray() } } task generateTypescriptDefinitions(type: BuildToolTask) { - if (!findProject(':dts-generator').is(null)) { - dependsOn ':dts-generator:jar' - } + if (napiEngine != null) { + // allJarLibraries is filled by collectAllJars at execution time; without + // this the generator was invoked with an empty -input and failed. The task + // has been off by default, so nothing exercised it. + dependsOn "collectAllJars" - def paramz = new ArrayList() - def includeDirs = ["com.android.support", "/platforms/" + android.compileSdkVersion] + def dtsGeneratorJar = "dts-generator.jar" + if (!findProject(':dts-generator').is(null)) { + dependsOn ':dts-generator:jar' + dtsGeneratorJar = Paths.get(project(':dts-generator').projectDir.path, + "build/libs/dts-generator.jar").toString() + } - workingDir "$BUILD_TOOLS_PATH" - mainClass = "-jar" + def paramz = new ArrayList() + def includeDirs = ["com.android.support", "/platforms/" + android.compileSdkVersion] + + workingDir "$BUILD_TOOLS_PATH" + mainClass = "-jar" + + // Declared so Gradle can skip the task outright when no jar has changed, + // which is what makes generating typings by default affordable: the common + // build does no work at all, and a classpath change costs one full run. + // + // Keyed on the jars' *contents*, not the list of paths -- a dependency + // upgrade keeps the same path and would otherwise go unnoticed. + inputs.dir(extractedDependenciesDir).withPropertyName("dependencyJars") + inputs.file(android.sdkDirectory.getAbsolutePath() + "/platforms/" + + android.compileSdkVersion + "/android.jar").withPropertyName("androidJar") + if (!findProject(':dts-generator').is(null)) { + // By path, not by task: the other project is not configured yet, so + // tasks.named("jar") would fail during evaluation. + inputs.files(fileTree("${project(':dts-generator').projectDir}/build/libs") { + include "*.jar" + }).withPropertyName("generator") + } + outputs.dir(TYPINGS_PATH).withPropertyName("typings") + + doFirst { + delete "$TYPINGS_PATH" + + paramz.add(dtsGeneratorJar) + paramz.add("-input") + + for (String jarPath : allJarLibraries) { + // don't generate typings for runtime jars and classes + if (shouldIncludeDirForTypings(jarPath, includeDirs)) { + paramz.add(jarPath) + } + } - doFirst { - delete "$TYPINGS_PATH" + paramz.add("-output") + paramz.add("$TYPINGS_PATH") - paramz.add("dts-generator.jar") - paramz.add("-input") + new File("$TYPINGS_PATH").mkdirs() - for (String jarPath : allJarLibraries) { - // don't generate typings for runtime jars and classes - if (shouldIncludeDirForTypings(jarPath, includeDirs)) { - paramz.add(jarPath) - } + logger.info("Task generateTypescriptDefinitions: Call dts-generator.jar with arguments: " + paramz.toString().replaceAll(',', '')) + outLogger.withStyle(Style.SuccessHeader).println "Task generateTypescriptDefinitions: Call dts-generator.jar with arguments: " + paramz.toString().replaceAll(',', '') + + setOutputs outLogger + + args paramz.toArray() + } + } else { + if (!findProject(':dts-generator').is(null)) { + dependsOn ':dts-generator:jar' } - paramz.add("-output") - paramz.add("$TYPINGS_PATH") + def paramz = new ArrayList() + def includeDirs = ["com.android.support", "/platforms/" + android.compileSdkVersion] - new File("$TYPINGS_PATH").mkdirs() + workingDir "$BUILD_TOOLS_PATH" + mainClass = "-jar" - logger.info("Task generateTypescriptDefinitions: Call dts-generator.jar with arguments: " + paramz.toString().replaceAll(',', '')) - outLogger.withStyle(Style.SuccessHeader).println "Task generateTypescriptDefinitions: Call dts-generator.jar with arguments: " + paramz.toString().replaceAll(',', '') + doFirst { + delete "$TYPINGS_PATH" - setOutputs outLogger + paramz.add("dts-generator.jar") + paramz.add("-input") - args paramz.toArray() + for (String jarPath : allJarLibraries) { + // don't generate typings for runtime jars and classes + if (shouldIncludeDirForTypings(jarPath, includeDirs)) { + paramz.add(jarPath) + } + } + + paramz.add("-output") + paramz.add("$TYPINGS_PATH") + + new File("$TYPINGS_PATH").mkdirs() + + logger.info("Task generateTypescriptDefinitions: Call dts-generator.jar with arguments: " + paramz.toString().replaceAll(',', '')) + outLogger.withStyle(Style.SuccessHeader).println "Task generateTypescriptDefinitions: Call dts-generator.jar with arguments: " + paramz.toString().replaceAll(',', '') + + setOutputs outLogger + + args paramz.toArray() + } } + } +// Off by default: the dts-generator walks the whole compile classpath, which is +// slow, and most builds never look at the output. TypeScript users who need the +// typings to call anything native opt in per build with -PgenerateTypings +// (bare, or =true); -PgenerateTypings=false is still an explicit no. generateTypescriptDefinitions.onlyIf { - (project.hasProperty("generateTypings") && Boolean.parseBoolean(project.generateTypings)) || PASSED_TYPINGS_PATH != null + project.hasProperty("generateTypings") && + (project.generateTypings.toString().isEmpty() + || Boolean.parseBoolean(project.generateTypings.toString())) } collectAllJars.finalizedBy(generateTypescriptDefinitions) @@ -1166,20 +1575,17 @@ def bytecodeForced = project.hasProperty("nsBytecodeForce") || project.hasProper // task names — the same heuristic the rest of this file uses. def isReleaseBuild = project.hasProperty("release") || project.gradle.startParameter.taskNames.any { it.toLowerCase().contains("release") } -// The engine name comes from the runtime's gradle.properties (ns_engine). Older -// runtimes don't declare it and have no bytecode compiler at all, so the whole -// feature stays off unless the property is present. -def bytecodeEngine = project.hasProperty("ns_engine") ? ns_engine as String : null -project.ext.bytecodeEnabled = bytecodeEngine != null && (isReleaseBuild || bytecodeForced) && !bytecodeDisabled + +project.ext.bytecodeEnabled = napiEngine != null && (isReleaseBuild || bytecodeForced) && !bytecodeDisabled // Announce the bytecode status once at configuration time so it's obvious in the // build log whether ahead-of-time bytecode will be produced. if (project.ext.bytecodeEnabled) { def buildKind = isReleaseBuild ? "release build" : "debug build (forced via -PnsBytecodeForce)" - outLogger.withStyle(Style.SuccessHeader).println "\t + [bytecode] ENABLED for ${buildKind} (engine: ${bytecodeEngine})" - outLogger.withStyle(Style.Info).println "\t ~ app JS will be compiled to bytecode after asset merge (if ${bytecodeEngine} has a compiler for this host)." + outLogger.withStyle(Style.SuccessHeader).println "\t + [bytecode] ENABLED for ${buildKind} (engine: ${napiEngine})" + outLogger.withStyle(Style.Info).println "\t ~ app JS will be compiled to bytecode after asset merge (if ${napiEngine} has a compiler for this host)." outLogger.withStyle(Style.Info).println "\t ~ disable with -PnsBytecodeDisabled" -} else if (bytecodeEngine != null) { +} else if (napiEngine != null) { def bytecodeReason = bytecodeDisabled ? "disabled via -PnsBytecodeDisabled" : "debug build (release-only feature; force with -PnsBytecodeForce)" outLogger.withStyle(Style.Info).println "\t ~ [bytecode] DISABLED — ${bytecodeReason}; shipping plain JS. isReleaseBuid: ${isReleaseBuild};" @@ -1219,8 +1625,8 @@ task compileBytecode { // fail-the-build behaviour with -PnsBytecodeStrict. def strict = project.hasProperty("nsBytecodeStrict") - outLogger.withStyle(Style.SuccessHeader).println "\t + [bytecode] compiling app JS → ${bytecodeEngine} bytecode" - outLogger.withStyle(Style.Info).println "\t ~ engine: ${bytecodeEngine}" + outLogger.withStyle(Style.SuccessHeader).println "\t + [bytecode] compiling app JS → ${napiEngine} bytecode" + outLogger.withStyle(Style.Info).println "\t ~ engine: ${napiEngine}" outLogger.withStyle(Style.Info).println "\t ~ app assets: ${appDir}" outLogger.withStyle(Style.Info).println "\t ~ driver: ${script}" if (project.hasProperty("bytecodeCompilerBinary")) { @@ -1229,7 +1635,7 @@ task compileBytecode { outLogger.withStyle(Style.Info).println "\t ~ source maps: ${sourceMaps ? "on" : "off"}" outLogger.withStyle(Style.Info).println "\t ~ on error: ${strict ? "fail the build (strict)" : "skip file, keep source"}" - def cmd = [node, script, "--app", appDir, "--engine", bytecodeEngine] + def cmd = [node, script, "--app", appDir, "--engine", napiEngine] if (project.hasProperty("bytecodeCompilerBinary")) { cmd += ["--compiler", bytecodeCompilerBinary as String] } @@ -1300,6 +1706,33 @@ tasks.configureEach({ DefaultTask currentTask -> if (currentTask =~ /merge.*Assets/) { currentTask.dependsOn(buildMetadata) + // Hand the runtime what it cannot infer, by patching the app's + // package.json where the assets were merged -- the app's own source file + // is never touched, and AppConfig already reads this one at startup. + // + // Driven off this task's own outputs rather than + // getMergedAssetsOutputPath(), which follows the pinned + // project.ext.selectedBuildType and so names the release assets even in + // a debug build. + currentTask.doLast { + if (!nsContentKeyedBindings) return + currentTask.outputs.files.files.each { File out -> + if (out.path.contains("${File.separator}incremental${File.separator}")) return + def packageJson = new File(out, "app/package.json") + if (!packageJson.exists()) return + + def json = new JsonSlurper().parseText(packageJson.text) + def androidConfig = json.android ?: [:] + // The static binding generator wrote the class names during this + // same build; the runtime has to look them up the same way. + androidConfig.contentKeyedBindings = true + json.android = androidConfig + packageJson.text = JsonOutput.prettyPrint(JsonOutput.toJson(json)) + + outLogger.withStyle(Style.SuccessHeader).println \ + "\t + [bindings] content-keyed bindings on (generator + runtime)" + } + } // Compile the freshly-merged app JS to engine bytecode right after the // assets are merged (the onlyIf guard makes this a no-op unless it's a // release build with bytecode enabled). Packaging is made to wait for it below. @@ -1333,9 +1766,11 @@ tasks.configureEach({ DefaultTask currentTask -> currentTask.dependsOn(compileBytecode) } - // ensure buildMetadata is done before R8 to allow custom proguard from metadata - if (currentTask =~ /minify(Debug|Release)WithR8/) { - buildMetadata.finalizedBy(currentTask) + // R8 consumes metadata-keep-rules.pro, which buildMetadata writes. Without + // this ordering R8 either reads the previous build's rules or fails on a + // missing file, and either way strips classes the app reaches from JS. + if (currentTask =~ /minify.*WithR8/) { + currentTask.dependsOn(buildMetadata) } if (currentTask =~ /assemble.*Debug/ || currentTask =~ /assemble.*Release/) {