Skip to content

Commit

Permalink
Add support for version patterns in URLs to update IU locations
Browse files Browse the repository at this point in the history
Some sites just use a given version scheme that is incremented on each
release but don'T offer a composite at the parent.

This now adds a new detector called 'versionPattern' that allows to
match a pattern and let the version be incremented as long as there is a
valid repo at that URL.
laeubi committed Jan 27, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent c6d7a98 commit 0f2b5c3
Showing 3 changed files with 207 additions and 158 deletions.
Original file line number Diff line number Diff line change
@@ -13,15 +13,19 @@
*******************************************************************************/
package org.eclipse.tycho.versionbump;

import static java.util.stream.Collectors.toList;

import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.stream.Stream;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.inject.Inject;
import javax.inject.Named;
@@ -36,17 +40,16 @@
import org.eclipse.equinox.p2.repository.metadata.IMetadataRepositoryManager;
import org.eclipse.tycho.p2resolver.TargetDefinitionVariableResolver;
import org.eclipse.tycho.targetplatform.TargetDefinition;
import org.eclipse.tycho.targetplatform.TargetDefinition.FollowRepositoryReferences;
import org.eclipse.tycho.targetplatform.TargetDefinition.IncludeMode;
import org.eclipse.tycho.targetplatform.TargetDefinition.InstallableUnitLocation;
import org.eclipse.tycho.targetplatform.TargetDefinition.Unit;
import org.eclipse.tycho.targetplatform.TargetDefinitionFile;

import de.pdark.decentxml.Element;

/**
* Updater for installable units
*/
@Named
public class InstallableUnitLocationUpdater {

private static final String VERSION_PATTERN_PREFIX = "versionPattern";
@Inject
private TargetDefinitionVariableResolver varResolver;
@Inject
@@ -85,37 +88,61 @@ boolean update(Element iuLocation, UpdateTargetMojo context)
private IMetadataRepository getMetadataRepository(Element iuLocation, UpdateTargetMojo context, List<IU> units)
throws URISyntaxException, ProvisionException {
ResolvedRepository location = getResolvedLocation(iuLocation);
URI uri = new URI(location.getLocation());
String discovery = context.getUpdateSiteDiscovery();
String raw = location.getLocation();
context.getLog().debug("Look for updates of location " + raw);
URI uri = new URI(raw);
List<String> discovery = context.getUpdateSiteDiscovery();
IMetadataRepositoryManager repositoryManager = agent.getService(IMetadataRepositoryManager.class);
if (discovery != null && !discovery.isBlank()) {
for (String strategy : discovery.split(",")) {
if (strategy.trim().equals("parent")) {
String str = uri.toASCIIString();
if (!str.endsWith("/")) {
str = str + "/";
}
URI parentURI = new URI(str + "../");
try {
IMetadataRepository repository = repositoryManager.loadRepository(parentURI, null);
List<IU> 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
for (URI child : getChildren(repository)) {
List<IU> find = findBestUnits(bestUnits, repositoryManager, child, context);
if (find != null) {
bestUnits = find;
bestURI = child;
}
}
if (bestURI != null) {
location.element().setAttribute("location", bestURI.toString());
return repositoryManager.loadRepository(bestURI, null);
for (String strategy : discovery) {
String trim = strategy.trim();
if (trim.equals("parent")) {
String str = uri.toASCIIString();
if (!str.endsWith("/")) {
str = str + "/";
}
URI parentURI = new URI(str + "../");
Collection<URI> children;
try {
children = getChildren(repositoryManager.loadRepository(parentURI, null));
} catch (ProvisionException e) {
// if we can't load it we can't use it but this is maybe because that no parent exits.
context.getLog()
.debug("No parent repository found for location " + uri + " using " + parentURI + ": " + e);
continue;
}
IMetadataRepository bestLocation = findBestLocation(context, units, location, repositoryManager,
children);
if (bestLocation != null) {
return bestLocation;
}
} else if (trim.startsWith(VERSION_PATTERN_PREFIX)) {
String substring = trim.substring(VERSION_PATTERN_PREFIX.length());
Pattern pattern;
if (substring.isEmpty()) {
pattern = Pattern.compile("(\\d+)\\.(\\d+)");
} else {
pattern = Pattern.compile(substring.substring(1));
}
context.getLog().debug("Using Pattern " + pattern + " to find version increments...");
Collection<URI> fromPattern = findUpdatesitesFromPattern(raw, pattern, repositoryManager,
context.getLog()::debug);
if (fromPattern.isEmpty()) {
context.getLog().debug("Nothing found to match the pattern " + pattern + " for location " + raw);
} else {
Set<URI> repositories = new HashSet<>();
for (URI repoURI : fromPattern) {
context.getLog().debug("Check location " + repoURI + "...");
try {
repositories.addAll(expandRepository(repoURI, repositoryManager));
} catch (ProvisionException e) {
// if we can't load it we can't use it but this is maybe because that no parent exits.
context.getLog().debug("No repository found for location " + repoURI + ": " + e);
}
} catch (ProvisionException e) {
// if we can't load it we can't use it but this is maybe because that no parent exits.
context.getLog().debug(
"No parent repository found for location " + uri + " using " + parentURI + ": " + e);
}
IMetadataRepository bestLocation = findBestLocation(context, units, location, repositoryManager,
repositories);
if (bestLocation != null) {
return bestLocation;
}
}
}
@@ -124,8 +151,35 @@ private IMetadataRepository getMetadataRepository(Element iuLocation, UpdateTarg
return repositoryManager.loadRepository(uri, null);
}

private IMetadataRepository findBestLocation(UpdateTargetMojo context, List<IU> units, ResolvedRepository location,
IMetadataRepositoryManager repositoryManager, Collection<URI> children) throws ProvisionException {
List<IU> 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
for (URI child : children) {
List<IU> find = findBestUnits(bestUnits, repositoryManager, child, context);
if (find != null) {
bestUnits = find;
bestURI = child;
}
}
if (bestURI != null) {
location.element().setAttribute("location", bestURI.toString());
return repositoryManager.loadRepository(bestURI, null);
}
return null;
}
//TODO this method should better be used (with an <exportedPackage>org.eclipse.equinox.p2.repository</exportedPackage> in extension.xml)
// But this currently fails the PGP mojo using org.bouncycastle.openpgp.PGPPublicKey
// private static Collection<URI> getChildren(IMetadataRepository repository) {
// if (repository instanceof ICompositeRepository<?> composite) {
// return composite.getChildren();
// }
// return List.of();
// }

@SuppressWarnings("unchecked")
private Collection<URI> getChildren(IMetadataRepository repository) {
private static Collection<URI> getChildren(IMetadataRepository repository) {
try {
Method method = repository.getClass().getDeclaredMethod("getChildren");
if (method.invoke(repository) instanceof Collection<?> c) {
@@ -136,7 +190,21 @@ private Collection<URI> getChildren(IMetadataRepository repository) {
return List.of();
}

private List<IU> findBestUnits(List<IU> units, IMetadataRepositoryManager repositoryManager, URI child,
private static Collection<URI> expandRepository(URI uri, IMetadataRepositoryManager repositoryManager)
throws ProvisionException {
IMetadataRepository repository = repositoryManager.loadRepository(uri, null);
Collection<URI> children = getChildren(repository);
if (children.isEmpty()) {
return List.of(uri);
}
List<URI> result = new ArrayList<>();
for (URI child : children) {
result.addAll(expandRepository(child, repositoryManager));
}
return result;
}

private static List<IU> findBestUnits(List<IU> units, IMetadataRepositoryManager repositoryManager, URI child,
UpdateTargetMojo context) throws ProvisionException {
IMetadataRepository childRepository = repositoryManager.loadRepository(child, null);
List<IU> list = new ArrayList<>();
@@ -180,119 +248,6 @@ private ResolvedRepository getResolvedLocation(Element iuLocation) {
return new ResolvedRepository(element.getAttributeValue("id"), resolved, element);
}

private static final class LatestVersionTarget implements TargetDefinition {

private TargetDefinitionFile delegate;
private TargetDefinitionVariableResolver varResolver;

public LatestVersionTarget(TargetDefinitionFile delegate, TargetDefinitionVariableResolver varResolver) {
this.delegate = delegate;
this.varResolver = varResolver;
}

@Override
public List<? extends Location> getLocations() {
return delegate.getLocations().stream().map(location -> {
if (location instanceof InstallableUnitLocation iuLocation) {
return new LatestVersionLocation(iuLocation, varResolver);
} else {
return location;
}
}).toList();
}

@Override
public boolean hasIncludedBundles() {
return delegate.hasIncludedBundles();
}

@Override
public String getOrigin() {
return delegate.getOrigin();
}

@Override
public String getTargetEE() {
return delegate.getTargetEE();
}

@Override
public Stream<ImplicitDependency> implicitDependencies() {
return delegate.implicitDependencies();
}

}

private static final class LatestVersionLocation implements InstallableUnitLocation {

private InstallableUnitLocation delegate;
private TargetDefinitionVariableResolver varResolver;

public LatestVersionLocation(InstallableUnitLocation delegate, TargetDefinitionVariableResolver varResolver) {
this.delegate = delegate;
this.varResolver = varResolver;
}

@Override
public List<? extends TargetDefinition.Repository> getRepositories() {
return delegate.getRepositories().stream().map(repo -> {
URI resolvedLocation = URI.create(varResolver.resolve(repo.getLocation()));
return new ResolvedRepository(repo.getId(), resolvedLocation.toString(), null);
}).collect(toList());
}

@Override
public List<? extends TargetDefinition.Unit> getUnits() {
return delegate.getUnits().stream().map(LatestVersionUnit::new).toList();
}

@Override
public IncludeMode getIncludeMode() {
return delegate.getIncludeMode();
}

@Override
public boolean includeAllEnvironments() {
return delegate.includeAllEnvironments();
}

@Override
public boolean includeSource() {
return delegate.includeSource();
}

@Override
public boolean includeConfigurePhase() {
return delegate.includeConfigurePhase();
}

@Override
public FollowRepositoryReferences followRepositoryReferences() {
return delegate.followRepositoryReferences();
}

}

private static final class LatestVersionUnit implements TargetDefinition.Unit {

private Unit delegate;

public LatestVersionUnit(TargetDefinition.Unit delegate) {
this.delegate = delegate;
}

@Override
public String getId() {
return delegate.getId();
}

@Override
public String getVersion() {
return "0.0.0";
}

}

private static final record ResolvedRepository(String id, String location, Element element)
implements TargetDefinition.Repository {

@@ -312,4 +267,81 @@ private static record IU(String id, String version, Element element) {

}

private static Collection<URI> findUpdatesitesFromPattern(String input, Pattern pattern,
IMetadataRepositoryManager repositoryManager, Consumer<String> debug) {
Matcher matcher = pattern.matcher(input);
if (matcher.find()) {
int count = matcher.groupCount();
int[] versions = new int[count];
String[] prefix = new String[count];
int offset = 0;
for (int i = 0; i < count; i++) {
int g = i + 1;
String group = matcher.group(g);
int start = matcher.start(g);
int end = matcher.end(g);
prefix[i] = input.substring(offset, start);
offset = end;
try {
versions[i] = Integer.parseInt(group);
} catch (RuntimeException e) {
versions[i] = -1;
}
}
Set<URI> uris = new LinkedHashSet<>();
for (int i = 0; i < versions.length; i++) {
if (versions[i] < 0) {
buildVersionString(versions, prefix, repositoryManager, debug).ifPresent(uris::add);
break;
}
}
for (int i = versions.length - 1; i >= 0; i--) {
int v = versions[i];
if (v > -1) {
int[] copy = versions.clone();
for (int j = i + 1; j < versions.length; j++) {
if (copy[j] > 0) {
copy[j] = 0;
}
}
Optional<URI> versionRepo;
do {
copy[i]++;
versionRepo = buildVersionString(copy, prefix, repositoryManager, debug);
} while (!versionRepo.isEmpty() && uris.add(versionRepo.get()));
}
}
return uris;
}
return List.of();
}

private static Optional<URI> buildVersionString(int[] versions, String[] prefix,
IMetadataRepositoryManager repositoryManager, Consumer<String> debug) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < versions.length; i++) {
sb.append(prefix[i]);
int v = versions[i];
if (v > -1) {
sb.append(v);
}
}
String string = sb.toString();
try {
URI uri = new URI(string);
try {
//if we can load the repository everything is fine
repositoryManager.loadRepository(uri, null);
return Optional.of(uri);
} catch (ProvisionException e) {
debug.accept("Candidate URI '" + uri + "' can not be loaded: " + e.getMessage());
return Optional.empty();
}
} catch (URISyntaxException e) {
debug.accept("Resulting candidate string '" + string + "' can not be parsed as a valid location uri: "
+ e.getMessage());
return Optional.empty();
}
}

}
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@
import java.io.Writer;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
@@ -53,14 +54,15 @@
* mvn -f [path to target project] tycho-version-bump:update-target
* </pre>
* <p>
* For updating <b>maven target locations</b> the mojo support
* For updating <b>maven target locations</b> the mojo supports
* <a href="https://www.mojohaus.org/versions/versions-maven-plugin/version-rules.html">Version
* number comparison rule-sets</a> similar to the
* <a href="https://www.mojohaus.org/versions/versions-maven-plugin">Versions Maven Plugin</a>
* please check the documentation there for further information about ruleset files.
* </p>
* <p>
* For updating <b>installable unit locations</b> (also known as update sites)
* For updating <b>installable unit locations</b> (also known as update sites) you can configure
* different strategies (see {@link #getUpdateSiteDiscovery()}) to discover updates.
* </p>
*/
@Mojo(name = "update-target")
@@ -103,13 +105,20 @@ public class UpdateTargetMojo extends AbstractUpdateMojo {
* A comma separated list of update site discovery strategies, the following is currently
* supported:
* <ul>
* <li>parent - search the parent path for a composite that can be used to find newer
* versions</li>
* <li><code>parent</code> - search the parent path for a composite that can be used to find
* newer versions</li>
* <li><code>versionPattern[:pattern]</code> - specifies a pattern to match in the URL (defaults
* to (\d+)\.(\d+) where it increments each numeric part beginning at the last group, if no
* repository is found using the next group setting the previous to zero. Any non numeric
* pattern will be replaced by the empty string</li>
* </ul>
*/
@Parameter(property = "discovery")
private String updateSiteDiscovery;

@Parameter
private List<String> updateSiteDiscoveryStrategies;

/**
* <p>
* Allows specifying a {@linkplain RuleSet} object describing rules on maven artifact versions
@@ -239,8 +248,14 @@ MojoExecution getMojoExecution() {
return mojoExecution;
}

String getUpdateSiteDiscovery() {
return updateSiteDiscovery;
List<String> getUpdateSiteDiscovery() {
if (updateSiteDiscovery == null || updateSiteDiscovery.isBlank()) {
if (updateSiteDiscoveryStrategies != null) {
return updateSiteDiscoveryStrategies;
}
return List.of();
}
return Arrays.stream(updateSiteDiscovery.split(",")).map(String::trim).toList();
}

Set<String> getMavenIgnoredVersions() {
Original file line number Diff line number Diff line change
@@ -18,6 +18,8 @@
<exportedPackage>org.eclipse.equinox.p2.publisher</exportedPackage>
<exportedPackage>org.eclipse.equinox.p2.publisher.eclipse</exportedPackage>
<exportedPackage>org.eclipse.equinox.p2.query</exportedPackage>
<!-- This currently produces problems with the PGP signing mojo failing to load org.bouncycastle.openpgp.PGPPublicKey (we need to probably export this as well) -->
<!-- <exportedPackage>org.eclipse.equinox.p2.repository</exportedPackage> -->
<exportedPackage>org.eclipse.equinox.p2.repository.artifact</exportedPackage>
<exportedPackage>org.eclipse.equinox.p2.repository.metadata</exportedPackage>
<exportedPackage>org.eclipse.equinox.internal.p2.metadata</exportedPackage>

0 comments on commit 0f2b5c3

Please sign in to comment.