From 973cbfc5747f3b9dce1fc4ef03f3560f0616ea73 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Tue, 22 Sep 2026 19:35:05 -0300 Subject: [PATCH 1/2] build: drop Gradle syntax deprecated for 9.0/10.0 - Assign DSL properties with `prop = value` instead of the Groovy space-assignment form (namespace, ndkVersion, cmake version/path, signingConfig, description, standardOutput/errorOutput, ignoreExitValue). - Replace the gradle.useLogger() failure logger with a BuildService listening for task completion events; it prints the same styled failure summary at the end of the build and still skips failures of BuildToolTask tasks, which log their own errors. - Run the SBG AST test builds with `-p` instead of `-b`: the static-binding-generator settings.gradle now selects runtests.gradle as its build file, and runtests.gradle uses mainClass instead of main. --- build.gradle | 2 +- test-app/app/build.gradle | 8 +- .../app/gradle-helpers/BuildToolTask.gradle | 4 +- .../CustomExecutionLogger.gradle | 80 ++++++++++--------- .../tests/specs/ast-parser-tests.spec.js | 4 +- .../static-binding-generator/runtests.gradle | 2 +- .../static-binding-generator/settings.gradle | 4 +- test-app/runtime/build.gradle | 12 +-- 8 files changed, 63 insertions(+), 53 deletions(-) diff --git a/build.gradle b/build.gradle index cffa741df..71f536ec2 100644 --- a/build.gradle +++ b/build.gradle @@ -427,7 +427,7 @@ createNpmPackage.dependsOn(copyReadme) task createPackage { println "CCache version: " + getCCacheVersion() - description "Builds the NativeScript Android cleanBuildArtefactsApp Package using an application project template." + description = "Builds the NativeScript Android cleanBuildArtefactsApp Package using an application project template." dependsOn createNpmPackage println "Creating NativeScript Android Package" } diff --git a/test-app/app/build.gradle b/test-app/app/build.gradle index da7d2c5f1..9fbc263bf 100644 --- a/test-app/app/build.gradle +++ b/test-app/app/build.gradle @@ -217,7 +217,7 @@ def computeNamespace = { -> } android { - namespace computeNamespace() + namespace = computeNamespace() applyBeforePluginGradleConfiguration() @@ -271,7 +271,7 @@ android { } buildTypes { release { - signingConfig signingConfigs.release + signingConfig = signingConfigs.release } } @@ -703,7 +703,7 @@ def resolveCompileSdkPlatformName(File sdkDirectory, String compileSdkVersion) { task 'collectAllJars' { dependsOn extractAllJars - description "gathers all paths to jar dependencies before building metadata with them" + description = "gathers all paths to jar dependencies before building metadata with them" def sdkPath = android.sdkDirectory.getAbsolutePath() @@ -964,7 +964,7 @@ task buildMetadata(type: BuildToolTask) { //buildMetadata.finalizedBy(copyMetadata) finalizedBy copyMetadata - description "builds metadata with provided jar dependencies" + description = "builds metadata with provided jar dependencies" inputs.files("$MDG_JAVA_DEPENDENCIES") diff --git a/test-app/app/gradle-helpers/BuildToolTask.gradle b/test-app/app/gradle-helpers/BuildToolTask.gradle index 7b6052f4c..c744f570b 100644 --- a/test-app/app/gradle-helpers/BuildToolTask.gradle +++ b/test-app/app/gradle-helpers/BuildToolTask.gradle @@ -6,8 +6,8 @@ class BuildToolTask extends JavaExec { if(logFile.exists()) { logFile.delete() } - standardOutput new FileOutputStream(logFile) - errorOutput new FailureOutputStream(logger, logFile) + standardOutput = new FileOutputStream(logFile) + errorOutput = new FailureOutputStream(logger, logFile) } } diff --git a/test-app/app/gradle-helpers/CustomExecutionLogger.gradle b/test-app/app/gradle-helpers/CustomExecutionLogger.gradle index ec8ea6c42..0367b870c 100644 --- a/test-app/app/gradle-helpers/CustomExecutionLogger.gradle +++ b/test-app/app/gradle-helpers/CustomExecutionLogger.gradle @@ -1,52 +1,60 @@ +import org.gradle.api.services.BuildService +import org.gradle.api.services.BuildServiceParameters +import org.gradle.build.event.BuildEventsListenerRegistry +import org.gradle.internal.logging.text.StyledTextOutput import org.gradle.internal.logging.text.StyledTextOutputFactory +import org.gradle.tooling.events.FinishEvent +import org.gradle.tooling.events.OperationCompletionListener +import org.gradle.tooling.events.task.TaskFailureResult +import org.gradle.tooling.events.task.TaskFinishEvent 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 - } +abstract class CustomExecutionLogger implements BuildService, OperationCompletionListener, AutoCloseable { + // BuildToolTask already logs its own errors; its failures are not repeated here. + final Set selfReportingTaskPaths = Collections.synchronizedSet(new HashSet()) + private final List failures = Collections.synchronizedList(new ArrayList()) - void buildStarted(Gradle gradle) { - failedTask = null - } + // StyledTextOutputFactory cannot be injected into a BuildService, so the output is handed over from the script. + StyledTextOutput output - void beforeExecute(Task task) { - } - - void afterExecute(Task task, TaskState state) { - def failure = state.getFailure() - if(failure) { - failedTask = task + @Override + void onFinish(FinishEvent event) { + if (event instanceof TaskFinishEvent && event.result instanceof TaskFailureResult) { + if (!selfReportingTaskPaths.contains(event.descriptor.taskPath)) { + failures.addAll(event.result.failures) + } } } - 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() + // Build services are closed once all tasks have finished, which makes close() the end-of-build hook. + @Override + void close() { + if (failures.isEmpty() || output == null) { + return + } + failures.each { failure -> + output.println() + output.withStyle(Style.FailureHeader).println failure.message - def causeException = failure.getCause() - while (causeException != null) { - failure = causeException - causeException = failure.getCause() + def rootCause = failure + while (!rootCause.causes.isEmpty()) { + rootCause = rootCause.causes[0] } - if(failure != causeException) { - logger.withStyle(Style.Failure).println failure.getMessage() + if (rootCause != failure) { + output.withStyle(Style.Failure).println rootCause.message } - println "" + output.println() } } } -gradle.useLogger(new CustomExecutionLogger(outLogger)) \ No newline at end of file +def customExecutionLogger = gradle.sharedServices.registerIfAbsent("nsCustomExecutionLogger", CustomExecutionLogger) {} +services.get(BuildEventsListenerRegistry).onTaskCompletion(customExecutionLogger) + +def outLogger = services.get(StyledTextOutputFactory).create("colouredOutputLogger") +gradle.taskGraph.whenReady { graph -> + customExecutionLogger.get().output = outLogger + def selfReporting = graph.allTasks.findAll { it.getClass().getName().contains("BuildToolTask") }*.path + customExecutionLogger.get().selfReportingTaskPaths.addAll(selfReporting) +} diff --git a/test-app/build-tools/jsparser/tests/specs/ast-parser-tests.spec.js b/test-app/build-tools/jsparser/tests/specs/ast-parser-tests.spec.js index b8030989c..a05c6b4fe 100644 --- a/test-app/build-tools/jsparser/tests/specs/ast-parser-tests.spec.js +++ b/test-app/build-tools/jsparser/tests/specs/ast-parser-tests.spec.js @@ -3,11 +3,11 @@ const exec = require("child_process").exec, fs = require("fs"), prefix = path.resolve(__dirname, "../cases/"), sbgBindingOutoutFile = path.resolve(__dirname, "../../../sbg-bindings.txt"), - testsGradleFile = path.resolve(__dirname, "../../../static-binding-generator/runtests.gradle"), + sbgProjectDir = path.resolve(__dirname, "../../../static-binding-generator"), gradleExecutable = path.resolve(__dirname, "../../../../../gradlew"); function execGradle(inputPath, generatedJavaClassesRoot, callback) { - const command = `${gradleExecutable} -b ${testsGradleFile} -PappRoot=${inputPath} -PgeneratedJavaClassesRoot=${generatedJavaClassesRoot}`; + const command = `${gradleExecutable} -p ${sbgProjectDir} -PappRoot=${inputPath} -PgeneratedJavaClassesRoot=${generatedJavaClassesRoot}`; const options = { cwd: path.dirname(gradleExecutable) }; diff --git a/test-app/build-tools/static-binding-generator/runtests.gradle b/test-app/build-tools/static-binding-generator/runtests.gradle index 321ae9f68..d46dd2141 100644 --- a/test-app/build-tools/static-binding-generator/runtests.gradle +++ b/test-app/build-tools/static-binding-generator/runtests.gradle @@ -76,7 +76,7 @@ task prepareInputFiles { task runSbg(type: JavaExec, dependsOn: 'prepareInputFiles') { classpath = files('build/libs/static-binding-generator.jar', '../') workingDir = "../" - main = "org.nativescript.staticbindinggenerator.Main" + mainClass = "org.nativescript.staticbindinggenerator.Main" } java { sourceCompatibility = JavaVersion.VERSION_17 diff --git a/test-app/build-tools/static-binding-generator/settings.gradle b/test-app/build-tools/static-binding-generator/settings.gradle index 48f57764a..6134158c2 100644 --- a/test-app/build-tools/static-binding-generator/settings.gradle +++ b/test-app/build-tools/static-binding-generator/settings.gradle @@ -1 +1,3 @@ -// empty file to avoid using settings.gradle file from test-app \ No newline at end of file +// Only read when this directory is built standalone (`gradlew -p`, as the jsparser tests do); +// as a test-app subproject, test-app's settings and build.gradle apply instead. +rootProject.buildFileName = 'runtests.gradle' diff --git a/test-app/runtime/build.gradle b/test-app/runtime/build.gradle index 57a5173d1..4add7d915 100644 --- a/test-app/runtime/build.gradle +++ b/test-app/runtime/build.gradle @@ -80,7 +80,7 @@ base { } android { - namespace "com.tns.android_runtime" + namespace = "com.tns.android_runtime" compileSdk NS_DEFAULT_COMPILE_SDK_VERSION as int buildToolsVersion = NS_DEFAULT_BUILD_TOOLS_VERSION as String @@ -96,9 +96,9 @@ android { } if (hasNdkVersion) { - ndkVersion project.ndkVersion + ndkVersion = project.ndkVersion } else { - ndkVersion defaultNdkVersion + ndkVersion = defaultNdkVersion } defaultConfig { @@ -152,8 +152,8 @@ android { } externalNativeBuild { cmake { - version "3.31.6" - path "CMakeLists.txt" + version = "3.31.6" + path = "CMakeLists.txt" } } @@ -320,7 +320,7 @@ def createPackageConfigFileTask(taskName) { def removeCmdParams = new ArrayList([aaptCommand, "remove", pathToAAR, "config.json"]) exec { - ignoreExitValue true + ignoreExitValue = true workingDir "$projectDir/src/main" commandLine removeCmdParams.toArray() } From 31c2931dc7cc23b2647a89d63f23e41fcad36b5d Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Tue, 22 Sep 2026 19:36:08 -0300 Subject: [PATCH 2/2] build: AGP 8.13.2 with R8 9.1.43 so D8 can read Kotlin 2.4 metadata The Kotlin version moved to 2.4.10 while AGP stayed on 8.12.1, whose bundled R8 (8.12.14) predates Kotlin 2.4 metadata. Every dex step then logs "An error occurred when parsing kotlin metadata" once per Kotlin stdlib class, about a thousand lines per build. Kotlin 2.4 needs R8 9.1.29 or newer, which no 8.x AGP bundles, so R8 is pinned on the buildscript classpath the way the AGP/Kotlin compatibility docs describe, and AGP moves to the last 8.x release. Both are overridable per project like the existing versions. --- test-app/build.gradle | 3 +++ test-app/gradle.properties | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/test-app/build.gradle b/test-app/build.gradle index 99a1643b2..9799ab860 100644 --- a/test-app/build.gradle +++ b/test-app/build.gradle @@ -134,8 +134,10 @@ version of the {N} CLI install a previous version of the runtime package - 'tns def computeKotlinVersion = { -> project.hasProperty("kotlinVersion") ? kotlinVersion : "${ns_default_kotlin_version}" } def computeBuildToolsVersion = { -> project.hasProperty("androidBuildToolsVersion") ? androidBuildToolsVersion : "${NS_DEFAULT_ANDROID_BUILD_TOOLS_VERSION}" } + def computeR8Version = { -> project.hasProperty("r8Version") ? r8Version : "${NS_DEFAULT_R8_VERSION}" } def kotlinVersion = computeKotlinVersion() def androidBuildToolsVersion = computeBuildToolsVersion() + def r8Version = computeR8Version() repositories { google() @@ -143,6 +145,7 @@ version of the {N} CLI install a previous version of the runtime package - 'tns } dependencies { classpath "com.android.tools.build:gradle:$androidBuildToolsVersion" + classpath "com.android.tools:r8:$r8Version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "org.apache.groovy:groovy-all:4.0.21" } diff --git a/test-app/gradle.properties b/test-app/gradle.properties index d5a310dc5..e58b0da6b 100644 --- a/test-app/gradle.properties +++ b/test-app/gradle.properties @@ -22,7 +22,9 @@ android.useAndroidX=true 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_ANDROID_BUILD_TOOLS_VERSION=8.13.2 +# R8 8.x cannot read Kotlin 2.4 metadata (needs 9.1.29+), so the AGP-bundled R8 is overridden. +NS_DEFAULT_R8_VERSION=9.1.43 ns_default_androidx_appcompat_version = 1.7.0 ns_default_androidx_exifinterface_version = 1.3.7