From 33f30bd358b44bf5d145a8e030a155310cceab0c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?=
Date: Sun, 10 Mar 2024 12:34:37 +0100
Subject: [PATCH] Add a timestamp provider that inherits the timestamp from the
parent
In some special setups (e.g. if fragment version should always match
host version) it could be useful to inherit the build timestamp from the
fragemnt host.
This adds a new timestamp provider to allow this use-case by specify
fragment-host.
---
.../targetplatform/ArtifactCollection.java | 10 +-
.../BuildQualifierAggregatorMojo.java | 163 +++++++-----------
.../buildversion/BuildQualifierMojo.java | 8 +-
.../FragmentHostBuildTimestampProvider.java | 100 +++++++++++
.../tycho/buildversion/TimestampFinder.java | 44 +++++
.../helper/PluginConfigurationHelper.java | 56 ++++--
6 files changed, 265 insertions(+), 116 deletions(-)
create mode 100644 tycho-packaging-plugin/src/main/java/org/eclipse/tycho/buildversion/FragmentHostBuildTimestampProvider.java
diff --git a/tycho-core/src/main/java/org/eclipse/tycho/core/osgitools/targetplatform/ArtifactCollection.java b/tycho-core/src/main/java/org/eclipse/tycho/core/osgitools/targetplatform/ArtifactCollection.java
index 80b1aae2d6..7e2ef1df5a 100644
--- a/tycho-core/src/main/java/org/eclipse/tycho/core/osgitools/targetplatform/ArtifactCollection.java
+++ b/tycho-core/src/main/java/org/eclipse/tycho/core/osgitools/targetplatform/ArtifactCollection.java
@@ -40,6 +40,7 @@
import org.eclipse.tycho.ReactorProject;
import org.eclipse.tycho.core.osgitools.DefaultArtifactDescriptor;
import org.osgi.framework.Version;
+import org.osgi.framework.VersionRange;
public class ArtifactCollection {
private static final Version VERSION_0_0_0 = new Version("0.0.0");
@@ -249,7 +250,14 @@ public ArtifactDescriptor getArtifact(String type, String id, String version) {
if (version == null) {
return relevantArtifacts.get(relevantArtifacts.firstKey()); // latest version
}
-
+ if (version.startsWith("(") || version.startsWith("[")) {
+ VersionRange range = VersionRange.valueOf(version);
+ for (Entry entry : relevantArtifacts.entrySet()) {
+ if (range.includes(entry.getKey())) {
+ return entry.getValue();
+ }
+ }
+ }
Version parsedVersion = new Version(version);
if (VERSION_0_0_0.equals(parsedVersion)) {
return relevantArtifacts.get(relevantArtifacts.firstKey()); // latest version
diff --git a/tycho-packaging-plugin/src/main/java/org/eclipse/tycho/buildversion/BuildQualifierAggregatorMojo.java b/tycho-packaging-plugin/src/main/java/org/eclipse/tycho/buildversion/BuildQualifierAggregatorMojo.java
index 66cf3be4cc..1a5a0281fa 100644
--- a/tycho-packaging-plugin/src/main/java/org/eclipse/tycho/buildversion/BuildQualifierAggregatorMojo.java
+++ b/tycho-packaging-plugin/src/main/java/org/eclipse/tycho/buildversion/BuildQualifierAggregatorMojo.java
@@ -12,8 +12,6 @@
*******************************************************************************/
package org.eclipse.tycho.buildversion;
-import java.text.ParsePosition;
-import java.text.SimpleDateFormat;
import java.util.Date;
import org.apache.maven.plugin.MojoExecutionException;
@@ -28,124 +26,93 @@
import org.eclipse.tycho.core.PluginDescription;
import org.eclipse.tycho.core.TychoProject;
import org.eclipse.tycho.core.osgitools.DefaultReactorProject;
-import org.osgi.framework.Version;
/**
*
- * This mojo calculates build timestamp as the latest timestamp of the project itself and timestamps
- * of bundles and features directly included in the project. This is meant to work with custom
- * timestamp providers and generate build qualifier based on build contents, i.e. the source code,
- * and not the time the build was started; rebuilding the same source code will result in the same
- * version qualifier.
+ * This mojo calculates build timestamp as the latest timestamp of the project
+ * itself and timestamps of bundles and features directly included in the
+ * project. This is meant to work with custom timestamp providers and generate
+ * build qualifier based on build contents, i.e. the source code, and not the
+ * time the build was started; rebuilding the same source code will result in
+ * the same version qualifier.
*
*
- * Timestamp of included bundles and features is determined by parsing their respective version
- * qualifiers. Qualifiers that cannot be parsed are silently ignored, which can result in old
- * version qualifier used even when aggregator project contents actually changed. In this case
- * aggregator project timestamp will have to be increased manually, using artificial SCM commit for
- * example.
+ * Timestamp of included bundles and features is determined by parsing their
+ * respective version qualifiers. Qualifiers that cannot be parsed are silently
+ * ignored, which can result in old version qualifier used even when aggregator
+ * project contents actually changed. In this case aggregator project timestamp
+ * will have to be increased manually, using artificial SCM commit for example.
*
*
- * Qualifier aggregation is enabled only for projects with custom timestamp provider, i.e.
- * <timestampProvider> is set in pom.xml to a value other than "default". The default build
- * timestamp provider uses build start time as build timestamp, which should be newer or equal than
- * timestamp of any included bundle/feature project, which makes qualifier aggregation redundant.
+ * Qualifier aggregation is enabled only for projects with custom timestamp
+ * provider, i.e. <timestampProvider> is set in pom.xml to a value other
+ * than "default". The default build timestamp provider uses build start time as
+ * build timestamp, which should be newer or equal than timestamp of any
+ * included bundle/feature project, which makes qualifier aggregation redundant.
*
*/
@Mojo(name = "build-qualifier-aggregator", defaultPhase = LifecyclePhase.VALIDATE, threadSafe = true)
public class BuildQualifierAggregatorMojo extends BuildQualifierMojo {
- private final TimestampFinder timestampFinder = new TimestampFinder();
+ @Component
+ private TimestampFinder timestampFinder;
@Component
private TargetPlatformService platformService;
- @Override
- protected Date getBuildTimestamp() throws MojoExecutionException {
- Date timestamp = super.getBuildTimestamp();
+ @Override
+ protected Date getBuildTimestamp() throws MojoExecutionException {
+ Date timestamp = super.getBuildTimestamp();
- if (timestampProvider == null) {
- // default timestamp is essentially this build start time
- // no included bundle/feature can have more recent timestamp
- return timestamp;
- }
+ if (timestampProvider == null) {
+ // default timestamp is essentially this build start time
+ // no included bundle/feature can have more recent timestamp
+ return timestamp;
+ }
- final Date[] latestTimestamp = new Date[] { timestamp };
+ final Date[] latestTimestamp = new Date[] { timestamp };
- TychoProject projectType = projectTypes.get(project.getPackaging());
- if (projectType == null) {
- throw new IllegalStateException("Unknown or unsupported packaging type " + packaging);
- }
+ TychoProject projectType = projectTypes.get(project.getPackaging());
+ if (projectType == null) {
+ throw new IllegalStateException("Unknown or unsupported packaging type " + packaging);
+ }
- final ReactorProject thisProject = DefaultReactorProject.adapt(project);
+ final ReactorProject thisProject = DefaultReactorProject.adapt(project);
// TODO we need to trigger TP loading now, probably we also should better use
// the target platform instead of walker of the project type?
platformService.getTargetPlatform(thisProject);
- projectType.getDependencyWalker(thisProject).walk(new ArtifactDependencyVisitor() {
- @Override
- public boolean visitFeature(FeatureDescription feature) {
- if (feature.getFeatureRef() == null || thisProject.equals(feature.getMavenProject())) {
- // 'this' feature
- return true; // visit immediately included features
- }
- visitArtifact(feature);
- return false; // do not visit indirectly included features/bundles
- }
-
- @Override
- public void visitPlugin(PluginDescription plugin) {
- if (plugin.getPluginRef() == null || thisProject.equals(plugin.getMavenProject())) {
- // 'this' bundle
- return;
- }
- visitArtifact(plugin);
- }
-
- private void visitArtifact(ArtifactDescriptor artifact) {
- ReactorProject otherProject = artifact.getMavenProject();
- String otherVersion = (otherProject != null) ? otherProject.getExpandedVersion()
- : artifact.getKey().getVersion();
- Version v = Version.parseVersion(otherVersion);
- String otherQualifier = v.getQualifier();
- if (otherQualifier != null) {
- Date timestamp = parseQualifier(otherQualifier);
- if (timestamp != null) {
- if (latestTimestamp[0].compareTo(timestamp) < 0) {
- if (getLog().isDebugEnabled()) {
- getLog().debug("Found '" + format.format(timestamp) + "' from qualifier '"
- + otherQualifier + "' for artifact " + artifact);
- }
- latestTimestamp[0] = timestamp;
- }
- } else {
- getLog().debug("Could not parse qualifier timestamp " + otherQualifier);
- }
- }
- }
-
- private Date parseQualifier(String qualifier) {
- Date timestamp = parseQualifier(qualifier, format);
- if (timestamp != null) {
- return timestamp;
- }
- return discoverTimestamp(qualifier);
- }
-
- private Date parseQualifier(String qualifier, SimpleDateFormat format) {
- ParsePosition pos = new ParsePosition(0);
- Date timestamp = format.parse(qualifier, pos);
- if (timestamp != null && pos.getIndex() == qualifier.length()) {
- return timestamp;
- }
- return null;
- }
-
- private Date discoverTimestamp(String qualifier) {
- return timestampFinder.findInString(qualifier);
- }
- });
-
- return latestTimestamp[0];
- }
+ projectType.getDependencyWalker(thisProject).walk(new ArtifactDependencyVisitor() {
+ @Override
+ public boolean visitFeature(FeatureDescription feature) {
+ if (feature.getFeatureRef() == null || thisProject.equals(feature.getMavenProject())) {
+ // 'this' feature
+ return true; // visit immediately included features
+ }
+ visitArtifact(feature);
+ return false; // do not visit indirectly included features/bundles
+ }
+
+ @Override
+ public void visitPlugin(PluginDescription plugin) {
+ if (plugin.getPluginRef() == null || thisProject.equals(plugin.getMavenProject())) {
+ // 'this' bundle
+ return;
+ }
+ visitArtifact(plugin);
+ }
+
+ private void visitArtifact(ArtifactDescriptor artifact) {
+
+ Date timestamp = timestampFinder.findByDescriptor(artifact, format);
+ if (timestamp != null && latestTimestamp[0].compareTo(timestamp) < 0) {
+ latestTimestamp[0] = timestamp;
+ }
+
+ }
+
+ });
+
+ return latestTimestamp[0];
+ }
}
diff --git a/tycho-packaging-plugin/src/main/java/org/eclipse/tycho/buildversion/BuildQualifierMojo.java b/tycho-packaging-plugin/src/main/java/org/eclipse/tycho/buildversion/BuildQualifierMojo.java
index 5c76d1df3f..92737c7806 100644
--- a/tycho-packaging-plugin/src/main/java/org/eclipse/tycho/buildversion/BuildQualifierMojo.java
+++ b/tycho-packaging-plugin/src/main/java/org/eclipse/tycho/buildversion/BuildQualifierMojo.java
@@ -90,7 +90,11 @@
@Mojo(name = "build-qualifier", defaultPhase = LifecyclePhase.VALIDATE, threadSafe = true)
public class BuildQualifierMojo extends AbstractVersionMojo {
- @Parameter(property = "session", readonly = true)
+ static final String PARAMETER_FORMAT = "format";
+
+ static final String DEFAULT_DATE_FORMAT = "yyyyMMddHHmm";
+
+ @Parameter(property = "session", readonly = true)
protected MavenSession session;
/**
@@ -98,7 +102,7 @@ public class BuildQualifierMojo extends AbstractVersionMojo {
* Specify a date format as specified by java.text.SimpleDateFormat. Timezone used is UTC.
*
*/
- @Parameter(defaultValue = "yyyyMMddHHmm", property = "tycho.buildqualifier.format")
+ @Parameter(name = PARAMETER_FORMAT, defaultValue = DEFAULT_DATE_FORMAT, property = "tycho.buildqualifier.format")
protected SimpleDateFormat format;
@Parameter(property = "forceContextQualifier")
diff --git a/tycho-packaging-plugin/src/main/java/org/eclipse/tycho/buildversion/FragmentHostBuildTimestampProvider.java b/tycho-packaging-plugin/src/main/java/org/eclipse/tycho/buildversion/FragmentHostBuildTimestampProvider.java
new file mode 100644
index 0000000000..e83a11f107
--- /dev/null
+++ b/tycho-packaging-plugin/src/main/java/org/eclipse/tycho/buildversion/FragmentHostBuildTimestampProvider.java
@@ -0,0 +1,100 @@
+/*******************************************************************************
+ * Copyright (c) 20024 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.buildversion;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Optional;
+
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.plugin.MojoExecution;
+import org.apache.maven.project.MavenProject;
+import org.codehaus.plexus.component.annotations.Component;
+import org.codehaus.plexus.component.annotations.Requirement;
+import org.codehaus.plexus.logging.Logger;
+import org.eclipse.osgi.util.ManifestElement;
+import org.eclipse.tycho.ArtifactDescriptor;
+import org.eclipse.tycho.ArtifactType;
+import org.eclipse.tycho.DependencyArtifacts;
+import org.eclipse.tycho.build.BuildTimestampProvider;
+import org.eclipse.tycho.core.BundleProject;
+import org.eclipse.tycho.core.TychoProject;
+import org.eclipse.tycho.core.TychoProjectManager;
+import org.eclipse.tycho.helper.PluginConfigurationHelper;
+import org.eclipse.tycho.helper.PluginConfigurationHelper.Configuration;
+import org.osgi.framework.Constants;
+
+/**
+ * Build timestamp provider that inherits the timestamp of the fragment host
+ */
+@Component(role = BuildTimestampProvider.class, hint = FragmentHostBuildTimestampProvider.ROLE_HINT)
+public class FragmentHostBuildTimestampProvider implements BuildTimestampProvider {
+
+ static final String ROLE_HINT = "fragment-host";
+
+ @Requirement
+ private TychoProjectManager projectManager;
+
+ @Requirement
+ private Logger logger;
+
+ @Requirement
+ private TimestampFinder timestampFinder;
+
+ @Requirement
+ private PluginConfigurationHelper configurationHelper;
+
+ @Override
+ public Date getTimestamp(MavenSession session, MavenProject project, MojoExecution execution) {
+ Optional tychoProject = projectManager.getTychoProject(project);
+ Exception exception = null;
+ if (tychoProject.isPresent()) {
+ if (tychoProject.get() instanceof BundleProject bundle) {
+ String fragmentHost = bundle.getManifestValue(Constants.FRAGMENT_HOST, project);
+ if (fragmentHost != null) {
+ try {
+ ManifestElement[] header = ManifestElement.parseHeader(Constants.FRAGMENT_HOST, fragmentHost);
+ for (ManifestElement element : header) {
+ DependencyArtifacts dependencyArtifacts = projectManager.getDependencyArtifacts(project)
+ .get();
+ ArtifactDescriptor descriptor = dependencyArtifacts.getArtifact(
+ ArtifactType.TYPE_ECLIPSE_PLUGIN, element.getValue(),
+ element.getAttribute(Constants.BUNDLE_VERSION_ATTRIBUTE));
+ if (descriptor != null) {
+
+ Configuration configuration = configurationHelper.getConfiguration();
+ Optional formatString = configuration
+ .getString(BuildQualifierMojo.PARAMETER_FORMAT);
+ SimpleDateFormat format = formatString.map(SimpleDateFormat::new)
+ .orElseGet(() -> new SimpleDateFormat(BuildQualifierMojo.DEFAULT_DATE_FORMAT));
+ Date date = timestampFinder.findByDescriptor(descriptor, format);
+ if (date != null) {
+ return date;
+ }
+ }
+ }
+ } catch (Exception e) {
+ exception = e;
+ }
+ }
+ }
+ }
+ logger.warn("Can't determine fragment host, fallback to default.", exception);
+ return session.getStartTime();
+ }
+
+ @Override
+ public void setQuiet(boolean quiet) {
+
+ }
+}
diff --git a/tycho-packaging-plugin/src/main/java/org/eclipse/tycho/buildversion/TimestampFinder.java b/tycho-packaging-plugin/src/main/java/org/eclipse/tycho/buildversion/TimestampFinder.java
index 4fca5c3543..85ad874854 100644
--- a/tycho-packaging-plugin/src/main/java/org/eclipse/tycho/buildversion/TimestampFinder.java
+++ b/tycho-packaging-plugin/src/main/java/org/eclipse/tycho/buildversion/TimestampFinder.java
@@ -22,11 +22,23 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import org.codehaus.plexus.component.annotations.Component;
+import org.codehaus.plexus.component.annotations.Requirement;
+import org.codehaus.plexus.logging.Logger;
+import org.eclipse.tycho.ArtifactDescriptor;
+import org.eclipse.tycho.ReactorProject;
+import org.osgi.framework.Version;
+
/**
* A helper for discovering common timestamps in strings
*/
+@Component(role = TimestampFinder.class)
public class TimestampFinder {
+ @Requirement
+ Logger logger;
+
+
private static Map defaultPatterns() {
Map result = new LinkedHashMap<>();
result.put(utcFormat("yyyyMMddHHmm"), Pattern.compile("([0-9]{12})"));
@@ -47,6 +59,38 @@ public TimestampFinder() {
datePatternsByRegularExpressions = defaultPatterns();
}
+ public Date findByDescriptor(ArtifactDescriptor artifact, SimpleDateFormat format) {
+ ReactorProject otherProject = artifact.getMavenProject();
+ String otherVersion = (otherProject != null) ? otherProject.getExpandedVersion()
+ : artifact.getKey().getVersion();
+ Version v = Version.parseVersion(otherVersion);
+ String otherQualifier = v.getQualifier();
+ if (otherQualifier != null) {
+ Date qualifier = parseQualifier(otherQualifier, format);
+ if (qualifier != null && logger.isDebugEnabled()) {
+ logger.debug("Found '" + format.format(qualifier) + "' from qualifier '" + otherQualifier
+ + "' for artifact " + artifact);
+ }
+ return qualifier;
+ } else {
+ logger.debug("Could not parse qualifier timestamp " + otherQualifier);
+ }
+ return null;
+ }
+
+ private Date parseQualifier(String qualifier, SimpleDateFormat format) {
+ ParsePosition pos = new ParsePosition(0);
+ Date timestamp = format.parse(qualifier, pos);
+ if (timestamp != null && pos.getIndex() == qualifier.length()) {
+ return timestamp;
+ }
+ return discoverTimestamp(qualifier);
+ }
+
+ private Date discoverTimestamp(String qualifier) {
+ return findInString(qualifier);
+ }
+
public Date findInString(String string) {
for (Entry e : datePatternsByRegularExpressions.entrySet()) {
Matcher matcher = e.getValue().matcher(string);
diff --git a/tycho-spi/src/main/java/org/eclipse/tycho/helper/PluginConfigurationHelper.java b/tycho-spi/src/main/java/org/eclipse/tycho/helper/PluginConfigurationHelper.java
index 1ffeafa52d..5978afe3fc 100644
--- a/tycho-spi/src/main/java/org/eclipse/tycho/helper/PluginConfigurationHelper.java
+++ b/tycho-spi/src/main/java/org/eclipse/tycho/helper/PluginConfigurationHelper.java
@@ -26,12 +26,15 @@
import org.apache.maven.plugin.LegacySupport;
import org.apache.maven.plugin.Mojo;
import org.apache.maven.plugin.MojoExecution;
+import org.apache.maven.plugin.PluginParameterExpressionEvaluator;
import org.apache.maven.plugin.descriptor.MojoDescriptor;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.plugin.descriptor.PluginDescriptorBuilder;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.component.annotations.Requirement;
+import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
+import org.codehaus.plexus.component.configurator.expression.TypeAwareExpressionEvaluator;
import org.codehaus.plexus.util.InterpolationFilterReader;
import org.codehaus.plexus.util.xml.Xpp3Dom;
@@ -51,21 +54,38 @@ public class PluginConfigurationHelper {
public Configuration getConfiguration() {
MojoExecution execution = MojoExecutionHelper.getExecution().orElse(null);
+ TypeAwareExpressionEvaluator evaluator = getEvaluator(legacySupport.getSession(), execution);
if (execution == null) {
- return new Configuration(null);
+ return new Configuration(null, evaluator);
}
Xpp3Dom configuration = execution.getConfiguration();
- return getConfiguration(configuration);
+ return new Configuration(configuration, evaluator);
+ }
+
+ private TypeAwareExpressionEvaluator getEvaluator(MavenSession mavenSession, MojoExecution execution) {
+ if (mavenSession == null) {
+ return null;
+ }
+ return new PluginParameterExpressionEvaluator(mavenSession, execution);
+ }
+
+ private MavenProject getProject() {
+ MavenSession session = legacySupport.getSession();
+ if (session != null) {
+ return session.getCurrentProject();
+ }
+ return null;
}
public Configuration getConfiguration(Xpp3Dom configuration) {
- return new Configuration(configuration);
+ return new Configuration(configuration, getEvaluator(legacySupport.getSession(), null));
}
public Configuration getConfiguration(String pluginGroupId, String pluginArtifactId, String goal,
MavenProject project, MavenSession mavenSession) {
return new Configuration(
- projectHelper.getPluginConfiguration(pluginGroupId, pluginArtifactId, goal, project, mavenSession));
+ projectHelper.getPluginConfiguration(pluginGroupId, pluginArtifactId, goal, project, mavenSession),
+ getEvaluator(mavenSession, null));
}
public Configuration getConfiguration(Class mojo) {
@@ -113,9 +133,11 @@ public Configuration getConfiguration(Class mojo, MavenProje
public static final class Configuration {
private Xpp3Dom configuration;
+ private TypeAwareExpressionEvaluator evaluator;
- Configuration(Xpp3Dom configuration) {
+ Configuration(Xpp3Dom configuration, TypeAwareExpressionEvaluator evaluator) {
this.configuration = configuration;
+ this.evaluator = evaluator;
}
public Optional getChild(String name) {
@@ -126,7 +148,7 @@ public Optional getChild(String name) {
if (child == null) {
return Optional.empty();
}
- return Optional.of(new Configuration(child));
+ return Optional.of(new Configuration(child, evaluator));
}
public Optional getString(String name) {
@@ -158,20 +180,24 @@ public String toString() {
public Optional> getStringList(String name) {
return getChild(name).map(child -> {
- return Arrays.stream(child.configuration.getChildren()).map(PluginConfigurationHelper::getValue)
- .toList();
+ return Arrays.stream(child.configuration.getChildren()).map(this::getValue).toList();
});
}
- }
-
- private static String getValue(Xpp3Dom dom) {
- String value = dom.getValue();
- if (value == null) {
- return dom.getAttribute("default-value");
+ private String getValue(Xpp3Dom dom) {
+ String value = dom.getValue();
+ if (value == null) {
+ value = dom.getAttribute("default-value");
+ }
+ if (value != null && evaluator != null) {
+ try {
+ return (String) evaluator.evaluate(value, String.class);
+ } catch (ExpressionEvaluationException e) {
+ }
+ }
+ return value;
}
- return value;
}
}