diff --git a/addon/addon-build.gradle.kts b/addon/addon-build.gradle.kts index 55be681..5f4652b 100644 --- a/addon/addon-build.gradle.kts +++ b/addon/addon-build.gradle.kts @@ -2,6 +2,9 @@ // © 2024-present Godot Mobile Plugins (https://github.com/godot-mobile-plugins) // +import org.gradle.api.file.FileSystemOperations +import javax.inject.Inject + plugins { id("base-conventions") } @@ -16,6 +19,11 @@ val pluginConfig = loadPluginConfig() val iosConfig = loadIosConfig() val godotConfig = loadGodotConfig() +interface Injected { + @get:Inject + val fsOps: FileSystemOperations +} + // -- Collect all catalog library aliases (used in @androidDependencies@ token) - val androidDependencies = @@ -79,6 +87,8 @@ fun TaskContainerScope.registerGdscriptFormatTask( val sharedGdformatrcDest = sharedSrcDir.resolve(".gdformatrc") val excludePatterns = listOf("**/*Plugin.gd", "**/MediationNetwork.gd") + val fsOps = project.objects.newInstance().fsOps + register(name) { this.description = description this.group = if (check) "verification" else "formatting" @@ -86,12 +96,12 @@ fun TaskContainerScope.registerGdscriptFormatTask( workingDir = addonSrcDir doFirst { - copy { + fsOps.copy { from(gdformatrcSource) into(addonSrcDir) } if (sharedSrcDir.exists()) { - copy { + fsOps.copy { from(gdformatrcSource) into(sharedSrcDir) } diff --git a/android/android-build.gradle.kts b/android/android-build.gradle.kts index ac7a994..6e920ca 100644 --- a/android/android-build.gradle.kts +++ b/android/android-build.gradle.kts @@ -13,7 +13,6 @@ import javax.xml.parsers.DocumentBuilderFactory plugins { id("base-conventions") alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) alias(libs.plugins.undercouch.download) alias(libs.plugins.openrewrite) alias(libs.plugins.node) @@ -242,6 +241,10 @@ configure { // -- Android configuration ----------------------------------------------------- +base { + archivesName.set(pluginConfig.pluginName) +} + android { namespace = pluginConfig.pluginPackageName compileSdk = @@ -275,13 +278,6 @@ android { } } - libraryVariants.all { - outputs.all { - (this as LibraryVariantOutputImpl).outputFileName = - "${pluginConfig.pluginName}-$name.aar" - } - } - buildTypes { // enableUnitTestCoverage instruments testDebugUnitTest with the JaCoCo // agent and wires up the createDebugUnitTestCoverageReport task, which @@ -655,11 +651,9 @@ tasks { dependsOn("testDebugUnitTest") dependsOn("createDebugUnitTestCoverageReport") + val buildDirProvider = project.layout.buildDirectory // Capture at configuration time doLast { - val buildDir = - project.layout.buildDirectory - .get() - .asFile + val buildDir = buildDirProvider.get().asFile val resultsDir = File(buildDir, "test-results/testDebugUnitTest") val coverageXml = File(buildDir, "reports/coverage/test/debug/report.xml") val coverageDir = coverageXml.parentFile diff --git a/common/build-logic/logic.gradle.kts b/common/build-logic/logic.gradle.kts index 42d9d42..9b309ec 100644 --- a/common/build-logic/logic.gradle.kts +++ b/common/build-logic/logic.gradle.kts @@ -15,9 +15,10 @@ plugins { `kotlin-dsl` `java-gradle-plugin` - alias(libs.plugins.kotlin.serialization) + kotlin("plugin.serialization") version embeddedKotlinVersion } + java { toolchain { languageVersion.set(JavaLanguageVersion.of(17)) diff --git a/common/build.gradle.kts b/common/build.gradle.kts index f100486..ca0b6f1 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -5,7 +5,6 @@ plugins { id("base-conventions") alias(libs.plugins.android.library) apply false - alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.undercouch.download) apply false alias(libs.plugins.openrewrite) apply false alias(libs.plugins.node) apply false @@ -216,13 +215,16 @@ tasks { workingDir = file(repositoryRootDir) + val providerFactory = providers // Capture at configuration time doFirst { val shellcheckAvailable = - project + providerFactory .exec { commandLine("which", "shellcheck") isIgnoreExitValue = true - }.exitValue == 0 + }.result + .get() + .exitValue == 0 if (!shellcheckAvailable) { throw GradleException( "shellcheck is not installed or not on PATH.\n" + @@ -249,13 +251,16 @@ tasks { workingDir = file(repositoryRootDir) + val providerFactory = providers // Capture at configuration time doFirst { val shellcheckAvailable = - project + providerFactory .exec { commandLine("which", "shellcheck") isIgnoreExitValue = true - }.exitValue == 0 + }.result + .get() + .exitValue == 0 if (!shellcheckAvailable) { throw GradleException( "shellcheck is not installed or not on PATH.\n" + @@ -277,63 +282,61 @@ tasks { .map { it.absolutePath } .sorted() - fun checkRubocop(project: Project) { + fun checkRubocop(providers: ProviderFactory) { val rubocopAvailable = - project + providers .exec { commandLine("which", "rubocop") isIgnoreExitValue = true - }.exitValue == 0 + }.result + .get() + .exitValue == 0 if (!rubocopAvailable) { throw GradleException("rubocop is not installed or not on PATH.") } } - register("checkRubyScriptFormat") { + register("checkRubyScriptFormat") { description = "Checks Rubocop compliance of all Ruby scripts under script/" group = "verification" + workingDir = file(repositoryRootDir) - doLast { - checkRubocop(project) + val providerFactory = providers // Capture at configuration time + doFirst { + checkRubocop(providerFactory) if (rubySourceFiles.isEmpty()) { throw GradleException("checkRubyScriptFormat: no *.rb files found.") } - - project.exec { - workingDir = file(repositoryRootDir) - commandLine( - listOf( - "rubocop", - "--config", - "$repositoryRootDir/.github/config/.rubocop.yml", - ) + rubySourceFiles, - ) - } + commandLine( + listOf( + "rubocop", + "--config", + "$repositoryRootDir/.github/config/.rubocop.yml", + ) + rubySourceFiles, + ) } } - register("applyRubyScriptFormat") { + register("applyRubyScriptFormat") { description = "Applies Rubocop suggested fixes to Ruby scripts under script/" group = "formatting" + workingDir = file(repositoryRootDir) + isIgnoreExitValue = true - doLast { - checkRubocop(project) + val providerFactory = providers // Capture at configuration time + doFirst { + checkRubocop(providerFactory) if (rubySourceFiles.isEmpty()) { throw GradleException("applyRubyScriptFormat: no *.rb files found.") } - - project.exec { - workingDir = file(repositoryRootDir) - isIgnoreExitValue = true - commandLine( - listOf( - "rubocop", - "--config", - "$repositoryRootDir/.github/config/.rubocop.yml", - "--autocorrect", - ) + rubySourceFiles, - ) - } + commandLine( + listOf( + "rubocop", + "--config", + "$repositoryRootDir/.github/config/.rubocop.yml", + "--autocorrect", + ) + rubySourceFiles, + ) } } @@ -350,98 +353,106 @@ tasks { ) }.files.map { it.absolutePath }.sorted() - fun checkYamllint(project: Project) { + fun checkYamllint(providers: ProviderFactory) { val yamllintAvailable = - project + providers .exec { commandLine("which", "yamllint") isIgnoreExitValue = true - }.exitValue == 0 + }.result + .get() + .exitValue == 0 if (!yamllintAvailable) { throw GradleException("yamllint is not installed or not on PATH.") } } - fun checkYamlfix(project: Project) { + fun checkYamlfix(providers: ProviderFactory) { val yamlfixAvailable = - project + providers .exec { commandLine("which", "yamlfix") isIgnoreExitValue = true - }.exitValue == 0 + }.result + .get() + .exitValue == 0 if (!yamlfixAvailable) { throw GradleException("yamlfix is not installed or not on PATH.") } } - fun checkActionlint(project: Project) { + fun checkActionlint(providers: ProviderFactory) { val actionlintAvailable = - project + providers .exec { commandLine("which", "actionlint") isIgnoreExitValue = true - }.exitValue == 0 + }.result + .get() + .exitValue == 0 if (!actionlintAvailable) { throw GradleException("actionlint is not installed or not on PATH.") } } - register("checkYaml") { - description = "Checks Yamllint (style) and Actionlint (syntax) compliance of YAML files" + register("checkYamllint") { + description = "Checks Yamllint (style) compliance of YAML files" group = "verification" + workingDir = file(repositoryRootDir) - doLast { - checkYamllint(project) + val providerFactory = providers // Capture at configuration time + doFirst { + checkYamllint(providerFactory) if (yamlSourceFiles.isEmpty()) { - throw GradleException("checkYaml: no *.yml or *.yaml files found.") - } - - // 1. Run yamllint to enforce the .yamllint.yml rules - project.exec { - workingDir = file(repositoryRootDir) - commandLine( - listOf( - "yamllint", - "--config-file", - "$repositoryRootDir/.github/config/.yamllint.yml", - ) + yamlSourceFiles, - ) + throw GradleException("checkYamllint: no *.yml or *.yaml files found.") } + commandLine( + listOf( + "yamllint", + "--config-file", + "$repositoryRootDir/.github/config/.yamllint.yml", + ) + yamlSourceFiles, + ) + } + } - checkActionlint(project) + register("checkActionlint") { + description = "Checks Actionlint (syntax) compliance of YAML files" + group = "verification" + workingDir = file(repositoryRootDir) - // 2. Run actionlint to match the github action workflow behavior - project.exec { - workingDir = file(repositoryRootDir) - // actionlint automatically detects files in .github/workflows - commandLine("actionlint") - } + val providerFactory = providers // Capture at configuration time + doFirst { + checkActionlint(providerFactory) + commandLine("actionlint") } } - register("formatYaml") { + register("checkYaml") { + description = "Checks Yamllint (style) and Actionlint (syntax) compliance of YAML files" + group = "verification" + dependsOn("checkYamllint", "checkActionlint") + } + + register("formatYaml") { description = "Formats YAML files in-place using yamlfix" group = "formatting" + workingDir = file(repositoryRootDir) + isIgnoreExitValue = true - doLast { - checkYamlfix(project) + val providerFactory = providers // Capture at configuration time + doFirst { + checkYamlfix(providerFactory) if (yamlSourceFiles.isEmpty()) { throw GradleException("formatYaml: no *.yml or *.yaml files found.") } - - project.exec { - workingDir = file(repositoryRootDir) - // Ignore exit value since yamlfix may return non-zero if files were modified - isIgnoreExitValue = true - - commandLine( - listOf( - "yamlfix", - "--config-file", - "$repositoryRootDir/.github/config/.yamlfix.toml", - ) + yamlSourceFiles, - ) - } + commandLine( + listOf( + "yamlfix", + "--config-file", + "$repositoryRootDir/.github/config/.yamlfix.toml", + ) + yamlSourceFiles, + ) } } diff --git a/common/gradle/libs.versions.toml b/common/gradle/libs.versions.toml index c458980..c11de9a 100644 --- a/common/gradle/libs.versions.toml +++ b/common/gradle/libs.versions.toml @@ -5,13 +5,12 @@ [versions] # -- Android ------------------------------------------------------------------- -compileSdk = "35" +compileSdk = "36" minSdk = "24" -buildTools = "35.0.0" -android-lib-plugin = "8.6.1" +buildTools = "36.0.0" +android-gradle-plugin = "9.2.0" # -- Build tool dependencies --------------------------------------------------- -kotlin-android-plugin = "2.0.21" undercouch-download-plugin = "5.6.0" checkstyle = "12.3.1" openrewrite = "6.27.1" @@ -52,9 +51,7 @@ runtime-androidx-appcompat = { module = "androidx.appcompat:appcompat", version. [plugins] -android-library = { id = "com.android.library", version.ref = "android-lib-plugin" } -kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin-android-plugin" } +android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" } undercouch-download = { id = "de.undercouch.download", version.ref = "undercouch-download-plugin" } openrewrite = { id = "org.openrewrite.rewrite", version.ref = "openrewrite" } node = { id = "com.github.node-gradle.node", version.ref = "node-plugin" } -kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin-android-plugin" } diff --git a/common/gradle/wrapper/gradle-wrapper.properties b/common/gradle/wrapper/gradle-wrapper.properties index befd4e1..c80926a 100644 --- a/common/gradle/wrapper/gradle-wrapper.properties +++ b/common/gradle/wrapper/gradle-wrapper.properties @@ -3,7 +3,7 @@ # distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/demo/project.godot b/demo/project.godot index 5b086a4..d67c03b 100644 --- a/demo/project.godot +++ b/demo/project.godot @@ -15,6 +15,7 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true [application] config/name="Plugin Template Demo" +config/tags=PackedStringArray("android", "ios", "plugin", "template") run/main_scene="uid://daro5x6n3pd26" config/features=PackedStringArray("4.7", "GL Compatibility") config/icon="res://assets/plugin-template-android.png" diff --git a/ios/ios-build.gradle.kts b/ios/ios-build.gradle.kts index edc9f8a..af08bcf 100644 --- a/ios/ios-build.gradle.kts +++ b/ios/ios-build.gradle.kts @@ -2,9 +2,12 @@ // © 2026-present Godot Mobile Plugins (https://github.com/godot-mobile-plugins) // +import org.gradle.api.file.ArchiveOperations +import org.gradle.api.file.FileSystemOperations import org.gradle.process.ExecOperations import java.time.LocalDateTime import java.time.format.DateTimeFormatter +import javax.inject.Inject plugins { id("base-conventions") @@ -28,6 +31,12 @@ val iosConfig = loadIosConfig() interface Injected { @get:Inject val execOps: ExecOperations + + @get:Inject + val fsOps: FileSystemOperations + + @get:Inject + val archiveOps: ArchiveOperations } val derivedDataDir = file("$projectDir/build/DerivedData") @@ -317,6 +326,7 @@ fun TaskContainerScope.registerIosBuildTask( val libDir = file("$projectDir/build/lib") val frameworkDir = file("$projectDir/build/framework") + val execOps = project.objects.newInstance().execOps register(name) { this.description = description group = "build" @@ -398,9 +408,6 @@ fun TaskContainerScope.registerIosBuildTask( ) } - // Inject ExecOperations using your predefined Injected interface - val execOps = project.objects.newInstance().execOps - // -- Dynamically strip SPM dependency objects from the static library -- // // xcodebuild archives every compiled SPM package object into the .a via @@ -535,6 +542,8 @@ fun TaskContainerScope.registerIosTestTask( workspace.exists() } + val extraProps = project.extra + doFirst { // Delete the existing result bundle to avoid xcodebuild errors val resultBundle = testResultsDir.resolve("$name.xcresult") @@ -546,8 +555,8 @@ fun TaskContainerScope.registerIosTestTask( // Try pulling from Gradle extra properties first (set by bootiOSSimulator), then fallback to env val simulatorUdid = - if (project.extra.has("SIMULATOR_UDID")) { - project.extra["SIMULATOR_UDID"] as String + if (extraProps.has("SIMULATOR_UDID")) { + extraProps["SIMULATOR_UDID"] as String } else { System.getenv("SIMULATOR_UDID")?.takeIf { it.isNotBlank() } } @@ -761,11 +770,14 @@ tasks { dest(archiveFile) overwrite(false) + val fsOps = objects.newInstance().fsOps + val archiveOps = objects.newInstance().archiveOps + doLast { godotDirectory.mkdirs() - project.copy { - from(project.zipTree(archiveFile)) + fsOps.copy { + from(archiveOps.zipTree(archiveFile)) includeEmptyDirs = false into(godotDirectory) } @@ -809,9 +821,12 @@ tasks { dest(archiveFile) overwrite(false) + val fsOps = objects.newInstance().fsOps + val archiveOps = objects.newInstance().archiveOps + doLast { - project.copy { - from(project.zipTree(archiveFile)) + fsOps.copy { + from(archiveOps.zipTree(archiveFile)) includeEmptyDirs = false into(godotDirectory) // Strip top-level wrapper directory so the result is always @@ -1434,10 +1449,10 @@ tasks { group = "clean" description = "Cleans iOS build outputs" - val iosBuildDir = provider { project.file("$projectDir/build") } + val iosBuildDir = layout.projectDirectory.dir("build") doFirst { - val dir = iosBuildDir.get() + val dir = iosBuildDir.asFile if (dir.exists()) { logger.lifecycle("Removing iOS build directory: ${dir.absolutePath}") } else { @@ -1486,6 +1501,7 @@ tasks { group = "setup" val execOps = objects.newInstance().execOps + val extraProps = project.extra doLast { val destName = iosConfig.testDestinationName @@ -1546,7 +1562,7 @@ tasks { logger.lifecycle("SpringBoard ready. UUD: {}", udid) // Save UDID for the testiOS task to consume - project.extra.set("SIMULATOR_UDID", udid) + extraProps.set("SIMULATOR_UDID", udid) } }