From bceef000098b4da19b45fc8484a994dc0fdbdec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Sat, 1 Feb 2025 10:26:26 +0100 Subject: [PATCH] Add a version report to target update mojo Fix https://github.com/eclipse-tycho/tycho/issues/4668 --- .../eclipse/tycho/core/MarkdownBuilder.java | 10 + .../InstallableUnitLocationUpdater.java | 200 +++++++++++++++--- .../versionbump/MavenLocationUpdater.java | 41 +++- .../tycho/versionbump/MavenVersionUpdate.java | 44 ++++ .../tycho/versionbump/UpdateTargetMojo.java | 45 +++- 5 files changed, 308 insertions(+), 32 deletions(-) create mode 100644 tycho-extras/tycho-version-bump-plugin/src/main/java/org/eclipse/tycho/versionbump/MavenVersionUpdate.java diff --git a/tycho-core/src/main/java/org/eclipse/tycho/core/MarkdownBuilder.java b/tycho-core/src/main/java/org/eclipse/tycho/core/MarkdownBuilder.java index 5fd3861804..21bd8f29f4 100644 --- a/tycho-core/src/main/java/org/eclipse/tycho/core/MarkdownBuilder.java +++ b/tycho-core/src/main/java/org/eclipse/tycho/core/MarkdownBuilder.java @@ -45,6 +45,11 @@ public MarkdownBuilder addListItem(String item) { return this; } + public MarkdownBuilder addListItem2(String item) { + lines.add(" - " + escape(item)); + return this; + } + public static String escape(String item) { return item.replace("@", "@").replace("#", "#"); } @@ -80,4 +85,9 @@ public void h3(String string) { lines.add(""); } + public void h4(String string) { + lines.add("#### " + escape(string)); + lines.add(""); + } + } diff --git a/tycho-extras/tycho-version-bump-plugin/src/main/java/org/eclipse/tycho/versionbump/InstallableUnitLocationUpdater.java b/tycho-extras/tycho-version-bump-plugin/src/main/java/org/eclipse/tycho/versionbump/InstallableUnitLocationUpdater.java index 99110cdfce..adcd6c30b8 100644 --- a/tycho-extras/tycho-version-bump-plugin/src/main/java/org/eclipse/tycho/versionbump/InstallableUnitLocationUpdater.java +++ b/tycho-extras/tycho-version-bump-plugin/src/main/java/org/eclipse/tycho/versionbump/InstallableUnitLocationUpdater.java @@ -41,10 +41,12 @@ import org.eclipse.equinox.p2.core.IProvisioningAgent; import org.eclipse.equinox.p2.core.ProvisionException; import org.eclipse.equinox.p2.metadata.IInstallableUnit; +import org.eclipse.equinox.p2.metadata.IRequirement; import org.eclipse.equinox.p2.metadata.Version; import org.eclipse.equinox.p2.query.QueryUtil; import org.eclipse.equinox.p2.repository.metadata.IMetadataRepository; import org.eclipse.equinox.p2.repository.metadata.IMetadataRepositoryManager; +import org.eclipse.tycho.core.MarkdownBuilder; import org.eclipse.tycho.p2resolver.TargetDefinitionVariableResolver; import org.eclipse.tycho.targetplatform.TargetDefinition; @@ -56,6 +58,7 @@ @Named public class InstallableUnitLocationUpdater { + private static final String EMPTY_VERSION = "0.0.0"; private static final Pattern DEFAULT_VERSION_PATTERN = Pattern.compile("(\\d+)\\.(\\d+)"); private static final Pattern DEFAULT_DATE_PATTERN = Pattern.compile("(\\d{4}-\\d{2})"); private static final Period DEFAULT_PERIOD = Period.ofMonths(3).plusDays(7); @@ -69,43 +72,88 @@ public class InstallableUnitLocationUpdater { boolean update(Element iuLocation, UpdateTargetMojo context) throws MojoFailureException, URISyntaxException, ProvisionException { + Log log = context.getLog(); + ResolvedRepository location = getResolvedLocation(iuLocation); + log.info("Check " + location.getLocation() + " for updates..."); List units = iuLocation.getChildren("unit").stream() - .map(unit -> new IU(unit.getAttributeValue("id"), unit.getAttributeValue("version"), unit)).toList(); - IMetadataRepository repository = getMetadataRepository(iuLocation, context, units); - boolean updated = false; + .map(unit -> new IU(unit.getAttributeValue("id"), getUnitVersion(unit), unit)).toList(); + IMetadataRepositoryManager repositoryManager = agent.getService(IMetadataRepositoryManager.class); + URI currentLocation = new URI(location.location()); + IMetadataRepository currentRepository = null; + MetadataRepositoryUpdate updateRepository = getMetadataRepository(location, context, units, repositoryManager); + boolean updated = updateRepository.updateLocation(location); + List updates = new ArrayList<>(); for (Element unit : iuLocation.getChildren("unit")) { String id = unit.getAttributeValue("id"); - IInstallableUnit latestUnit = repository + IInstallableUnit latestUnit = updateRepository.update() .query(QueryUtil.createLatestQuery(QueryUtil.createIUQuery(id)), null).stream().findFirst() .orElse(null); if (latestUnit != null) { + String currentVersion = getUnitVersion(unit); + if (EMPTY_VERSION.equals(currentVersion) && !context.isUpdateEmptyVersion()) { + continue; + } String newVersion = latestUnit.getVersion().toString(); - String currentVersion = unit.getAttributeValue("version"); if (newVersion.equals(currentVersion)) { - context.getLog().debug("unit '" + id + "' is already up-to date"); + log.debug("unit '" + id + "' is already up-to date"); } else { - updated = true; - context.getLog() - .info("Update version of unit '" + id + "' from " + currentVersion + " > " + newVersion); - unit.setAttribute("version", newVersion); + if (currentRepository == null) { + currentRepository = repositoryManager.loadRepository(currentLocation, null); + } + IInstallableUnit currentIU = currentRepository + .query(QueryUtil.createIUQuery(id, Version.create(currentVersion)), null).stream() + .findFirst().orElse(null); + VersionUpdate update = new VersionUpdate(id, currentVersion, currentIU, latestUnit); + if (update.hasVersionChange() && isCompatibleChange(update, context)) { + log.info("Update version of unit '" + id + "' from " + update.getPreviousVersion() + " > " + + newVersion); + updates.add(update); + updated = true; + unit.setAttribute("version", newVersion); + } } } else { - context.getLog().warn( - "Resolution result does not contain root installable unit '" + id + "' update is skipped!"); + log.warn("Resolution result does not contain root installable unit '" + id + "' update is skipped!"); + } + } + MarkdownBuilder builder = context.builder; + if (updates.isEmpty()) { + if (updateRepository.hasUpdate(currentLocation)) { + builder.h3("The location " + location.location() + " was updated:"); + builder.addListItem("Location changed to " + updateRepository.uri()); + builder.newLine(); } + return updated; + } + if (updateRepository.hasUpdate(currentLocation)) { + builder.h3("The location " + location.location() + " was updated:"); + builder.addListItem("Location changed to " + updateRepository.uri()); + } else { + builder.h3("The content of the location " + location.location() + " was updated:"); + } + for (VersionUpdate versionUpdate : updates) { + describeUpdate(versionUpdate, builder); } + builder.newLine(); return updated; } - private IMetadataRepository getMetadataRepository(Element iuLocation, UpdateTargetMojo context, List units) + private String getUnitVersion(Element unit) { + String value = unit.getAttributeValue("version"); + if (value == null || value.isBlank()) { + return EMPTY_VERSION; + } + return value; + } + + private MetadataRepositoryUpdate getMetadataRepository(ResolvedRepository location, UpdateTargetMojo context, + List units, IMetadataRepositoryManager repositoryManager) throws URISyntaxException, ProvisionException { - ResolvedRepository location = getResolvedLocation(iuLocation); - String raw = location.getLocation(); Log log = context.getLog(); - log.debug("Look for updates of location " + raw); + log.debug("Look for updates of location " + location.getLocation()); + String raw = location.getLocation(); URI uri = new URI(raw); List discovery = context.getUpdateSiteDiscovery(); - IMetadataRepositoryManager repositoryManager = agent.getService(IMetadataRepositoryManager.class); for (String strategy : discovery) { String trim = strategy.trim(); if (trim.equals("parent")) { @@ -122,7 +170,7 @@ private IMetadataRepository getMetadataRepository(Element iuLocation, UpdateTarg log.debug("No parent repository found for location " + uri + " using " + parentURI + ": " + e); continue; } - IMetadataRepository bestLocation = findBestLocation(context, units, location, repositoryManager, + MetadataRepositoryUpdate bestLocation = findBestLocation(context, units, location, repositoryManager, children); if (bestLocation != null) { return bestLocation; @@ -150,8 +198,8 @@ private IMetadataRepository getMetadataRepository(Element iuLocation, UpdateTarg log.debug("No repository found for location " + repoURI + ": " + e); } } - IMetadataRepository bestLocation = findBestLocation(context, units, location, repositoryManager, - repositories); + MetadataRepositoryUpdate bestLocation = findBestLocation(context, units, location, + repositoryManager, repositories); if (bestLocation != null) { return bestLocation; } @@ -192,7 +240,7 @@ private IMetadataRepository getMetadataRepository(Element iuLocation, UpdateTarg log.debug("Check location " + nextURL + " for updates with " + currentDateString + " > " + nextDateString + "..."); try { - IMetadataRepository bestLocation = findBestLocation(context, units, location, + MetadataRepositoryUpdate bestLocation = findBestLocation(context, units, location, repositoryManager, List.of(new URI(nextURL))); if (bestLocation != null) { return bestLocation; @@ -207,11 +255,12 @@ private IMetadataRepository getMetadataRepository(Element iuLocation, UpdateTarg } } //if nothing else is applicable return the original location repository... - return repositoryManager.loadRepository(uri, null); + return new MetadataRepositoryUpdate(null, repositoryManager.loadRepository(uri, null)); } - private IMetadataRepository findBestLocation(UpdateTargetMojo context, List units, ResolvedRepository location, - IMetadataRepositoryManager repositoryManager, Collection children) throws ProvisionException { + private MetadataRepositoryUpdate findBestLocation(UpdateTargetMojo context, List units, + ResolvedRepository location, IMetadataRepositoryManager repositoryManager, Collection children) + throws ProvisionException { List bestUnits = units; URI bestURI = null; //we now need to find a repository that has all units and they must have the same or higher version @@ -223,8 +272,7 @@ private IMetadataRepository findBestLocation(UpdateTargetMojo context, List } } if (bestURI != null) { - location.element().setAttribute("location", bestURI.toString()); - return repositoryManager.loadRepository(bestURI, null); + return new MetadataRepositoryUpdate(bestURI, repositoryManager.loadRepository(bestURI, null)); } return null; } @@ -249,6 +297,13 @@ private static Collection getChildren(IMetadataRepository repository) { return List.of(); } + private static boolean isCompatibleChange(VersionUpdate update, UpdateTargetMojo context) { + if (update.isMajorChange() && !context.isAllowMajorUpdates()) { + return false; + } + return true; + } + private static Collection expandRepository(URI uri, IMetadataRepositoryManager repositoryManager) throws ProvisionException { IMetadataRepository repository = repositoryManager.loadRepository(uri, null); @@ -326,6 +381,10 @@ public String getId() { return id(); } + public void setLocation(URI uri) { + element().setAttribute("location", uri.toString()); + } + } private static record IU(String id, String version, Element element) { @@ -409,4 +468,93 @@ private static Optional buildVersionString(int[] versions, String[] prefix, } } + private static void describeUpdate(VersionUpdate versionUpdate, MarkdownBuilder builder) { + IInstallableUnit update = versionUpdate.update(); + builder.addListItem(String.format("Unit %s was updated from %s to %s", versionUpdate.id(), + versionUpdate.getPreviousVersion(), update.getVersion())); + IInstallableUnit current = versionUpdate.current(); + if (current != null) { + Collection currentRequirements = current.getRequirements(); + for (IRequirement requirement : update.getRequirements()) { + if (!currentRequirements.contains(requirement) && requirement.getMin() > 0) { + builder.addListItem2( + String.format("additionally requires %s compared to the previous version", requirement)); + } + } + } + } + + private static record VersionUpdate(String id, String previousVersion, IInstallableUnit current, + IInstallableUnit update) { + + public boolean hasVersionChange() { + if (EMPTY_VERSION.equals(previousVersion)) { + return true; + } + if (current != null) { + return !current.getVersion().equals(update.getVersion()); + } + return !update.getVersion().toString().equals(previousVersion); + } + + public boolean isMajorChange() { + var currentVersion = getOSGiVersion(current); + var updateVersion = getOSGiVersion(update); + if (isVersion(currentVersion) && isVersion(updateVersion)) { + return updateVersion.getMajor() > currentVersion.getMajor(); + } + return false; + } + + private boolean isVersion(org.osgi.framework.Version version) { + return version != null && !org.osgi.framework.Version.emptyVersion.equals(version); + } + + public String getPreviousVersion() { + if (current != null) { + if (!current.getVersion().toString().equals(update.getVersion().toString())) { + return current.getVersion().toString(); + } + } + return previousVersion; + } + + } + + private static org.osgi.framework.Version getOSGiVersion(IInstallableUnit unit) { + if (unit != null) { + try { + return org.osgi.framework.Version.parseVersion(unit.getVersion().toString()); + } catch (RuntimeException e) { + //can't parse + } + } + return null; + } + + private static record MetadataRepositoryUpdate(URI uri, IMetadataRepository update) { + + public boolean updateLocation(ResolvedRepository element) { + if (uri != null) { + try { + if (new URI(element.getLocation()).equals(uri)) { + return false; + } + } catch (URISyntaxException e) { + } + element.setLocation(uri); + return true; + } + return false; + } + + public boolean hasUpdate(URI other) { + if (uri == null) { + return false; + } + return !uri.equals(other); + } + + } + } diff --git a/tycho-extras/tycho-version-bump-plugin/src/main/java/org/eclipse/tycho/versionbump/MavenLocationUpdater.java b/tycho-extras/tycho-version-bump-plugin/src/main/java/org/eclipse/tycho/versionbump/MavenLocationUpdater.java index 3fd175237f..4c86a989c3 100644 --- a/tycho-extras/tycho-version-bump-plugin/src/main/java/org/eclipse/tycho/versionbump/MavenLocationUpdater.java +++ b/tycho-extras/tycho-version-bump-plugin/src/main/java/org/eclipse/tycho/versionbump/MavenLocationUpdater.java @@ -12,6 +12,9 @@ *******************************************************************************/ package org.eclipse.tycho.versionbump; +import java.io.File; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -32,7 +35,13 @@ import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.resolution.ArtifactResolutionException; import org.eclipse.aether.resolution.VersionRangeResolutionException; +import org.eclipse.equinox.p2.metadata.IArtifactKey; +import org.eclipse.equinox.p2.metadata.IInstallableUnit; +import org.eclipse.equinox.p2.publisher.IPublisherInfo; +import org.eclipse.equinox.p2.publisher.PublisherInfo; +import org.eclipse.osgi.service.resolver.BundleDescription; import org.eclipse.tycho.TychoConstants; +import org.eclipse.tycho.p2maven.tmp.BundlesAction; import de.pdark.decentxml.Element; @@ -50,11 +59,12 @@ public class MavenLocationUpdater { @Inject protected Map wagonMap; - boolean update(Element mavenLocation, UpdateTargetMojo context) throws VersionRangeResolutionException, - ArtifactResolutionException, MojoExecutionException, VersionRetrievalException { + List update(Element mavenLocation, UpdateTargetMojo context) + throws VersionRangeResolutionException, ArtifactResolutionException, MojoExecutionException, + VersionRetrievalException { VersionsHelper helper = getHelper(context); - boolean changed = false; Element dependencies = mavenLocation.getChild("dependencies"); + List updates = new ArrayList<>(); if (dependencies != null) { for (Element dependency : dependencies.getChildren("dependency")) { Dependency mavenDependency = getDependency(dependency); @@ -69,14 +79,35 @@ boolean update(Element mavenLocation, UpdateTargetMojo context) throws VersionRa if (newVersion.equals(oldVersion)) { context.getLog().debug(mavenDependency + " is already up-to date"); } else { - changed = true; UpdateTargetMojo.setElementValue("version", newVersion, dependency); context.getLog().info("update " + mavenDependency + " to version " + newVersion); + IInstallableUnit current = getIU(helper, dependencyArtifact); + Dependency clone = mavenDependency.clone(); + clone.setVersion(newVersion); + IInstallableUnit update = getIU(helper, helper.createDependencyArtifact(clone)); + updates.add(new MavenVersionUpdate(dependencyArtifact, newVersion, current, update)); } } } } - return changed; + return updates; + } + + private IInstallableUnit getIU(VersionsHelper helper, Artifact dependencyArtifact) { + try { + helper.resolveArtifact(dependencyArtifact, false); + File file = dependencyArtifact.getFile(); + BundleDescription bundleDescription = BundlesAction.createBundleDescription(file); + if (bundleDescription != null) { + IArtifactKey key = BundlesAction.createBundleArtifactKey(bundleDescription.getSymbolicName(), + bundleDescription.getVersion().toString()); + PublisherInfo publisherInfo = new PublisherInfo(); + publisherInfo.setArtifactOptions(IPublisherInfo.A_INDEX); + return BundlesAction.createBundleIU(bundleDescription, key, publisherInfo); + } + } catch (Exception e) { + } + return null; } VersionsHelper getHelper(UpdateTargetMojo context) throws MojoExecutionException { diff --git a/tycho-extras/tycho-version-bump-plugin/src/main/java/org/eclipse/tycho/versionbump/MavenVersionUpdate.java b/tycho-extras/tycho-version-bump-plugin/src/main/java/org/eclipse/tycho/versionbump/MavenVersionUpdate.java new file mode 100644 index 0000000000..b4481c3c8a --- /dev/null +++ b/tycho-extras/tycho-version-bump-plugin/src/main/java/org/eclipse/tycho/versionbump/MavenVersionUpdate.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright (c) 2025 Christoph Läubrich and others. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.tycho.versionbump; + +import java.util.Collection; + +import org.apache.maven.artifact.Artifact; +import org.eclipse.equinox.p2.metadata.IInstallableUnit; +import org.eclipse.equinox.p2.metadata.IRequirement; +import org.eclipse.tycho.core.MarkdownBuilder; + +record MavenVersionUpdate(Artifact currentArtifact, String newVersion, IInstallableUnit current, + IInstallableUnit update) { + + public void describeUpdate(MarkdownBuilder builder) { + String msg = String.format("`%s` has been updated to version `%s`", currentArtifact().getId(), newVersion()); + builder.addListItem(msg); + IInstallableUnit current = current(); + IInstallableUnit updated = update(); + if (current != null && updated != null) { + Collection currentRequirements = current.getRequirements(); + for (IRequirement requirement : update.getRequirements()) { + if (!currentRequirements.contains(requirement) && requirement.getMin() > 0) { + builder.addListItem2( + String.format("additionally requires %s compared to the previous version", requirement)); + } + } + } + } + + public String id() { + return currentArtifact().getId() + "->" + newVersion(); + } +} diff --git a/tycho-extras/tycho-version-bump-plugin/src/main/java/org/eclipse/tycho/versionbump/UpdateTargetMojo.java b/tycho-extras/tycho-version-bump-plugin/src/main/java/org/eclipse/tycho/versionbump/UpdateTargetMojo.java index 29d1797647..c66836c0c0 100644 --- a/tycho-extras/tycho-version-bump-plugin/src/main/java/org/eclipse/tycho/versionbump/UpdateTargetMojo.java +++ b/tycho-extras/tycho-version-bump-plugin/src/main/java/org/eclipse/tycho/versionbump/UpdateTargetMojo.java @@ -22,6 +22,8 @@ import java.net.URI; import java.net.URISyntaxException; import java.time.Period; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Stream; @@ -37,6 +39,7 @@ import org.apache.maven.plugins.annotations.Parameter; import org.codehaus.mojo.versions.api.Segment; import org.codehaus.mojo.versions.model.RuleSet; +import org.eclipse.tycho.core.MarkdownBuilder; import org.eclipse.tycho.targetplatform.TargetPlatformArtifactResolver; import org.eclipse.tycho.targetplatform.TargetResolveException; @@ -145,6 +148,19 @@ public class UpdateTargetMojo extends AbstractUpdateMojo { @Parameter(property = "maven.version.rules") private String mavenRulesUri; + @Parameter(defaultValue = "Please review the changes and merge if appropriate, or cherry pick individual updates.", property = "tycho.updatetarget.report.preamble") + private String reportPreamble; + + @Parameter(defaultValue = "${project.build.directory}/targetUpdates.md", property = "tycho.updatetarget.report") + private File reportFileName; + + /** + * If enabled, missing or empty versions are updated to the most recent found in the repository + * to guarantee a stable build + */ + @Parameter(defaultValue = "true", property = "tycho.updatetarget.updateEmptyVersion") + private boolean updateEmptyVersion; + @Component private MavenSession mavenSession; @@ -157,6 +173,8 @@ public class UpdateTargetMojo extends AbstractUpdateMojo { @Inject private InstallableUnitLocationUpdater installableUnitLocationUpdater; + MarkdownBuilder builder; + @Override protected void doUpdate(File file) throws Exception { getLog().info("Update target file " + file); @@ -164,12 +182,20 @@ protected void doUpdate(File file) throws Exception { XMLParser parser = new XMLParser(); Document target = parser.parse(new XMLIOSource(file)); boolean changed = false; + builder = new MarkdownBuilder(reportFileName); + builder.h2("The content of the target `" + file.getName() + "` was updated"); + if (reportPreamble != null && !reportPreamble.isBlank()) { + builder.add(reportPreamble); + builder.newLine(); + } + List mavenUpdates = new ArrayList<>(); try (FileInputStream input = new FileInputStream(file)) { for (Element iuLocation : getLocations("InstallableUnit", target)) { changed |= installableUnitLocationUpdater.update(iuLocation, this); } for (Element mavenLocation : getLocations("Maven", target)) { - changed |= mavenLocationUpdater.update(mavenLocation, this); + mavenUpdates.addAll(mavenLocationUpdater.update(mavenLocation, this)); + changed |= mavenUpdates.size() > 0; } } if (changed) { @@ -182,7 +208,20 @@ protected void doUpdate(File file) throws Exception { xw.flush(); } } + if (mavenUpdates.size() > 0) { + builder.h3("The following maven artifacts have been updated:"); + Set updatedMsg = new HashSet<>(); + for (MavenVersionUpdate update : mavenUpdates) { + if (updatedMsg.add(update.id())) { + update.describeUpdate(builder); + } + } + builder.newLine(); + } + builder.newLine(); + builder.write(); } + builder = null; } static void setElementValue(String name, String value, Element root) { @@ -242,6 +281,10 @@ boolean isAllowMajorUpdates() { return allowMajorUpdates; } + boolean isUpdateEmptyVersion() { + return updateEmptyVersion; + } + MavenSession getMavenSession() { return mavenSession; }