Skip to content

Commit

Permalink
Add a timestamp provider that inherits the timestamp from the parent
Browse files Browse the repository at this point in the history
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
<timestampProvider>fragment-host</timestampProvider>.
  • Loading branch information
laeubi committed Mar 10, 2024
1 parent 1f6d96e commit 5a981ee
Show file tree
Hide file tree
Showing 6 changed files with 265 additions and 116 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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<Version, ArtifactDescriptor> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/**
* <p>
* 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.
* </p>
* <p>
* 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.
* </p>
* <p>
* Qualifier aggregation is enabled only for projects with custom timestamp provider, i.e.
* &lt;timestampProvider&gt; 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. &lt;timestampProvider&gt; 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.
* </p>
*/
@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];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,15 +90,19 @@
@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;

/**
* <p>
* Specify a date format as specified by java.text.SimpleDateFormat. Timezone used is UTC.
* </p>
*/
@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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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> 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<String> 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) {

}
}
Loading

0 comments on commit 5a981ee

Please sign in to comment.