Skip to content

Commit f4d0bd9

Browse files
committed
Merge branch 'main' into nmcp
2 parents 3cc5ad7 + 6f40db6 commit f4d0bd9

9 files changed

Lines changed: 860 additions & 1 deletion

File tree

librarian-gradle-plugin/api/librarian-gradle-plugin.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,10 @@ public final class com/gradleup/librarian/gradle/internal/Publishing_androidKt {
149149
public static final fun createAndroidPublication (Lorg/gradle/api/Project;Ljava/lang/String;)V
150150
}
151151

152+
public abstract interface class com/gradleup/librarian/gradle/internal/task/Content {
153+
public abstract fun writeTo (Lokio/BufferedSink;)V
154+
}
155+
152156
public final class com/gradleup/librarian/gradle/internal/task/GenerateStaticContentKt {
153157
public static final fun generateStaticContentTask (Ljava/util/List;Ljava/util/List;Ljava/io/File;)V
154158
}

librarian-gradle-plugin/src/main/kotlin/com/gradleup/librarian/gradle/internal/publishing.android.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package com.gradleup.librarian.gradle.internal
33
import com.android.build.api.dsl.LibraryExtension
44
import com.android.build.gradle.tasks.SourceJarTask
55
import com.gradleup.librarian.gradle.LIBRARIAN_GENERATE_VERSION
6-
import com.gradleup.librarian.gradle.internal.androidExtension
76
import org.gradle.api.Project
87
import org.gradle.api.publish.PublishingExtension
98
import org.gradle.api.publish.maven.MavenPublication
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.gradleup.librarian.gradle.internal.task
2+
3+
import gratatouille.tasks.GInputFiles
4+
5+
6+
internal fun GInputFiles.findDeploymentName(): String {
7+
val gavs = mapNotNull {
8+
if (!it.normalizedPath.endsWith(".pom")) {
9+
return@mapNotNull null
10+
}
11+
12+
Gav.from(it.normalizedPath.substringBeforeLast('/'))
13+
}
14+
15+
val groups = gavs.map { it.groupId }.distinct()
16+
val artifacts = gavs.map { it.artifactId }.distinct()
17+
val versions = gavs.map { it.version }.distinct()
18+
19+
return buildString {
20+
if (groups.size == 1) {
21+
append(groups.single())
22+
} else {
23+
append("multiple-groups")
24+
}
25+
append(':')
26+
if (artifacts.size == 1) {
27+
append(artifacts.single())
28+
} else {
29+
append("multiple-artifacts")
30+
}
31+
append(':')
32+
if (versions.size == 1) {
33+
append(versions.single())
34+
} else {
35+
append("multiple-versions")
36+
}
37+
}
38+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.gradleup.librarian.gradle.internal.task
2+
3+
internal data class Gav(
4+
val groupId: String,
5+
val artifactId: String,
6+
val version: String,
7+
) {
8+
companion object {
9+
fun from(gavPath: String): Gav {
10+
val versionIndex = gavPath.lastIndexOf('/')
11+
check(versionIndex != -1) {
12+
"Librarian: invalid maven path '$gavPath' (expected group/artifact/version)"
13+
}
14+
val version = gavPath.substring(versionIndex + 1)
15+
val artifactIndex = gavPath.lastIndexOf('/', versionIndex - 1)
16+
check(artifactIndex != -1) {
17+
"Librarian: invalid maven path '$gavPath' (expected group/artifact/version)"
18+
}
19+
val artifact = gavPath.substring(artifactIndex + 1, versionIndex)
20+
val group = gavPath.substring(0, artifactIndex)
21+
22+
check(group.isNotEmpty()) {
23+
"Librarian: empty groupId in '$gavPath'"
24+
}
25+
check(artifact.isNotEmpty()) {
26+
"Librarian: empty artifactId in '$gavPath'"
27+
}
28+
check(version.isNotEmpty()) {
29+
"Librarian: empty version in '$gavPath'"
30+
}
31+
return Gav(group.toGroupId(), artifact, version)
32+
}
33+
}
34+
}
35+
36+
internal fun String.replaceBuildNumber(artifactId: String, snapshotVersion: String, newBuildNumber: Int): String {
37+
// module1-0.0.3-20250623.104441-1.jar.asc
38+
val versionWithoutSnapshot = snapshotVersion.replace("-SNAPSHOT","")
39+
return replace(Regex("(${artifactId}-$versionWithoutSnapshot-[0-9]{8}\\.[0-9]{6}-)[0-9]+(.*)")) {
40+
"${it.groupValues[1]}$newBuildNumber${it.groupValues[2]}"
41+
}
42+
}
43+
44+
internal fun String.toPath() = this.replace('.', '/')
45+
internal fun String.toGroupId() = this.replace('/', '.')
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package com.gradleup.librarian.gradle.internal.task
2+
3+
import kotlinx.serialization.Serializable
4+
import kotlinx.serialization.StringFormat
5+
import nl.adaptivity.xmlutil.serialization.XML
6+
import nl.adaptivity.xmlutil.serialization.XmlChildrenName
7+
import nl.adaptivity.xmlutil.serialization.XmlElement
8+
import nl.adaptivity.xmlutil.serialization.XmlSerialName
9+
10+
@Serializable
11+
@XmlSerialName("metadata")
12+
internal data class VersionMetadata(
13+
val modelVersion: String = "1.1.0",
14+
@XmlElement
15+
val groupId: String,
16+
@XmlElement
17+
val artifactId: String,
18+
val versioning: Versioning,
19+
@XmlElement
20+
val version: String,
21+
) {
22+
@Serializable
23+
@XmlSerialName("versioning")
24+
data class Versioning(
25+
@XmlElement
26+
val lastUpdated: String,
27+
val snapshot: Snapshot,
28+
@XmlChildrenName("snapshotVersion")
29+
val snapshotVersions: List<SnapshotVersion>,
30+
)
31+
32+
@Serializable
33+
@XmlSerialName("snapshot")
34+
data class Snapshot(
35+
@XmlElement
36+
val timestamp: String,
37+
@XmlElement
38+
val buildNumber: Int,
39+
)
40+
41+
@Serializable
42+
@XmlSerialName("snapshotVersion")
43+
data class SnapshotVersion(
44+
@XmlElement
45+
val classifier: String?,
46+
@XmlElement
47+
val extension: String,
48+
@XmlElement
49+
val value: String,
50+
@XmlElement
51+
val updated: String,
52+
)
53+
}
54+
55+
@Serializable
56+
@XmlSerialName("metadata")
57+
internal data class ArtifactMetadata(
58+
@XmlElement
59+
val groupId: String,
60+
@XmlElement
61+
val artifactId: String,
62+
val versioning: Versioning,
63+
) {
64+
@Serializable
65+
@XmlSerialName("versioning")
66+
data class Versioning(
67+
@XmlElement
68+
val latest: String,
69+
@XmlElement
70+
val release: String?, // Maybe null if the element is missing or empty if empty
71+
@XmlElement
72+
@XmlChildrenName("version")
73+
val versions: List<String>,
74+
@XmlElement
75+
val lastUpdated: String,
76+
)
77+
}
78+
79+
val xml: StringFormat = XML {
80+
indent = 2
81+
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package nmcp.internal.task
2+
3+
import com.gradleup.librarian.gradle.internal.task.ArtifactMetadata
4+
import com.gradleup.librarian.gradle.internal.task.FilesystemTransport
5+
import com.gradleup.librarian.gradle.internal.task.Gav
6+
import com.gradleup.librarian.gradle.internal.task.GcsTransport
7+
import com.gradleup.librarian.gradle.internal.task.HttpTransport
8+
import com.gradleup.librarian.gradle.internal.task.NmcpCredentials
9+
import com.gradleup.librarian.gradle.internal.task.Transport
10+
import com.gradleup.librarian.gradle.internal.task.VersionMetadata
11+
import com.gradleup.librarian.gradle.internal.task.put
12+
import com.gradleup.librarian.gradle.internal.task.replaceBuildNumber
13+
import com.gradleup.librarian.gradle.internal.task.toPath
14+
import com.gradleup.librarian.gradle.internal.task.xml
15+
import gratatouille.tasks.FileWithPath
16+
import gratatouille.tasks.GInputFiles
17+
import gratatouille.tasks.GLogger
18+
import gratatouille.tasks.GTask
19+
import java.security.MessageDigest
20+
import kotlinx.serialization.decodeFromString
21+
import kotlinx.serialization.encodeToString
22+
import okio.ByteString.Companion.toByteString
23+
24+
@GTask(pure = false)
25+
fun nmcpPublishFileByFile(
26+
logger: GLogger,
27+
url: String,
28+
username: String?,
29+
password: String?,
30+
inputFiles: GInputFiles,
31+
) {
32+
val credentials = if (username != null) {
33+
check(!password.isNullOrBlank()) {
34+
"Librarian: password is missing"
35+
}
36+
NmcpCredentials(username, password)
37+
} else {
38+
null
39+
}
40+
val transport = when {
41+
url.startsWith("http://") || url.startsWith("https://") -> {
42+
HttpTransport(url, credentials, logger)
43+
}
44+
45+
url.startsWith("file://") -> {
46+
FilesystemTransport(url.substring("file://".length))
47+
}
48+
49+
url.startsWith("gcs://") -> {
50+
GcsTransport(logger, url.substring("gcs://".length), googleServicesJson = password ?: error("Librarian: missing google services json"))
51+
}
52+
53+
else -> {
54+
error("Librarian: unsupported url '$url'")
55+
}
56+
}
57+
58+
inputFiles
59+
.filter { it.file.isFile }
60+
.groupBy {
61+
it.normalizedPath.substringBeforeLast('/')
62+
}.forEach { (gavPath, files) ->
63+
val gav = Gav.from(gavPath)
64+
val version = gav.version
65+
66+
if (files.all { it.normalizedPath.substringAfterLast('/').startsWith("maven-metadata") }) {
67+
/**
68+
* Update the [artifact metadata](https://maven.apache.org/repositories/metadata.html).
69+
*
70+
* See https://repo1.maven.org/maven2/com/apollographql/apollo/apollo-api-jvm/maven-metadata.xml for an example.
71+
*/
72+
val localArtifactMetadataFile =
73+
files.firstOrNull { it.normalizedPath.substringAfterLast('/') == "maven-metadata.xml" }
74+
if (localArtifactMetadataFile == null) {
75+
error("Librarian: cannot find artifact maven-metadata.xml in '${gav.groupId.toPath()}/${gav.artifactId}'")
76+
}
77+
val artifactMetadataPath = localArtifactMetadataFile.normalizedPath
78+
79+
val localArtifactMetadata = xml.decodeFromString<ArtifactMetadata>(localArtifactMetadataFile.file.readText())
80+
val remoteArtifactMetadata = transport.get(artifactMetadataPath)
81+
82+
val existingVersions = if (remoteArtifactMetadata != null) {
83+
xml.decodeFromString<ArtifactMetadata>(remoteArtifactMetadata.use { it.readUtf8() }).versioning.versions
84+
} else {
85+
emptyList()
86+
}
87+
88+
/**
89+
* See https://github.com/gradle/gradle/blob/cb0c615fb8e3690971bb7f89ad80f58943360624/platforms/software/maven/src/main/java/org/gradle/api/publish/maven/internal/publisher/AbstractMavenPublisher.java#L116.
90+
*/
91+
val versions = existingVersions.toMutableList()
92+
if (!versions.none { it == gav.version }) {
93+
versions.add(gav.version)
94+
}
95+
val newArtifactMetadata = localArtifactMetadata.copy(
96+
versioning = localArtifactMetadata.versioning.copy(
97+
versions = versions,
98+
),
99+
)
100+
101+
val bytes = encodeToXml(newArtifactMetadata).toByteArray()
102+
transport.put(artifactMetadataPath, bytes)
103+
setOf("md5", "sha1", "sha256", "sha512").forEach {
104+
transport.put("$artifactMetadataPath.$it", bytes.digest(it.uppercase()))
105+
}
106+
107+
return@forEach
108+
}
109+
/**
110+
* This is a proper directory containing artifacts
111+
*/
112+
if (version.endsWith("-SNAPSHOT")) {
113+
/**
114+
* This is a snapshot:
115+
* - update the [version metadata](https://maven.apache.org/repositories/metadata.html).
116+
* - path the file names to include the new build number.
117+
*
118+
* See https://s01.oss.sonatype.org/content/repositories/snapshots/com/apollographql/apollo/apollo-api-jvm/maven-metadata.xml for an example.
119+
*
120+
* For snapshots, it's not 100% clear who owns the metadata as the repository might expire some snapshot and therefore need to rewrite the
121+
* metadata to keep things consistent. This means, there are 2 possibly concurrent writers to maven-metadata.xml: the repository and the
122+
* publisher. Hopefully it's not too much of a problem in practice.
123+
*
124+
* See https://github.com/gradle/gradle/blob/d1ee068b1ee7f62ffcbb549352469307781af72e/platforms/software/maven/src/main/java/org/gradle/api/publish/maven/internal/publisher/MavenRemotePublisher.java#L70.
125+
*/
126+
val versionMetadataPath = "$gavPath/maven-metadata.xml"
127+
val localVersionMetadataFile = files.firstOrNull {
128+
it.normalizedPath == versionMetadataPath
129+
}
130+
if (localVersionMetadataFile == null) {
131+
error("Librarian: cannot find version maven-metadata.xml in '$gavPath'")
132+
}
133+
134+
val localVersionMetadata =
135+
xml.decodeFromString<VersionMetadata>(localVersionMetadataFile.file.readText())
136+
val remoteVersionMetadata = transport.get(versionMetadataPath)
137+
138+
val buildNumber = if (remoteVersionMetadata == null) {
139+
1
140+
} else {
141+
xml.decodeFromString<VersionMetadata>(remoteVersionMetadata.use { it.readUtf8() }).versioning.snapshot.buildNumber + 1
142+
}
143+
144+
val newVersionMetadata = localVersionMetadata.copy(
145+
versioning = localVersionMetadata.versioning.copy(
146+
snapshot = localVersionMetadata.versioning.snapshot.copy(buildNumber = buildNumber),
147+
),
148+
)
149+
150+
val renamedFiles = files.mapNotNull {
151+
if (it.file.name.startsWith("maven-metadata.xml")) {
152+
return@mapNotNull null
153+
}
154+
val newName = it.file.name.replaceBuildNumber(gav.artifactId, gav.version, buildNumber)
155+
FileWithPath(it.file, "$gavPath/$newName")
156+
}
157+
158+
transport.uploadFiles(renamedFiles)
159+
160+
val bytes = encodeToXml(newVersionMetadata).toByteArray()
161+
transport.put(versionMetadataPath, bytes)
162+
setOf("md5", "sha1", "sha256", "sha512").forEach {
163+
transport.put("$versionMetadataPath.$it", bytes.digest(it.uppercase()))
164+
}
165+
} else {
166+
/**
167+
* Not a snapshot, plainly update all the files
168+
*/
169+
transport.uploadFiles(files)
170+
}
171+
}
172+
}
173+
174+
private fun Transport.uploadFiles(filesWithPath: List<FileWithPath>) {
175+
filesWithPath.forEach {
176+
put(it.normalizedPath, it.file)
177+
}
178+
}
179+
180+
/**
181+
* Helper function to add the `<?xml...` preamble as I haven't found how to do it with xmlutils
182+
*/
183+
internal inline fun <reified T> encodeToXml(t: T): String {
184+
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml.encodeToString(t)
185+
}
186+
187+
private fun ByteArray.digest(name: String): String {
188+
val md = MessageDigest.getInstance(name)
189+
190+
md.update(this, 0, size)
191+
val digest = md.digest()
192+
193+
return digest.toByteString().hex()
194+
}

0 commit comments

Comments
 (0)