diff --git a/gradle-plugin/src/main/kotlin/xyz/block/artifactswap/ArtifactSwapBomService.kt b/gradle-plugin/src/main/kotlin/xyz/block/artifactswap/ArtifactSwapBomService.kt index 20e6e507..cb1e6a6f 100644 --- a/gradle-plugin/src/main/kotlin/xyz/block/artifactswap/ArtifactSwapBomService.kt +++ b/gradle-plugin/src/main/kotlin/xyz/block/artifactswap/ArtifactSwapBomService.kt @@ -1,20 +1,19 @@ package xyz.block.artifactswap -import xyz.block.gradle.services.SharedServiceKey -import xyz.block.gradle.services.SharedServices -import xyz.block.artifactswap.ArtifactSwapBomService.Parameters import groovy.xml.XmlSlurper import groovy.xml.slurpersupport.GPathResult +import java.nio.file.Path +import kotlin.io.path.Path +import kotlin.io.path.exists +import kotlin.io.path.inputStream import org.gradle.api.logging.Logger import org.gradle.api.logging.Logging import org.gradle.api.provider.Property import org.gradle.api.services.BuildService import org.gradle.api.services.BuildServiceParameters -import java.nio.file.Path -import kotlin.io.path.Path -import kotlin.io.path.exists -import kotlin.io.path.inputStream - +import xyz.block.artifactswap.ArtifactSwapBomService.Parameters +import xyz.block.gradle.services.SharedServiceKey +import xyz.block.gradle.services.SharedServices // Service to parse local BOM file once per sync abstract class ArtifactSwapBomService : BuildService { @@ -44,8 +43,9 @@ abstract class ArtifactSwapBomService : BuildService { val dependencyManagement = pom.getProperty("dependencyManagement") as GPathResult val dependencies = dependencyManagement.getProperty("dependencies") as GPathResult val dependencySequence = dependencies.children().asSequence().filterIsInstance() - dependencySequence - .associate { it.getProperty("artifactId").toString() to it.getProperty("version").toString() } + dependencySequence.associate { + it.getProperty("artifactId").toString() to it.getProperty("version").toString() + } } else { logger.error("Artifact sync bom does not exist: {}", bomFile) emptyMap() @@ -53,4 +53,5 @@ abstract class ArtifactSwapBomService : BuildService { } } -internal val SharedServices.artifactSyncBomService get() = get(ArtifactSwapBomService.KEY) +internal val SharedServices.artifactSyncBomService + get() = get(ArtifactSwapBomService.KEY) diff --git a/gradle-plugin/src/main/kotlin/xyz/block/artifactswap/ArtifactSwapConfigService.kt b/gradle-plugin/src/main/kotlin/xyz/block/artifactswap/ArtifactSwapConfigService.kt new file mode 100644 index 00000000..75980fdf --- /dev/null +++ b/gradle-plugin/src/main/kotlin/xyz/block/artifactswap/ArtifactSwapConfigService.kt @@ -0,0 +1,28 @@ +package xyz.block.artifactswap + +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.services.BuildService +import org.gradle.api.services.BuildServiceParameters +import xyz.block.gradle.services.SharedServiceKey +import xyz.block.gradle.services.SharedServices +import java.io.File + +/** + * Build service that holds artifact swap publishing configuration. This allows the configuration to + * be set at settings time and accessed in projects in a configuration-cache compatible way. + */ +abstract class ArtifactSwapConfigService : BuildService { + + interface Params : BuildServiceParameters { + val artifactHashFile: RegularFileProperty + val repoUrl: Property + val repoUsername: Property + val repoPassword: Property + } + + internal object KEY : SharedServiceKey("artifactSwapConfig") +} + +internal val SharedServices.artifactSwapConfigService + get() = get(ArtifactSwapConfigService.KEY) diff --git a/gradle-plugin/src/main/kotlin/xyz/block/artifactswap/ArtifactSwapProjectPlugin.kt b/gradle-plugin/src/main/kotlin/xyz/block/artifactswap/ArtifactSwapProjectPlugin.kt index 197eba08..bc14c870 100644 --- a/gradle-plugin/src/main/kotlin/xyz/block/artifactswap/ArtifactSwapProjectPlugin.kt +++ b/gradle-plugin/src/main/kotlin/xyz/block/artifactswap/ArtifactSwapProjectPlugin.kt @@ -2,14 +2,14 @@ package xyz.block.artifactswap -import xyz.block.gradle.generatedProtosVersion -import xyz.block.gradle.protosSchemaVersion -import xyz.block.gradle.services.services import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.artifacts.DependencySubstitution import org.gradle.api.artifacts.component.ModuleComponentSelector import org.jetbrains.kotlin.util.prefixIfNot +import xyz.block.gradle.generatedProtosVersion +import xyz.block.gradle.protosSchemaVersion +import xyz.block.gradle.services.services /** * Artifact Sync project sub-plugin. This plugin is responsible for performing dependency substitution diff --git a/gradle-plugin/src/main/kotlin/xyz/block/artifactswap/ArtifactSwapProjectPublishPlugin.kt b/gradle-plugin/src/main/kotlin/xyz/block/artifactswap/ArtifactSwapProjectPublishPlugin.kt index d3848d33..6e9da539 100644 --- a/gradle-plugin/src/main/kotlin/xyz/block/artifactswap/ArtifactSwapProjectPublishPlugin.kt +++ b/gradle-plugin/src/main/kotlin/xyz/block/artifactswap/ArtifactSwapProjectPublishPlugin.kt @@ -13,34 +13,37 @@ import org.gradle.api.publish.maven.MavenPom import org.gradle.api.publish.maven.MavenPublication import org.gradle.api.publish.maven.tasks.PublishToMavenRepository import org.gradle.api.tasks.bundling.Jar +import xyz.block.gradle.artifactVersion import xyz.block.gradle.isAndroid import xyz.block.gradle.isKotlin -import xyz.block.gradle.sandbagVersion -import xyz.block.gradle.toSandbagArtifact +import xyz.block.gradle.services.services +import xyz.block.gradle.toProjectArtifactName /** - * Artifact Swap project publish plugin for sandbags. This plugin is responsible for configuring - * Maven publishing with sandbag-specific settings when sandbag publishing is enabled. - * - * This plugin extracts the sandbag publishing logic from PublishPlugin and AndroidLibJavaPlugin - * to centralize artifact swap publishing concerns. + * Artifact Swap project publish plugin for project artifacts. This plugin is responsible for + * configuring Maven publishing with artifact-swap-specific settings when artifact publishing is + * enabled. * * For reference and searchability, the ID of this plugin is `xyz.block.artifactswap.publish`. */ @Suppress("unused") class ArtifactSwapProjectPublishPlugin : Plugin { + private lateinit var configService: ArtifactSwapConfigService + override fun apply(target: Project): Unit = target.run { - val version = sandbagVersion ?: return@run + val version = artifactVersion ?: return@run + + configService = gradle.services.artifactSwapConfigService pluginManager.apply("maven-publish") extensions.getByType(PublishingExtension::class.java).also { mavenPublishing -> - val repo = configureSandbagRepository(mavenPublishing) + val repo = configureArtifactRepository(mavenPublishing) - // Other plugins configure the components to be published, so we have to configure them after - // those plugins run + // Other plugins configure the components to be published, so we have to configure them + // after those plugins run afterEvaluate { - val publication = configureSandbagPublication(mavenPublishing, version) + val publication = configureArtifactPublication(mavenPublishing, version) createPublishAliasTask(repo, publication) } } @@ -50,33 +53,35 @@ class ArtifactSwapProjectPublishPlugin : Plugin { } } - private fun Project.configureSandbagRepository( + private fun Project.configureArtifactRepository( mavenPublishing: PublishingExtension, - ): MavenArtifactRepository = with(mavenPublishing) { - val sandbagsUrl = providers.gradleProperty("square.sandbagsUrl").get() - return repositories.maven { repo -> - repo.name = "artifactSwap" - repo.url = uri(sandbagsUrl) - - getSandbagCredentials()?.apply { - repo.credentials(PasswordCredentials::class.java) { creds -> - creds.username = username - creds.password = password + ): MavenArtifactRepository = + with(mavenPublishing) { + val repoUrl = configService.parameters.repoUrl.get() + return repositories.maven { repo -> + repo.name = "artifactSwap" + repo.url = uri(repoUrl) + + val username = configService.parameters.repoUsername.orNull + val password = configService.parameters.repoPassword.orNull + if (username != null && password != null) { + repo.credentials(PasswordCredentials::class.java) { creds -> + creds.username = username + creds.password = password + } } } } - } - private fun Project.configureSandbagPublication( + private fun Project.configureArtifactPublication( mavenPublishing: PublishingExtension, version: String ): MavenPublication { - val publication = mavenPublishing.publications - .maybeCreate("projectArtifact", MavenPublication::class.java) + val publication = mavenPublishing.publications.maybeCreate("projectArtifact", MavenPublication::class.java) - // Automatically configure maven coordinates for sandbag + // Automatically configure maven coordinates for artifact publication.groupId = ARTIFACT_SWAP_MAVEN_GROUP - publication.artifactId = path.toSandbagArtifact + publication.artifactId = path.toProjectArtifactName publication.version = version // For non-Android projects, automatically configure the java component and sources @@ -85,7 +90,7 @@ class ArtifactSwapProjectPublishPlugin : Plugin { addSourcesArtifact(publication) } - configureSandbagPom(publication.pom) + configureArtifactPom(publication.pom) return publication } @@ -113,26 +118,11 @@ class ArtifactSwapProjectPublishPlugin : Plugin { } } - private fun Project.configureSandbagPom(pom: MavenPom) { + private fun Project.configureArtifactPom(pom: MavenPom) { with(pom) { name.set(project.name) - description.set("Sandbag for ${project.name} in build ${project.isolated.rootProject.name}") - url.set(providers.gradleProperty("square.repoUrl")) - scm { scm -> - scm.connection.set(providers.gradleProperty("square.scmConnectionUrl")) - scm.developerConnection.set(providers.gradleProperty("square.scmDeveloperConnectionUrl")) - scm.url.set(providers.gradleProperty("square.repoUrl")) - } - } - } - - private fun Project.getSandbagCredentials(): SandbagCredentials? { - val username = providers.gradleProperty("square.artifactory.username").orNull - val password = providers.gradleProperty("square.artifactory.password").orNull - return if (username != null && password != null) { - SandbagCredentials(username, password) - } else { - null + description.set("Artifact for ${project.name} in build ${project.isolated.rootProject.name}") + url.set(configService.parameters.repoUrl) } } @@ -145,9 +135,4 @@ class ArtifactSwapProjectPublishPlugin : Plugin { it.dependsOn(tasks.named(publishTaskName)) } } - - private data class SandbagCredentials( - val username: String, - val password: String - ) } diff --git a/gradle-plugin/src/main/kotlin/xyz/block/artifactswap/ArtifactSwapSettingsPlugin.kt b/gradle-plugin/src/main/kotlin/xyz/block/artifactswap/ArtifactSwapSettingsPlugin.kt index 117adce8..da6fbef6 100644 --- a/gradle-plugin/src/main/kotlin/xyz/block/artifactswap/ArtifactSwapSettingsPlugin.kt +++ b/gradle-plugin/src/main/kotlin/xyz/block/artifactswap/ArtifactSwapSettingsPlugin.kt @@ -2,20 +2,20 @@ package xyz.block.artifactswap +import java.io.File import org.gradle.api.Plugin import org.gradle.api.initialization.Settings import org.gradle.api.initialization.resolve.DependencyResolutionManagement import org.gradle.api.logging.Logger import org.gradle.api.logging.Logging +import xyz.block.artifactswap.dsl.ArtifactSwapExtension import xyz.block.gradle.LOCAL_PROTOS_ARTIFACTS import xyz.block.gradle.bomVersion -import xyz.block.gradle.isArtifactPublishingEnabled import xyz.block.gradle.services.services import xyz.block.gradle.useArtifactSync import xyz.block.gradle.useLocalProtos import xyz.block.ide.forceSettingsModulesOverride import xyz.block.ide.isIdeSync -import java.io.File /** * Main Artifact Sync settings plugin. This plugin is responsible for: @@ -31,7 +31,11 @@ import java.io.File */ @Suppress("unused") class ArtifactSwapSettingsPlugin : Plugin { + private lateinit var extension: ArtifactSwapExtension + override fun apply(target: Settings) = target.run { + extension = ArtifactSwapExtension.of(target) + applyProjectIncludes() maybeApplyArtifactSync() maybeApplyPublishPlugin() @@ -130,13 +134,20 @@ class ArtifactSwapSettingsPlugin : Plugin { } } - /** - * Applies the publish plugin to all projects when sandbag publishing is enabled. - */ + /** Applies the publish plugin to all projects when artifact publishing is enabled. */ private fun Settings.maybeApplyPublishPlugin() { - if (isArtifactPublishingEnabled) { - gradle.lifecycle.beforeProject { project -> - project.plugins.apply(ArtifactSwapProjectPublishPlugin::class.java) + gradle.settingsEvaluated { + if (extension.publishing.enabled.get()) { + gradle.services.register(ArtifactSwapConfigService.KEY) { spec -> + spec.parameters.artifactHashFile.set(extension.publishing.artifactHashFile) + spec.parameters.repoUrl.set(extension.publishing.repo.url) + spec.parameters.repoUsername.set(extension.publishing.repo.username) + spec.parameters.repoPassword.set(extension.publishing.repo.password) + } + + gradle.lifecycle.beforeProject { project -> + project.plugins.apply(ArtifactSwapProjectPublishPlugin::class.java) + } } } } diff --git a/gradle-plugin/src/main/kotlin/xyz/block/artifactswap/dsl/ArtifactSwapExtension.kt b/gradle-plugin/src/main/kotlin/xyz/block/artifactswap/dsl/ArtifactSwapExtension.kt new file mode 100644 index 00000000..5324ca55 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/xyz/block/artifactswap/dsl/ArtifactSwapExtension.kt @@ -0,0 +1,122 @@ +@file:Suppress("unused") + +package xyz.block.artifactswap.dsl + +import org.gradle.api.Action +import org.gradle.api.artifacts.repositories.PasswordCredentials +import org.gradle.api.file.RegularFile +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.initialization.Settings +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.provider.ProviderFactory +import java.io.File +import javax.inject.Inject + +/** + * Extension for configuring Artifact Swap publishing credentials. + * + * Example usage in settings.gradle: + * ``` + * artifactSwap { + * publishing { + * enabled true + * repo { + * url "https://repo.example.com/maven" + * username "user" + * password "password" + * } + * } + * } + * ``` + */ +abstract class ArtifactSwapExtension +@Inject +constructor(objects: ObjectFactory, providers: ProviderFactory) { + companion object { + const val NAME = "artifactSwap" + + fun of(settings: Settings): ArtifactSwapExtension { + return settings.extensions.create(NAME, ArtifactSwapExtension::class.java) + } + } + + /** Publishing configuration. */ + val publishing: PublishingConfiguration = + objects.newInstance(PublishingConfiguration::class.java, providers) + + fun publishing(action: Action) { + action.execute(publishing) + } + + /** Configuration for publishing artifacts. */ + abstract class PublishingConfiguration + @Inject + constructor(objects: ObjectFactory, providers: ProviderFactory) { + + internal val enabled: Property = objects.property(Boolean::class.java).convention(false) + internal val artifactHashFile: RegularFileProperty = objects.fileProperty() + internal val repo: RepoConfiguration = objects.newInstance(RepoConfiguration::class.java, providers) + + /** Whether artifact publishing is enabled. */ + fun enabled(enabled: Provider) { + this.enabled.set(enabled) + this.enabled.disallowChanges() + } + + /** The artifact hash file containing project versions. */ + fun artifactHashFile(file: Provider) { + this.artifactHashFile.set(file) + this.artifactHashFile.disallowChanges() + } + + /** Repository configuration. */ + fun repo(action: Action) { + action.execute(repo) + } + } + + /** Configuration for repository credentials and URL. */ + abstract class RepoConfiguration + @Inject + constructor(private val objects: ObjectFactory, private val providers: ProviderFactory) { + + internal val url: Property = objects.property(String::class.java) + internal val username: Property = objects.property(String::class.java) + internal val password: Property = objects.property(String::class.java) + + /** The repository URL for publishing artifacts. */ + fun url(url: String) { + this.url.set(url) + this.url.disallowChanges() + } + + /** The repository username for publishing artifacts. */ + fun username(username: String) { + this.username.set(username) + this.username.disallowChanges() + } + + /** The repository password for publishing artifacts. */ + fun password(password: Provider) { + this.password.set(password) + this.password.disallowChanges() + } + + /** Gets the credentials if both username and password are available. */ + internal fun getCredentials(): PasswordCredentials? { + val username = username.orNull + val password = password.orNull?.trim() + + return if (username != null && password != null) { + objects.newInstance(PasswordCredentials::class.java).apply { + this.username = username + this.password = password + } + } else { + null + } + } + } +} diff --git a/gradle-plugin/src/main/kotlin/xyz/block/gradle/Flags.kt b/gradle-plugin/src/main/kotlin/xyz/block/gradle/Flags.kt index 59b7c079..396610dc 100644 --- a/gradle-plugin/src/main/kotlin/xyz/block/gradle/Flags.kt +++ b/gradle-plugin/src/main/kotlin/xyz/block/gradle/Flags.kt @@ -3,14 +3,14 @@ package xyz.block.gradle import org.gradle.api.GradleException import org.gradle.api.Project import org.gradle.api.initialization.Settings +import xyz.block.artifactswap.artifactSwapConfigService +import xyz.block.gradle.services.services import java.io.File private const val SQUARE_GENERATED_PROTOS_VERSION = "square.protosGeneratedVersion" private const val SQUARE_PROTOS_SCHEMA_VERSION = "square.protosSchemaVersion" const val USE_ARTIFACT_SYNC = "useArtifactSync" internal const val LOCAL_PROTOS_ARTIFACTS = "square.useLocalProtos" -private const val IS_SANDBAG_PUBLISHING = "artifactswap.enableSandbagPublishing" -private const val SANDBAG_HASH_FILE = "artifactswap.hashFile" internal val Project.generatedProtosVersion get() = providers.gradleProperty(SQUARE_GENERATED_PROTOS_VERSION).get() @@ -36,31 +36,22 @@ internal val Settings.useLocalProtos: Boolean get() = providers.gradleProperty(LOCAL_PROTOS_ARTIFACTS) .getOrElse("false").toBoolean() -val Project.isSandbagPublishingEnabled: Boolean - get() = providers.gradleProperty(IS_SANDBAG_PUBLISHING) - .getOrElse("false").toBoolean() - -val Settings.isArtifactPublishingEnabled: Boolean - get() = providers.gradleProperty(IS_SANDBAG_PUBLISHING) - .getOrElse("false").toBoolean() - -val Project.sandbagHashFile: File - get() = File(rootDir, providers.gradleProperty(SANDBAG_HASH_FILE).get()) +val Project.artifactHashFile: File + get() = gradle.services.artifactSwapConfigService.parameters.artifactHashFile.get().asFile -/** - * Gets the sandbag version for this project from the sandbag hash file. - */ -val Project.sandbagVersion: String? +/** Gets the sandbag version for this project from the sandbag hash file. */ +val Project.artifactVersion: String? get() { - if (!sandbagHashFile.exists()) { + if (!artifactHashFile.exists()) { throw GradleException( - "Sandbag hash file was not found in $sandbagHashFile. Please run sandbag tool." + "Artifact hash file was not found in $artifactHashFile. Please run artifact-swap tool." ) } - val result = sandbagHashFile.useLines { lines -> - return@useLines lines.firstOrNull { line -> - return@firstOrNull line.substringBefore('|') == project.path + val result = + artifactHashFile.useLines { lines -> + return@useLines lines.firstOrNull { line -> + return@firstOrNull line.substringBefore('|') == project.path + } } - } return result?.substringAfter('|') } diff --git a/gradle-plugin/src/main/kotlin/xyz/block/gradle/ProjectExtensions.kt b/gradle-plugin/src/main/kotlin/xyz/block/gradle/ProjectExtensions.kt index e00c1b5c..2e0583f5 100644 --- a/gradle-plugin/src/main/kotlin/xyz/block/gradle/ProjectExtensions.kt +++ b/gradle-plugin/src/main/kotlin/xyz/block/gradle/ProjectExtensions.kt @@ -19,5 +19,5 @@ val Project.isKotlin: Boolean * Converts a project path to a sandbag artifact name. * Example: ":hobbits:frodo" -> "hobbits_frodo" */ -val String.toSandbagArtifact: String +val String.toProjectArtifactName: String get() = removePrefix(":").replace(":", "_") diff --git a/gradle-plugin/src/main/kotlin/xyz/block/ide/IdeProperties.kt b/gradle-plugin/src/main/kotlin/xyz/block/ide/IdeProperties.kt index 122187b0..a1783fc4 100644 --- a/gradle-plugin/src/main/kotlin/xyz/block/ide/IdeProperties.kt +++ b/gradle-plugin/src/main/kotlin/xyz/block/ide/IdeProperties.kt @@ -12,6 +12,5 @@ internal val Project.isIdeSync: Boolean get() = internal val Settings.isIdeSync: Boolean get() = providers.systemProperty(INTELLIJ_SYNC_ACTIVE_SYSTEM_PROPERTY).orNull != null - internal val Settings.forceSettingsModulesOverride: Boolean get() = providers.gradleProperty(SQUARE_SETTINGS_OVERRIDE_FORCE_PROPERTY).orNull != null