fix(build): isolate model catalog bundle preparation

This commit is contained in:
2026-07-23 02:11:50 +02:00
parent 62be4c9127
commit 1f1b03c6a8
7 changed files with 618 additions and 74 deletions

View File

@@ -0,0 +1,113 @@
package org.egothor.radixor
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.security.MessageDigest
import java.util.stream.Stream
/** Prepares the two POM-only catalog publications for a Maven Central bundle. */
abstract class PrepareModelCatalogBundleInputTask extends DefaultTask {
@InputDirectory
@PathSensitive(PathSensitivity.RELATIVE)
abstract DirectoryProperty getRawRepositoryDirectory()
@OutputDirectory
abstract DirectoryProperty getPreparedBundleDirectory()
@Input
abstract Property<String> getCatalogVersion()
/** Copies permitted publication files and creates Central's required legacy checksums. */
@TaskAction
void prepare() {
prepareBundle(rawRepositoryDirectory.get().asFile.toPath(),
preparedBundleDirectory.get().asFile.toPath(), catalogVersion.get())
}
static void prepareBundle(final Path rawRepository, final Path preparedDirectory,
final String version) {
if (!Files.isDirectory(rawRepository)) {
throw new GradleException("The raw model catalog staging repository does not exist: ${rawRepository}.")
}
deleteTree(preparedDirectory)
Files.createDirectories(preparedDirectory)
final Set<String> expectedPoms = [
"org/egothor/radixor-models-standard/${version}/radixor-models-standard-${version}.pom",
"org/egothor/radixor-models-bom/${version}/radixor-models-bom-${version}.pom"
] as Set<String>
final List<Path> copied = []
Files.walk(rawRepository).withCloseable { Stream<Path> paths ->
paths.filter(Files::isRegularFile).sorted().forEach { Path source ->
final String relative = rawRepository.relativize(source).toString().replace(File.separatorChar, '/' as char)
if (isExcludedPublicationMetadata(relative)) return
if (relative.endsWith('.jar') || relative.endsWith('/stemmer.gz')
|| relative.contains('benchmark-pack')) {
throw new GradleException("Unsupported model catalog publication file: ${relative}.")
}
final String pom = expectedPoms.find { String candidate ->
relative == candidate || relative.startsWith(candidate + '.')
}
if (pom == null) {
throw new GradleException("Unexpected file in the raw model catalog repository: ${relative}.")
}
if (relative == pom || relative == pom + '.asc') {
final Path target = preparedDirectory.resolve(relative)
Files.createDirectories(target.parent)
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING)
copied.add(target)
} else if (!(relative ==~ /.*\.pom(?:\.asc)?\.(?:md5|sha1|sha256|sha512)/)) {
throw new GradleException("Unsupported model catalog publication file: ${relative}.")
}
}
}
final List<Path> poms = copied.findAll { Path path -> path.fileName.toString().endsWith('.pom') }
if (copied.isEmpty()) {
throw new GradleException('No model catalog publication files were copied from the raw staging repository.')
}
if (poms.size() != 2 || !expectedPoms.every { String expected -> Files.isRegularFile(preparedDirectory.resolve(expected)) }) {
throw new GradleException("The prepared model catalog must contain exactly the standard and BOM POMs; found ${poms.size()} POM files.")
}
copied.each { Path artifact ->
writeDigest(artifact, 'MD5', artifact.resolveSibling(artifact.fileName.toString() + '.md5'))
writeDigest(artifact, 'SHA-1', artifact.resolveSibling(artifact.fileName.toString() + '.sha1'))
}
}
private static boolean isExcludedPublicationMetadata(final String relative) {
final String name = relative.substring(relative.lastIndexOf('/') + 1)
return name ==~ /maven-metadata.*\.xml(?:\..*)?/ || relative ==~ /.*\.module(?:\..*)?/
}
private static void writeDigest(final Path source, final String algorithm, final Path target) {
final MessageDigest digest = MessageDigest.getInstance(algorithm)
Files.newInputStream(source).withCloseable { InputStream input ->
final byte[] buffer = new byte[16 * 1024]
int count
while ((count = input.read(buffer)) >= 0) {
if (count > 0) digest.update(buffer, 0, count)
}
}
Files.writeString(target, digest.digest().encodeHex().toString(), java.nio.charset.StandardCharsets.US_ASCII)
}
private static void deleteTree(final Path directory) {
if (!Files.exists(directory)) return
Files.walk(directory).withCloseable { Stream<Path> paths ->
paths.sorted(Comparator.reverseOrder()).forEach(Files::delete)
}
}
}

View File

@@ -12,5 +12,13 @@ final class RadixorBuildSupportPlugin implements Plugin<Project> {
group = 'verification'
description = 'Creates an isolated local Maven repository for model dependency-resolution integration tests.'
}
project.tasks.register('prepareModelCatalogReleaseCandidate', PrepareModelCatalogBundleInputTask) {
group = 'publishing'
description = 'Prepares the isolated POM-only model catalog input for Maven Central.'
}
project.tasks.register('verifyModelCatalogReleaseCandidate', VerifyModelCatalogReleaseCandidateTask) {
group = 'verification'
description = 'Verifies catalog bundle contents, checksums, coordinates, and dependency semantics.'
}
}
}

View File

@@ -0,0 +1,194 @@
package org.egothor.radixor
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.w3c.dom.Element
import javax.xml.XMLConstants
import javax.xml.parsers.DocumentBuilderFactory
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.security.MessageDigest
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
/** Verifies the contents and Maven semantics of the model catalog Central bundle. */
abstract class VerifyModelCatalogReleaseCandidateTask extends DefaultTask {
@InputFile
@PathSensitive(PathSensitivity.RELATIVE)
abstract RegularFileProperty getBundleFile()
@OutputFile
abstract RegularFileProperty getReportFile()
@Input abstract Property<String> getCatalogVersion()
@Input abstract Property<String> getModelVersion()
@Input abstract ListProperty<String> getDefaultModelIds()
@Input abstract ListProperty<String> getAllModelIds()
/** Performs byte-level archive and semantic POM validation. */
@TaskAction
void verify() {
final List<String> entries = verifyBundle(bundleFile.get().asFile, catalogVersion.get(),
modelVersion.get(), defaultModelIds.get(), allModelIds.get())
final File report = reportFile.get().asFile
Files.createDirectories(report.toPath().parent)
Files.writeString(report.toPath(), "Bundle: ${bundleFile.get().asFile.name}\nBytes: ${bundleFile.get().asFile.length()}\n"
+ entries.join('\n') + '\n', StandardCharsets.UTF_8)
}
static List<String> verifyBundle(final File bundle, final String catalogVersion,
final String modelVersion, final List<String> defaultIds, final List<String> allIds) {
if (!bundle.isFile() || bundle.length() == 0L) {
throw new GradleException("The model catalog Central bundle is missing or empty: ${bundle}.")
}
final Map<String, byte[]> content = new TreeMap<>()
new ZipFile(bundle).withCloseable { ZipFile archive ->
archive.entries().each { ZipEntry entry ->
if (!entry.directory) {
archive.getInputStream(entry).withCloseable { InputStream input ->
content.put(entry.name, input.readAllBytes())
}
}
}
}
final List<String> entries = content.keySet().toList()
final List<String> poms = entries.findAll { String entry -> entry.endsWith('.pom') }
final List<String> unsupported = entries.findAll { String entry ->
!(entry ==~ 'org/egothor/radixor-models-(?:standard|bom)/[^/]+/'
+ 'radixor-models-(?:standard|bom)-[^/]+\\.pom(?:\\.asc)?(?:\\.(?:md5|sha1))?')
}
if (!unsupported.isEmpty()) {
throw new GradleException("The model catalog bundle contains unsupported files: ${unsupported}.")
}
if (poms.size() != 2) {
throw new GradleException("The model catalog bundle must contain exactly two POM files; found ${poms.size()}.")
}
if (entries.any { String entry -> entry.endsWith('.jar') || entry.endsWith('/stemmer.gz')
|| entry.endsWith('.module') || entry.contains('maven-metadata') || entry.contains('benchmark-pack') }) {
throw new GradleException('The model catalog bundle contains forbidden publication content.')
}
poms.each { String pom -> verifyChecksums(content, pom) }
entries.findAll { String entry -> entry.endsWith('.pom.asc') }.each { String signature ->
verifyChecksums(content, signature)
}
final String standardPath = expectedPomPath('standard', catalogVersion)
final String bomPath = expectedPomPath('bom', catalogVersion)
if (!content.containsKey(standardPath) || !content.containsKey(bomPath)) {
throw new GradleException('The bundle does not contain the expected standard and BOM coordinates.')
}
final Element standard = parsePom(content.get(standardPath))
final Element bom = parsePom(content.get(bomPath))
verifyCoordinates(standard, 'radixor-models-standard', catalogVersion)
verifyCoordinates(bom, 'radixor-models-bom', catalogVersion)
final Map<String, String> standardDependencies = dependencies(standard, false)
final Map<String, String> bomConstraints = dependencies(bom, true)
final Set<String> expectedDefaults = defaultIds.collect { String id -> "org.egothor:radixor-model-${id}" } as Set<String>
final Set<String> expectedAll = allIds.collect { String id -> "org.egothor:radixor-model-${id}" } as Set<String>
if (standardDependencies.keySet() != expectedDefaults
|| standardDependencies.values().any { String version -> version != modelVersion }
|| standardDependencies.containsKey('org.egothor:radixor-model-pl-pl-polimorf')
|| dependencyScopes(standard).any { String scope -> scope != 'runtime' }) {
throw new GradleException('The standard catalog POM must reference exactly the 20 default model artifacts at the model version.')
}
if (!dependencies(bom, false).isEmpty()) {
throw new GradleException('The model BOM must not introduce runtime dependencies.')
}
if (bomConstraints.keySet() != expectedAll
|| bomConstraints.values().any { String version -> version != modelVersion }) {
throw new GradleException('The model BOM must manage exactly all 21 model artifacts at the model version.')
}
return entries
}
private static String expectedPomPath(final String kind, final String version) {
return "org/egothor/radixor-models-${kind}/${version}/radixor-models-${kind}-${version}.pom"
}
private static void verifyChecksums(final Map<String, byte[]> content, final String artifact) {
['MD5': 'md5', 'SHA-1': 'sha1'].each { String algorithm, String extension ->
final String checksum = artifact + '.' + extension
if (!content.containsKey(checksum)) {
throw new GradleException("The catalog artifact is missing its ${algorithm} checksum: ${artifact}.")
}
final String expected = MessageDigest.getInstance(algorithm).digest(content.get(artifact)).encodeHex().toString()
final String actual = new String(content.get(checksum), StandardCharsets.US_ASCII).trim()
if (actual != expected) {
throw new GradleException("The ${algorithm} checksum does not match ${artifact}.")
}
}
}
private static Element parsePom(final byte[] xml) {
final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance()
factory.setNamespaceAware(true)
factory.setFeature('http://apache.org/xml/features/disallow-doctype-decl', true)
factory.setFeature('http://xml.org/sax/features/external-general-entities', false)
factory.setFeature('http://xml.org/sax/features/external-parameter-entities', false)
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, '')
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, '')
return factory.newDocumentBuilder().parse(new ByteArrayInputStream(xml)).documentElement
}
private static void verifyCoordinates(final Element project, final String artifactId, final String version) {
if (directText(project, 'groupId') != 'org.egothor'
|| directText(project, 'artifactId') != artifactId
|| directText(project, 'version') != version) {
throw new GradleException("Unexpected Maven coordinates for ${artifactId}.")
}
}
private static Map<String, String> dependencies(final Element project, final boolean managed) {
final Map<String, String> result = new TreeMap<>()
final Element parent = managed ? directChild(project, 'dependencyManagement') : project
final Element container = parent == null ? null : directChild(parent, 'dependencies')
if (container == null) return result
childElements(container, 'dependency').each { Element dependency ->
final String coordinate = directText(dependency, 'groupId') + ':' + directText(dependency, 'artifactId')
if (result.put(coordinate, directText(dependency, 'version')) != null) {
throw new GradleException("The catalog POM contains duplicate dependency ${coordinate}.")
}
}
return result
}
private static List<String> dependencyScopes(final Element project) {
final Element container = directChild(project, 'dependencies')
if (container == null) return []
return childElements(container, 'dependency').collect { Element dependency -> directText(dependency, 'scope') }
}
private static String directText(final Element parent, final String name) {
final Element child = directChild(parent, name)
return child == null ? null : child.textContent.trim()
}
private static Element directChild(final Element parent, final String name) {
if (parent == null) return null
for (int index = 0; index < parent.childNodes.length; index++) {
if (parent.childNodes.item(index) instanceof Element
&& parent.childNodes.item(index).localName == name) return (Element) parent.childNodes.item(index)
}
return null
}
private static List<Element> childElements(final Element parent, final String name) {
final List<Element> result = []
for (int index = 0; index < parent.childNodes.length; index++) {
if (parent.childNodes.item(index) instanceof Element
&& parent.childNodes.item(index).localName == name) result.add((Element) parent.childNodes.item(index))
}
return result
}
}

View File

@@ -0,0 +1,259 @@
package org.egothor.radixor
import org.gradle.api.GradleException
import org.gradle.testkit.runner.GradleRunner
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import java.security.MessageDigest
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import static org.junit.jupiter.api.Assertions.assertArrayEquals
import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.assertFalse
import static org.junit.jupiter.api.Assertions.assertThrows
import static org.junit.jupiter.api.Assertions.assertTrue
/** Exercises catalog publication filtering, isolation, checksums, and semantic verification. */
final class ModelCatalogBundleTaskTest {
private static final String CATALOG_VERSION = '2026.1'
private static final String MODEL_VERSION = '1.0.0'
private static final List<String> DEFAULTS = ['alpha', 'beta']
private static final List<String> ALL = ['alpha', 'beta', 'pl-pl-polimorf']
@TempDir Path temporaryDirectory
/** Prepares exactly two unsigned POMs and their checksums without changing raw bytes. */
@Test
void preparesUnsignedPublicationsWithoutMutatingRawInput() {
final Path raw = fixture(false)
final byte[] before = Files.readAllBytes(standardPom(raw))
final Path prepared = temporaryDirectory.resolve('prepared')
PrepareModelCatalogBundleInputTask.prepareBundle(raw, prepared, CATALOG_VERSION)
assertArrayEquals(before, Files.readAllBytes(standardPom(raw)))
assertEquals(6L, regularFiles(prepared))
assertTrue(Files.isRegularFile(prepared.resolve(relativeStandardPom() + '.md5')))
assertTrue(Files.isRegularFile(prepared.resolve(relativeBomPom() + '.sha1')))
}
/** Copies test-only signatures and generates checksums for both signatures. */
@Test
void preparesSignedPublications() {
final Path prepared = temporaryDirectory.resolve('prepared')
PrepareModelCatalogBundleInputTask.prepareBundle(fixture(true), prepared, CATALOG_VERSION)
assertEquals(12L, regularFiles(prepared))
assertTrue(Files.isRegularFile(prepared.resolve(relativeStandardPom() + '.asc.md5')))
assertTrue(Files.isRegularFile(prepared.resolve(relativeBomPom() + '.asc.sha1')))
}
/** Deletes stale prepared content before copying current publication files. */
@Test
void removesStalePreparedContent() {
final Path prepared = temporaryDirectory.resolve('prepared')
Files.createDirectories(prepared)
Files.writeString(prepared.resolve('stale.jar'), 'stale')
PrepareModelCatalogBundleInputTask.prepareBundle(fixture(false), prepared, CATALOG_VERSION)
assertFalse(Files.exists(prepared.resolve('stale.jar')))
}
/** Excludes Gradle module metadata, its sidecars, and Maven metadata. */
@Test
void excludesModuleAndMavenMetadata() {
final Path raw = fixture(false)
final Path module = standardPom(raw).resolveSibling("radixor-models-standard-${CATALOG_VERSION}.module")
Files.writeString(module, 'module')
Files.writeString(module.resolveSibling(module.fileName.toString() + '.asc'), 'signature')
Files.writeString(module.resolveSibling(module.fileName.toString() + '.sha1'), 'checksum')
Files.writeString(module.parent.resolve('maven-metadata-local.xml'), 'metadata')
final Path prepared = temporaryDirectory.resolve('prepared')
PrepareModelCatalogBundleInputTask.prepareBundle(raw, prepared, CATALOG_VERSION)
assertEquals(6L, regularFiles(prepared))
}
/** Rejects a missing standard publication. */
@Test
void rejectsMissingStandardPom() {
final Path raw = fixture(false)
Files.delete(standardPom(raw))
assertThrows(GradleException) {
PrepareModelCatalogBundleInputTask.prepareBundle(raw, temporaryDirectory.resolve('prepared'), CATALOG_VERSION)
}
}
/** Rejects a missing BOM publication. */
@Test
void rejectsMissingBomPom() {
final Path raw = fixture(false)
Files.delete(bomPom(raw))
assertThrows(GradleException) {
PrepareModelCatalogBundleInputTask.prepareBundle(raw, temporaryDirectory.resolve('prepared'), CATALOG_VERSION)
}
}
/** Rejects unexpected binary publication content. */
@Test
void rejectsUnexpectedJar() {
final Path raw = fixture(false)
Files.writeString(standardPom(raw).resolveSibling('unexpected.jar'), 'binary')
assertThrows(GradleException) {
PrepareModelCatalogBundleInputTask.prepareBundle(raw, temporaryDirectory.resolve('prepared'), CATALOG_VERSION)
}
}
/** Rejects dictionary content in the catalog staging repository. */
@Test
void rejectsDictionaryContent() {
final Path raw = fixture(false)
final Path dictionary = raw.resolve('unrelated/stemmer.gz')
Files.createDirectories(dictionary.parent)
Files.writeString(dictionary, 'dictionary')
assertThrows(GradleException) {
PrepareModelCatalogBundleInputTask.prepareBundle(raw, temporaryDirectory.resolve('prepared'), CATALOG_VERSION)
}
}
/** Produces and semantically verifies a nonempty ZIP from prepared files. */
@Test
void verifiesRealPreparedArchive() {
final Path prepared = temporaryDirectory.resolve('prepared')
PrepareModelCatalogBundleInputTask.prepareBundle(fixture(false), prepared, CATALOG_VERSION)
final File archive = zip(prepared, temporaryDirectory.resolve('catalog.zip'))
final List<String> entries = VerifyModelCatalogReleaseCandidateTask.verifyBundle(
archive, CATALOG_VERSION, MODEL_VERSION, DEFAULTS, ALL)
assertEquals(6, entries.size())
}
/** Rejects an archived checksum that does not match its POM. */
@Test
void rejectsIncorrectArchivedChecksum() {
final Path prepared = temporaryDirectory.resolve('prepared')
PrepareModelCatalogBundleInputTask.prepareBundle(fixture(false), prepared, CATALOG_VERSION)
Files.writeString(prepared.resolve(relativeStandardPom() + '.sha1'), 'incorrect')
final File archive = zip(prepared, temporaryDirectory.resolve('catalog.zip'))
assertThrows(GradleException) {
VerifyModelCatalogReleaseCandidateTask.verifyBundle(
archive, CATALOG_VERSION, MODEL_VERSION, DEFAULTS, ALL)
}
}
/** Repeated preparation replaces restored or stale output deterministically. */
@Test
void repeatedPreparationRecreatesValidInput() {
final Path raw = fixture(false)
final Path prepared = temporaryDirectory.resolve('prepared')
PrepareModelCatalogBundleInputTask.prepareBundle(raw, prepared, CATALOG_VERSION)
final String first = treeDigest(prepared)
Files.writeString(prepared.resolve('restored-history-stale.txt'), 'stale')
PrepareModelCatalogBundleInputTask.prepareBundle(raw, prepared, CATALOG_VERSION)
assertEquals(first, treeDigest(prepared))
}
/** Creates a real Gradle ZIP, rebuilds a missing output, and reuses Configuration Cache. */
@Test
void gradleZipRebuildsWithConfigurationCacheReuse() {
final Path project = temporaryDirectory.resolve('testkit-project')
Files.createDirectories(project)
Files.writeString(project.resolve('settings.gradle'), "rootProject.name = 'catalog-fixture'\n")
Files.writeString(project.resolve('build.gradle'), '''plugins {
id 'org.egothor.radixor.build-support'
}
tasks.named('prepareModelCatalogReleaseCandidate') {
rawRepositoryDirectory = layout.projectDirectory.dir('raw')
preparedBundleDirectory = layout.buildDirectory.dir('prepared')
catalogVersion = '2026.1'
}
tasks.register('bundle', Zip) {
dependsOn(tasks.named('prepareModelCatalogReleaseCandidate'))
from(layout.buildDirectory.dir('prepared'))
destinationDirectory = layout.buildDirectory.dir('candidate')
archiveFileName = 'catalog.zip'
}
''')
final Path raw = project.resolve('raw')
write(standardPom(raw), pom('radixor-models-standard', false))
write(bomPom(raw), pom('radixor-models-bom', true))
final List<String> arguments = ['bundle', '--configuration-cache',
'--configuration-cache-problems=fail', '--warning-mode=fail']
final String first = GradleRunner.create().withProjectDir(project.toFile())
.withPluginClasspath().withArguments(arguments).build().output
final Path archive = project.resolve('build/candidate/catalog.zip')
assertTrue(Files.size(archive) > 0L)
Files.delete(archive)
final String second = GradleRunner.create().withProjectDir(project.toFile())
.withPluginClasspath().withArguments(arguments).build().output
assertTrue(Files.size(archive) > 0L)
assertTrue(first.contains('Configuration cache entry stored.'))
assertTrue(second.contains('Configuration cache entry reused.'))
}
private Path fixture(final boolean signed) {
final Path raw = temporaryDirectory.resolve('raw')
write(standardPom(raw), pom('radixor-models-standard', false))
write(bomPom(raw), pom('radixor-models-bom', true))
if (signed) {
Files.writeString(standardPom(raw).resolveSibling(standardPom(raw).fileName.toString() + '.asc'), 'test signature')
Files.writeString(bomPom(raw).resolveSibling(bomPom(raw).fileName.toString() + '.asc'), 'test signature')
}
return raw
}
private static String pom(final String artifact, final boolean managed) {
final List<String> ids = managed ? ALL : DEFAULTS
final String dependencies = ids.collect { String id ->
"<dependency><groupId>org.egothor</groupId><artifactId>radixor-model-${id}</artifactId>" +
"<version>${MODEL_VERSION}</version>${managed ? '' : '<scope>runtime</scope>'}</dependency>"
}.join()
final String body = managed ? "<dependencyManagement><dependencies>${dependencies}</dependencies></dependencyManagement>"
: "<dependencies>${dependencies}</dependencies>"
return "<?xml version=\"1.0\"?><project xmlns=\"http://maven.apache.org/POM/4.0.0\">" +
"<modelVersion>4.0.0</modelVersion><groupId>org.egothor</groupId>" +
"<artifactId>${artifact}</artifactId><version>${CATALOG_VERSION}</version>${body}</project>"
}
private static Path standardPom(final Path raw) { raw.resolve(relativeStandardPom()) }
private static Path bomPom(final Path raw) { raw.resolve(relativeBomPom()) }
private static String relativeStandardPom() {
"org/egothor/radixor-models-standard/${CATALOG_VERSION}/radixor-models-standard-${CATALOG_VERSION}.pom"
}
private static String relativeBomPom() {
"org/egothor/radixor-models-bom/${CATALOG_VERSION}/radixor-models-bom-${CATALOG_VERSION}.pom"
}
private static void write(final Path path, final String value) {
Files.createDirectories(path.parent)
Files.writeString(path, value, StandardCharsets.UTF_8)
}
private static long regularFiles(final Path root) {
Files.walk(root).withCloseable { paths -> paths.filter(Files::isRegularFile).count() }
}
private static File zip(final Path root, final Path target) {
new ZipOutputStream(Files.newOutputStream(target)).withCloseable { ZipOutputStream output ->
Files.walk(root).withCloseable { paths ->
paths.filter(Files::isRegularFile).sorted().forEach { Path file ->
output.putNextEntry(new ZipEntry(root.relativize(file).toString().replace(File.separatorChar, '/' as char)))
Files.copy(file, output)
output.closeEntry()
}
}
}
return target.toFile()
}
private static String treeDigest(final Path root) {
final MessageDigest digest = MessageDigest.getInstance('SHA-256')
Files.walk(root).withCloseable { paths ->
paths.filter(Files::isRegularFile).sorted().forEach { Path path ->
digest.update(root.relativize(path).toString().getBytes(StandardCharsets.UTF_8))
digest.update(Files.readAllBytes(path))
}
}
return digest.digest().encodeHex().toString()
}
}

View File

@@ -603,12 +603,13 @@ tasks.named('prepareModelConsumerTestRepository') {
repositoryDirectory = layout.buildDirectory.dir('model-consumer-repository')
}
def cleanModelCatalogStaging = tasks.register('cleanModelCatalogStaging') {
def rawModelCatalogRepository = layout.buildDirectory.dir('model-catalog-staging-repository')
def preparedModelCatalogBundleInput = layout.buildDirectory.dir('model-catalog-bundle-input')
def cleanModelCatalogStaging = tasks.register('cleanModelCatalogStaging', Delete) {
group = 'publishing'
description = 'Cleans the isolated model catalog Maven staging repository.'
doLast {
project.delete(layout.buildDirectory.dir('model-catalog-staging-repository'))
}
delete(rawModelCatalogRepository)
}
gradle.projectsEvaluated {
@@ -620,40 +621,28 @@ gradle.projectsEvaluated {
}
}
tasks.register('prepareModelCatalogReleaseCandidate') {
group = 'publishing'
description = 'Stages the POM-only standard aggregate and model BOM with Central checksums.'
def prepareModelCatalogReleaseCandidate = tasks.named('prepareModelCatalogReleaseCandidate') {
dependsOn(project(':models:standard').tasks.named('check'))
dependsOn(project(':models:bom').tasks.named('check'))
dependsOn(':models:standard:publishStandardPublicationToCatalogStagingRepository')
dependsOn(':models:bom:publishBomPublicationToCatalogStagingRepository')
outputs.dir(layout.buildDirectory.dir('model-catalog-staging-repository'))
doLast {
File repository = layout.buildDirectory.dir('model-catalog-staging-repository').get().asFile
repository.eachFileRecurse { File artifact ->
if (artifact.isFile() && !['.md5', '.sha1', '.sha256', '.sha512'].any {
String extension -> artifact.name.endsWith(extension)
}) {
['MD5': 'md5', 'SHA-1': 'sha1'].each { String algorithm, String extension ->
String digest = java.security.MessageDigest.getInstance(algorithm)
.digest(artifact.bytes).encodeHex().toString()
new File(artifact.absolutePath + ".${extension}").setText(digest, 'US-ASCII')
}
}
}
}
rawRepositoryDirectory = rawModelCatalogRepository
preparedBundleDirectory = preparedModelCatalogBundleInput
catalogVersion = project(':models:standard').version.toString()
}
tasks.register('modelCatalogCentralBundle', Zip) {
def modelCatalogCentralBundle = tasks.register('modelCatalogCentralBundle', Zip) {
group = 'publishing'
description = 'Builds the local POM-only model catalog bundle without remote publication.'
dependsOn(tasks.named('prepareModelCatalogReleaseCandidate'))
from(layout.buildDirectory.dir('model-catalog-staging-repository')) {
exclude('**/maven-metadata*.xml*', '**/*.module*')
}
dependsOn(prepareModelCatalogReleaseCandidate)
from(preparedModelCatalogBundleInput)
destinationDirectory = layout.buildDirectory.dir('model-catalog-release-candidate')
archiveFileName = "radixor-models-catalog-${project(':models:standard').version}-central-bundle.zip"
doFirst {
File preparedInput = preparedModelCatalogBundleInput.get().asFile
if (!preparedInput.isDirectory() || preparedInput.listFiles() == null || preparedInput.listFiles().length == 0) {
throw new GradleException("The prepared model catalog bundle input is missing or empty: ${preparedInput}.")
}
if (providers.environmentVariable('GITHUB_REF_TYPE').orNull == 'tag'
&& (providers.environmentVariable('SIGNING_KEY').orNull?.isBlank() != false
|| providers.environmentVariable('SIGNING_PASSWORD').orNull?.isBlank() != false)) {
@@ -662,45 +651,20 @@ tasks.register('modelCatalogCentralBundle', Zip) {
}
}
tasks.register('verifyModelCatalogReleaseCandidate') {
group = 'verification'
description = 'Verifies that the local catalog bundle contains only two POM publications, signatures when configured, and checksums.'
dependsOn(tasks.named('modelCatalogCentralBundle'))
outputs.file(layout.buildDirectory.file('reports/models/catalog-release-candidate.txt'))
doLast {
File bundle = tasks.named('modelCatalogCentralBundle', Zip).get().archiveFile.get().asFile
List<String> entries = []
new java.util.zip.ZipFile(bundle).withCloseable { java.util.zip.ZipFile archive ->
archive.entries().each { java.util.zip.ZipEntry entry ->
if (!entry.directory) entries.add(entry.name)
}
}
entries.sort()
List<String> poms = entries.findAll { String entry -> entry.endsWith('.pom') }
if (poms.size() != 2 || entries.any { String entry ->
entry.endsWith('.jar') || entry.endsWith('/stemmer.gz') || entry.contains('benchmark-pack')
}) {
throw new GradleException('The model catalog bundle must contain only the standard and BOM POM publications.')
}
List<String> unsupported = entries.findAll { String entry ->
!(entry.endsWith('.pom') || entry.endsWith('.pom.md5') || entry.endsWith('.pom.sha1')
|| entry.endsWith('.pom.sha256') || entry.endsWith('.pom.sha512')
|| entry.endsWith('.pom.asc') || entry.endsWith('.pom.asc.md5')
|| entry.endsWith('.pom.asc.sha1') || entry.endsWith('.pom.asc.sha256')
|| entry.endsWith('.pom.asc.sha512'))
}
if (!unsupported.isEmpty()) {
throw new GradleException("The model catalog bundle contains unsupported files: ${unsupported}.")
}
poms.each { String pom ->
if (!entries.contains(pom + '.md5') || !entries.contains(pom + '.sha1')) {
throw new GradleException("The model catalog POM is missing Central checksums: ${pom}.")
}
}
File report = layout.buildDirectory.file('reports/models/catalog-release-candidate.txt').get().asFile
report.parentFile.mkdirs()
report.setText("Bundle: ${bundle.name}\nBytes: ${bundle.length()}\n" + entries.join('\n') + '\n', 'UTF-8')
def publishedModelVersions = modelProjects().collect { Project modelProject ->
modelProject.file('model-version.txt').getText('UTF-8').trim()
}.toSet()
if (publishedModelVersions.size() != 1) {
throw new GradleException("The catalog verifier requires one common model version; found ${publishedModelVersions}.")
}
tasks.named('verifyModelCatalogReleaseCandidate') {
bundleFile = modelCatalogCentralBundle.flatMap { Zip archive -> archive.archiveFile }
reportFile = layout.buildDirectory.file('reports/models/catalog-release-candidate.txt')
catalogVersion = project(':models:standard').version.toString()
modelVersion = publishedModelVersions.first()
defaultModelIds = defaultModelProjects().collect { Project modelProject -> modelProject.name }
allModelIds = modelProjects().collect { Project modelProject -> modelProject.name }
}
tasks.register('verifyArtifactSizes') {

View File

@@ -74,23 +74,26 @@ signing {
}
}
def bomPublicationPom = layout.buildDirectory.file('publications/bom/pom-default.xml')
def expectedBomArtifactIds = modelIds.collect { String modelId -> "radixor-model-${modelId}" }
def bomHasJarTasks = !tasks.withType(Jar).isEmpty()
tasks.register('verifyPomOnlyPlatform') {
group = 'verification'
description = 'Verifies the POM-only model dependency-management platform.'
dependsOn(tasks.named('generatePomFileForBomPublication'))
inputs.file(bomPublicationPom)
doLast {
File pomFile = layout.buildDirectory.file('publications/bom/pom-default.xml').get().asFile
File pomFile = bomPublicationPom.get().asFile
Node pom = new XmlParser().parse(pomFile)
List<Node> constraints = pom.dependencyManagement.dependencies.dependency as List<Node>
List<String> artifactIds = constraints.collect { Node dependency -> dependency.artifactId.text() }
List<String> expected = modelIds.collect { String modelId -> "radixor-model-${modelId}" }
if (pom.packaging.text() != 'pom' || artifactIds != expected) {
if (pom.packaging.text() != 'pom' || artifactIds != expectedBomArtifactIds) {
throw new GradleException('radixor-models-bom must publish exactly the ordered model constraints as Maven packaging pom.')
}
if (!pom.dependencies.isEmpty()) {
throw new GradleException('radixor-models-bom must not introduce runtime model dependencies.')
}
if (!tasks.withType(Jar).isEmpty()) {
if (bomHasJarTasks) {
throw new GradleException('radixor-models-bom must not create binary, sources, or Javadoc JARs.')
}
}

View File

@@ -85,28 +85,31 @@ signing {
}
}
def standardPublicationPom = layout.buildDirectory.file('publications/standard/pom-default.xml')
def expectedStandardArtifactIds = defaultModelIds.collect { String modelId -> "radixor-model-${modelId}" }
def standardHasJarTasks = !tasks.withType(Jar).isEmpty()
tasks.register('verifyPomOnlyAggregate') {
group = 'verification'
description = 'Verifies the standard POM-only aggregate and its runtime model dependencies.'
dependsOn(tasks.named('generatePomFileForStandardPublication'))
inputs.file(standardPublicationPom)
doLast {
File pomFile = layout.buildDirectory.file('publications/standard/pom-default.xml').get().asFile
File pomFile = standardPublicationPom.get().asFile
Node pom = new XmlParser().parse(pomFile)
List<Node> dependencies = pom.dependencies.dependency as List<Node>
List<String> artifactIds = dependencies.collect { Node dependency ->
dependency.artifactId.text()
}
List<String> expected = defaultModelIds.collect { String modelId -> "radixor-model-${modelId}" }
if (pom.packaging.text() != 'pom') {
throw new GradleException('radixor-models-standard must publish Maven packaging pom.')
}
if (artifactIds != expected || dependencies.any { Node dependency -> dependency.scope.text() != 'runtime' }) {
if (artifactIds != expectedStandardArtifactIds || dependencies.any { Node dependency -> dependency.scope.text() != 'runtime' }) {
throw new GradleException('radixor-models-standard must contain exactly the ordered default model runtime dependencies.')
}
if (artifactIds.contains('radixor-model-pl-pl-polimorf')) {
throw new GradleException('radixor-models-standard must exclude the optional PoliMorf model.')
}
if (!tasks.withType(Jar).isEmpty()) {
if (standardHasJarTasks) {
throw new GradleException('radixor-models-standard must not create binary, sources, or Javadoc JARs.')
}
}