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..463a9d4a0f --- /dev/null +++ b/vendor/gradle-app/app/build.gradle @@ -0,0 +1,1926 @@ +/* +* 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] +* -PabiFilters=[abi][,abi...] //build only these abis +* -PsplitEnabled //one apk per abi +*/ + +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." +} + +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") + ? 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") +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" +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 +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" + +// 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() + 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 if (splitEnabled) { + // the splits block below decides which abi ends up in which apk + } else if (requestedAbis) { + abiFilters.addAll(requestedAbis) + } else { + abiFilters.addAll(DEFAULT_ABIS) + } + } + } + + splits { + abi { + enable splitEnabled + reset() + include(*(requestedAbis ?: DEFAULT_ABIS)) + universalApk false + } + } + + 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"] + } + + 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") && + 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 + + // 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 + } + + } + } + + 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 ///////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////// + +// 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() + 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") + } + 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 + + 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 } + // 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 { + 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) + } +} + +// 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 { + + 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' { + 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") + + 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) + } + + 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") + } + + 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 (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 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() + } + + 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) + } + } + + 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() + } + } else { + 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() + } + } + +} + +// 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") && + (project.generateTypings.toString().isEmpty() + || Boolean.parseBoolean(project.generateTypings.toString())) +} + +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") } + +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: ${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 (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};" +} + +// 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 → ${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")) { + 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", napiEngine] + 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) + // 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. + 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) + } + + // 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/) { + 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"