diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 0ee9b1a..29c870e 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -32,7 +32,7 @@ jobs: if: always() # always run even if the previous step fails with: report_paths: 'build/test-results/**/*.xml' - require_tests: false # currently no tests present + require_tests: true annotate_only: true detailed_summary: true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6354dd7..653b237 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -41,7 +41,7 @@ jobs: # ORG_GRADLE_PROJECT_signing.password: ${{ secrets.SONATYE_PGP_PASSWORD }} # ORG_GRADLE_PROJECT_signing.keyId: ${{ secrets.SONATYE_PGP_KEY_ID }} # ORG_GRADLE_PROJECT_signing.secretKeyRingFile: /home/runner/.gnupg/secring.gpg - + # in-memory key ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SONATYE_PGP_PASSWORD }} ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SONATYE_PGP_PRIVATE_KEY }} @@ -55,8 +55,8 @@ jobs: if: always() # always run even if the previous step fails with: report_paths: 'build/test-results/**/*.xml' - require_tests: false # currently no tests present + require_tests: true # currently no tests present annotate_only: true detailed_summary: true - # fail_on_failure: true \ No newline at end of file + # fail_on_failure: true diff --git a/.gitignore b/.gitignore index 494adcc..3cfd3e3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ bin/ .project .settings/ *~ +/.idea/ diff --git a/README.md b/README.md index 9feab4b..15dff51 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ apply plugin: 'org.standardout.bnd-platform' The **platform** plugin comes with several Gradle tasks - the following are the main tasks and build upon each other: * ***bundles*** - create bundles and write them to **build/plugins** -* ***potentialOptionalImports*** Creates a potentialOptionalImports.txt file of imported packages of all generated bundles with the optionalImport instruction (See "Optional Dependencies" section below) +* ***potentialOptionalImports*** Creates a potentialOptionalImports.txt file of imported packages of all generated bundles with the optionalImport instruction (See "Optional Dependencies" section below) * ***updateSite*** - create a p2 repository from the bundles and write it to **build/updatesite** (default) * ***updateSiteZip*** - create a ZIP archive from the p2 repository and write it to **build/updatesite.zip** (default) @@ -462,6 +462,7 @@ Via the platform extension there are several settings you can provide: * **updateSiteDir** - the directory the generated p2 repository is written to (default: `new File(buildDir, 'updatesite')`) * **updateSiteZipFile** - the target file for the zipped p2 repository (default: `new File(buildDir, 'updatesite.zip')`) * **appendUpdateSite** - if any the generated p2 repository should be appended to the one that already exists in **updateSiteDir** (default: `false`) +* **createFeatureVersionFiles** - if for the created update site, a version file should be created per feature, e.g. `_versions.json`, that includes information on the versions of the feature available in the p2 repository (default: `false`) * **eclipseHome** - File object pointing to the directory of a local Eclipse installation to be used for generating the p2 repository (default: `null`) * **eclipseMirror** - Eclipse download URLs to be used when no local installation is provided via *eclipseHome*. Since version 3 uses an Eclipse 2023-09 mirror by default. * **downloadsDir** - the directory to store the downloaded Eclipse installation on local, this works if *eclipseHome* is not specified. (default: `new File(buildDir, 'eclipse-downloads')`) diff --git a/build.gradle b/build.gradle index 6ccacc2..5bd4c67 100644 --- a/build.gradle +++ b/build.gradle @@ -15,8 +15,11 @@ repositories { group = 'org.standardout' version = '3.1.0-SNAPSHOT' -sourceCompatibility = '1.8' -targetCompatibility = '1.8' +java { + toolchain { + languageVersion = JavaLanguageVersion.of(8) + } +} jar { // include license into jar @@ -35,6 +38,16 @@ dependencies { implementation 'commons-io:commons-io:2.15.1' implementation 'de.undercouch:gradle-download-task:5.6.0' implementation localGroovy() + + // Testing + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.2' + + testImplementation("org.assertj:assertj-core:3.25.1") +} + +test { + useJUnitPlatform() } tasks.wrapper { @@ -100,7 +113,7 @@ publishing { pom { name = 'bnd-platform' packaging = 'jar' - + configurePom(pom) } } diff --git a/src/main/groovy/org/standardout/gradle/plugin/platform/PlatformPlugin.groovy b/src/main/groovy/org/standardout/gradle/plugin/platform/PlatformPlugin.groovy index 8b6562e..3a56a77 100644 --- a/src/main/groovy/org/standardout/gradle/plugin/platform/PlatformPlugin.groovy +++ b/src/main/groovy/org/standardout/gradle/plugin/platform/PlatformPlugin.groovy @@ -16,6 +16,8 @@ package org.standardout.gradle.plugin.platform +import org.standardout.gradle.plugin.platform.internal.util.VersionFile + import java.util.jar.* import org.gradle.api.GradleException @@ -275,6 +277,11 @@ public class PlatformPlugin implements Plugin { } project.logger.info 'Built p2 repository.' + + def createFeatureVersionFiles = project.platform.createFeatureVersionFiles + if (createFeatureVersionFiles) { + VersionFile.createFeatureVersionFiles(project.platform.updateSiteDir) + } } } diff --git a/src/main/groovy/org/standardout/gradle/plugin/platform/PlatformPluginExtension.groovy b/src/main/groovy/org/standardout/gradle/plugin/platform/PlatformPluginExtension.groovy index fe2fb98..aa244c9 100644 --- a/src/main/groovy/org/standardout/gradle/plugin/platform/PlatformPluginExtension.groovy +++ b/src/main/groovy/org/standardout/gradle/plugin/platform/PlatformPluginExtension.groovy @@ -309,6 +309,16 @@ class PlatformPluginExtension { * If the update site should be appended instead of over-written. */ boolean appendUpdateSite = false + + /** + * If after creating the update site, additional files should be created for + * each feature contained in the update site, that includes the information + * on which versions are contained in the update site. + * + * This can be used with tools like Renovate to automated updates of feature + * versions where the update site is used. + */ + boolean createFeatureVersionFiles = false /** * The directory of a local Eclipse installation. If none is specified the diff --git a/src/main/groovy/org/standardout/gradle/plugin/platform/internal/util/VersionFile.groovy b/src/main/groovy/org/standardout/gradle/plugin/platform/internal/util/VersionFile.groovy new file mode 100644 index 0000000..4454cb7 --- /dev/null +++ b/src/main/groovy/org/standardout/gradle/plugin/platform/internal/util/VersionFile.groovy @@ -0,0 +1,96 @@ +package org.standardout.gradle.plugin.platform.internal.util + +import groovy.json.JsonOutput +import groovy.xml.XmlSlurper +import groovy.xml.slurpersupport.GPathResult + +import java.util.zip.ZipFile + +class VersionFile { + + /** + * Read an XML file from a Jar/Zip file using XMLSlurper. + * + * @param jarFile the Jar file the XML is contained in + * @param xmlFileName the name of the XML file in the Jar file + * @return the parsed XML file or null if the file did not exist + */ + private static GPathResult readXmlFromJar(File jarFile, String xmlFileName) { + def zipFile = new ZipFile(jarFile) + def entry = zipFile.getEntry(xmlFileName) + + if (entry) { + zipFile.getInputStream(entry).withStream { + new XmlSlurper().parse(it) + } + } else { + null + } + } + + /** + * Create version files for features in a p2 repository. + * + * @param updateSiteDir the location of the p2 repository + */ + static def createFeatureVersionFiles(File updateSiteDir) { + //TODO delete any previously existing version files? + + GPathResult artifactsXml + + def artifactsJar = new File(updateSiteDir, 'artifacts.jar') + if (artifactsJar.exists()) { + artifactsXml = readXmlFromJar(artifactsJar, 'artifacts.xml') + } + + if (artifactsXml == null) { + def artifactsXmlFile = new File(updateSiteDir, 'artifacts.xml') + artifactsXml = new XmlSlurper().parse(artifactsXmlFile) + } + + if (artifactsXml != null) { + createFeatureVersionFiles(updateSiteDir, artifactsXml) + } + } + + static def createFeatureVersionFiles(File updateSiteDir, GPathResult artifactsXml) { + def featureVersions = collectFeatureVersions(artifactsXml) + + featureVersions.forEach { featureId, versions -> + def versionFile = new File(updateSiteDir, "${featureId}_version.json") + writeVersionFile(versionFile, versions) + } + } + + static Map> collectFeatureVersions(GPathResult artifactsXml) { + def features = artifactsXml.artifacts.artifact.findAll{ a -> a.@classifier == 'org.eclipse.update.feature' } + + features + .findResults { feature -> + def id = feature.@id as String + def version = feature.@version as String + if (id && version) { + [id: id, version: version] + } + else { + null + } + } + .groupBy { it.id } + .collectEntries { id, objects -> + [id, objects.collect { obj -> obj.version }] + } + } + + /** + * Write a version file with the given versions. + * The format used is a Json file that is compatible with custom datasources in Renovate. + * See https://docs.renovatebot.com/modules/datasource/custom/#usage + * + * @param versionFile the file to write + * @param versions the versions + */ + static void writeVersionFile(File versionFile, List versions) { + versionFile.text = JsonOutput.toJson([releases: versions.collect { [version: it] }]) + } +} diff --git a/src/test/groovy/org/standardout/gradle/plugin/platform/util/VersionFileTest.groovy b/src/test/groovy/org/standardout/gradle/plugin/platform/util/VersionFileTest.groovy new file mode 100644 index 0000000..e92398b --- /dev/null +++ b/src/test/groovy/org/standardout/gradle/plugin/platform/util/VersionFileTest.groovy @@ -0,0 +1,41 @@ +package org.standardout.gradle.plugin.platform.util + +import groovy.xml.XmlSlurper +import org.junit.jupiter.api.Test +import org.standardout.gradle.plugin.platform.internal.util.VersionFile + +import static org.assertj.core.api.Assertions.* + +class VersionFileTest { + + @Test + void testCollectFeatureVersions() { + def xml = getClass().getClassLoader().getResourceAsStream('artifacts-xml/example.xml').withStream { + new XmlSlurper().parse(it) + } + + def featureName = 'to.wetransform.offlineresources.feature' + + def versions = VersionFile.collectFeatureVersions(xml) + + assertThat(versions) + .as('should have exactly one feature') + .hasSize(1) + .containsOnlyKeys(featureName) + .extractingByKey(featureName) + .asList() + .hasSize(2) // 2 versions expected + .containsExactly('2024.3.15.bnd-Ia6g4Q', '2024.3.18.bnd-tNmhEg') + } + + @Test + void testCollectFeatureVersionsEmpty() { + def xml = new XmlSlurper().parseText('') + + def versions = VersionFile.collectFeatureVersions(xml) + + assertThat(versions) + .isEmpty() + } + +} diff --git a/src/test/resources/artifacts-xml/example.xml b/src/test/resources/artifacts-xml/example.xml new file mode 100644 index 0000000..cdcc228 --- /dev/null +++ b/src/test/resources/artifacts-xml/example.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +