blob: 452551892bca565ed9e20bf71b2c13f58b94c1d7 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2011, 2018 Sonatype Inc. and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Sonatype Inc. - initial API and implementation
* Bachmann GmbH. - Bug 538395 Generate valid feature xml
*******************************************************************************/
package org.eclipse.tycho.extras.sourcefeature;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import org.apache.maven.archiver.MavenArchiveConfiguration;
import org.apache.maven.archiver.MavenArchiver;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper;
import org.codehaus.plexus.archiver.Archiver;
import org.codehaus.plexus.archiver.jar.JarArchiver;
import org.codehaus.plexus.archiver.util.DefaultFileSet;
import org.codehaus.plexus.configuration.PlexusConfiguration;
import org.codehaus.plexus.logging.Logger;
import org.eclipse.sisu.equinox.EquinoxServiceFactory;
import org.eclipse.tycho.PackagingType;
import org.eclipse.tycho.artifacts.TargetPlatform;
import org.eclipse.tycho.core.osgitools.DebugUtils;
import org.eclipse.tycho.core.utils.TychoProjectUtils;
import org.eclipse.tycho.model.Feature;
import org.eclipse.tycho.model.FeatureRef;
import org.eclipse.tycho.model.PluginRef;
import org.eclipse.tycho.osgi.adapters.MavenLoggerAdapter;
import org.eclipse.tycho.p2.resolver.facade.P2ResolutionResult;
import org.eclipse.tycho.p2.resolver.facade.P2ResolutionResult.Entry;
import org.eclipse.tycho.p2.resolver.facade.P2Resolver;
import org.eclipse.tycho.p2.resolver.facade.P2ResolverFactory;
import org.eclipse.tycho.packaging.LicenseFeatureHelper;
import de.pdark.decentxml.Document;
import de.pdark.decentxml.Element;
/**
* Generates a source feature for projects of packaging type <code>eclipse-feature</code>. By
* default, the generated source feature
* <ul>
* <li>Declares feature id <code>&lt;originalFeatureId&gt;.source</code></li>
* <li>Declares feature label <code>"&lt;originalFeatureLabel&gt; Developer Resources"</code></li>
* <li>If present, reuses feature provider, description, copyright and license as well as respective
* URLs from &lt;originalFeature&gt;</li>
* <li>Includes all plugins included by &lt;originalFeature&gt;, but each with <code>.source</code>
* appended to each plugin id</li>
* <li>Includes all features included by &lt;originalFeature&gt;, but each with <code>.source</code>
* appended to each feature id</li>
* <li>Includes the original feature. This ensures that binaries and corresponding sources
* match.</li>
* </ul>
*
* Source feature generation can be customized by adding files under path
* <code>sourceTemplateFeature/</code>. Files added here will be added to the root of the source
* feature jar. Especially, if file <code>sourceTemplateFeature/feature.properties</code> is found,
* values in this file override values of respective keys in
* <code>&lt;originalFeature&gt;/feature.properties</code>.
*
*/
@Mojo(name = "source-feature", defaultPhase = LifecyclePhase.PACKAGE)
public class SourceFeatureMojo extends AbstractMojo {
public static final String FEATURE_TEMPLATE_DIR = "sourceTemplateFeature";
public static final String SOURCES_FEATURE_CLASSIFIER = "sources-feature";
private static final String FEATURE_PROPERTIES = "feature.properties";
private static final String GEN_DIR = "sources-feature";
@Parameter(property = "project", readonly = true)
private MavenProject project;
/**
* Whether to skip source feature generation.
*/
@Parameter(defaultValue = "false")
private boolean skip;
/**
* Whether to add an include dependency from the source feature to the corresponding binary
* feature. If <code>true</code>, this ensures the version of the installed sources matches the
* binaries.
*/
@Parameter(defaultValue = "true")
private boolean includeBinaryFeature;
/**
* Source feature label suffix. Unless explicitly provided in
* <code>sourceTemplateFeature/feature.properties</code>, this suffix will be appended to the
* original feature label to construct the source feature label.
*/
@Parameter(defaultValue = " Developer Resources")
private String labelSuffix;
/**
* Use this to explicitly set the <a href=
* "http://help.eclipse.org/juno/index.jsp?topic=%2Forg.eclipse.pde.doc.user%2Fguide%2Ftools%2Feditors%2Ffeature_editor%2Ffeature_editor.htm"
* > branding plugin attribute</a> of the generated source feature (overrides
* {@link #reuseBrandingPlugin}).
*/
@Parameter
private String brandingPlugin;
/**
* Whether to reuse an explicit branding plugin from the binary feature for the generated source
* feature.
*/
@Parameter(defaultValue = "true")
private boolean reuseBrandingPlugin;
/**
* Bundles and features that do not have corresponding sources. Example:
*
* <pre>
* &lt;excludes&gt;
* &lt;plugin id="plugin.nosource"/&gt;
* &lt;feature id="feature.nosource"/&gt;
* &lt;/excludes&gt;
* </pre>
*
*/
@Parameter
private PlexusConfiguration excludes;
/**
* Additional plugins to include in the generated source feature. Beware that these additional
* plugins are not considered during build target platform calculation and ordering of reactor
* projects. Use &lt;extraRequirements&gt; dependency resolver configuration to guarantee proper
* reactor build order.
* <p>
* <strong>WARNING</strong> This experimental parameter may be removed from future
* source-feature mojo versions without prior notice.
*
*/
@Parameter
private PlexusConfiguration plugins;
@Parameter(property = "session", readonly = true)
private MavenSession session;
private final Set<String> excludedPlugins = new HashSet<>();
private final Set<String> excludedFeatures = new HashSet<>();
private final Set<PluginRef> extraPlugins = new HashSet<>();
@Parameter
private MavenArchiveConfiguration archive = new MavenArchiveConfiguration();
/**
* The filename to be used for the generated archive file. For the source-feature goal,
* "-sources-feature" is appended to this filename.
*/
@Parameter(property = "project.build.finalName")
private String finalName;
@Component(role = Archiver.class, hint = "jar")
private JarArchiver jarArchiver;
@Component
private MavenProjectHelper projectHelper;
@Component
private LicenseFeatureHelper licenseFeatureHelper;
@Component
private EquinoxServiceFactory equinox;
@Component
private Logger logger;
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
if (!PackagingType.TYPE_ECLIPSE_FEATURE.equals(project.getPackaging()) || skip) {
return;
}
try {
Properties sourceFeatureTemplateProps = readSourceTemplateFeatureProperties();
Properties mergedSourceFeatureProps = mergeFeatureProperties(sourceFeatureTemplateProps);
File sourceFeatureXml = generateSourceFeatureXml(mergedSourceFeatureProps, sourceFeatureTemplateProps);
writeProperties(mergedSourceFeatureProps, getMergedSourceFeaturePropertiesFile());
MavenArchiver archiver = new MavenArchiver();
archiver.setArchiver(jarArchiver);
File outputJarFile = getOutputJarFile();
archiver.setOutputFile(outputJarFile);
File template = new File(project.getBasedir(), FEATURE_TEMPLATE_DIR);
if (template.isDirectory()) {
DefaultFileSet templateFileSet = new DefaultFileSet();
templateFileSet.setDirectory(template);
// make sure we use generated feature.xml and feature.properties
templateFileSet.setExcludes(new String[] { Feature.FEATURE_XML, FEATURE_PROPERTIES });
archiver.getArchiver().addFileSet(templateFileSet);
}
archiver.getArchiver().addFile(sourceFeatureXml, Feature.FEATURE_XML);
archiver.getArchiver().addFile(getMergedSourceFeaturePropertiesFile(), FEATURE_PROPERTIES);
File licenseFeature = licenseFeatureHelper
.getLicenseFeature(Feature.read(new File(project.getBasedir(), "feature.xml")), project);
if (licenseFeature != null) {
archiver.getArchiver()
.addArchivedFileSet(licenseFeatureHelper.getLicenseFeatureFileSet(licenseFeature));
}
archiver.createArchive(session, project, archive);
projectHelper.attachArtifact(project, outputJarFile, SOURCES_FEATURE_CLASSIFIER);
} catch (MojoExecutionException e) {
throw e;
} catch (Exception e) {
throw new MojoExecutionException("Could not package source feature jar", e);
}
}
static File getSourcesFeatureOutputDir(MavenProject project) {
File dir = new File(project.getBuild().getDirectory(), GEN_DIR);
dir.mkdirs();
// TODO why is this needed?
new File(dir, "p2.inf").delete();
return dir;
}
private Properties mergeFeatureProperties(Properties sourceFeatureTemplateProps) throws IOException {
Properties generatedOriginalFeatureProps = readPropertiesIfExists(
new File(project.getBuild().getDirectory(), FEATURE_PROPERTIES));
Properties mergedProperties = new Properties();
mergedProperties.putAll(generatedOriginalFeatureProps);
mergedProperties.putAll(sourceFeatureTemplateProps);
return mergedProperties;
}
private Properties readSourceTemplateFeatureProperties() throws IOException {
return readPropertiesIfExists(new File(project.getBasedir(), FEATURE_TEMPLATE_DIR + "/" + FEATURE_PROPERTIES));
}
private File generateSourceFeatureXml(Properties mergedSourceFeatureProps, Properties sourceTemplateProps)
throws IOException, MojoExecutionException {
File sourceFeatureXml = new File(getSourcesFeatureOutputDir(project), Feature.FEATURE_XML);
Feature feature = Feature.read(new File(this.project.getBuild().getDirectory(), "feature.xml"));
final Feature sourceFeature = createSourceFeatureSkeleton(feature, mergedSourceFeatureProps,
sourceTemplateProps);
fillReferences(sourceFeature, feature, TychoProjectUtils.getTargetPlatform(project));
Feature.write(sourceFeature, sourceFeatureXml, " ");
return sourceFeatureXml;
}
private File getMergedSourceFeaturePropertiesFile() {
return new File(getSourcesFeatureOutputDir(project), FEATURE_PROPERTIES);
}
private static Properties readPropertiesIfExists(File propertiesFile) throws IOException {
Properties properties = new Properties();
if (propertiesFile.isFile()) {
try (FileInputStream propertiesStream = new FileInputStream(propertiesFile)) {
properties.load(propertiesStream);
}
}
return properties;
}
private static void writeProperties(Properties props, File propertiesFile) throws IOException {
propertiesFile.getParentFile().mkdirs();
try (FileOutputStream out = new FileOutputStream(propertiesFile)) {
props.store(out, "");
}
}
/**
* This only create the new feature skeleton by setting labels and other not-structural values
* that don't require platform resolution.
*/
Feature createSourceFeatureSkeleton(Feature feature, Properties mergedFeatureProperties,
Properties sourceTemplateProperties) throws IOException, MojoExecutionException {
Document document = new Document();
document.setEncoding("UTF-8");
document.setRootNode(new Element("feature"));
Feature sourceFeature = new Feature(document);
sourceFeature.setId(feature.getId() + ".source");
sourceFeature.setVersion(feature.getVersion());
if (reuseBrandingPlugin && brandingPlugin == null) {
if (feature.getBrandingPluginId() != null) {
sourceFeature.setBrandingPluginId(feature.getBrandingPluginId());
}
} else if (brandingPlugin != null) {
sourceFeature.setBrandingPluginId(brandingPlugin);
}
if (feature.getLabel() != null) {
String originalLabel = feature.getLabel();
if (originalLabel.startsWith("%")) {
sourceFeature.setLabel(validateValue(originalLabel, mergedFeatureProperties));
String labelKey = originalLabel.substring(1);
if (sourceTemplateProperties.getProperty(labelKey) == null) {
mergedFeatureProperties.setProperty(labelKey,
mergedFeatureProperties.getProperty(labelKey) + labelSuffix);
} else {
// keep source template value
}
} else {
sourceFeature.setLabel(originalLabel + labelSuffix);
}
}
if (feature.getProvider() != null) {
sourceFeature.setProvider(validateValue(feature.getProvider(), mergedFeatureProperties));
}
if (feature.getDescription() != null) {
sourceFeature.setDescription(validateValue(feature.getDescription(), mergedFeatureProperties));
}
if (feature.getDescriptionURL() != null) {
sourceFeature.setDescriptionURL(validateValue(feature.getDescriptionURL(), mergedFeatureProperties));
}
if (feature.getCopyright() != null) {
sourceFeature.setCopyright(validateValue(feature.getCopyright(), mergedFeatureProperties));
}
if (feature.getCopyrightURL() != null) {
sourceFeature.setCopyrightURL(validateValue(feature.getCopyrightURL(), mergedFeatureProperties));
}
if (feature.getLicense() != null) {
sourceFeature.setLicense(validateValue(feature.getLicense(), mergedFeatureProperties));
}
if (feature.getLicenseURL() != null) {
sourceFeature.setLicenseURL(validateValue(feature.getLicenseURL(), mergedFeatureProperties));
}
if (includeBinaryFeature) {
FeatureRef binaryRef = new FeatureRef(new Element("includes"));
binaryRef.setId(feature.getId());
binaryRef.setVersion(feature.getVersion());
if (feature.getOS() != null) {
binaryRef.setOS(feature.getOS());
}
if (feature.getWS() != null) {
binaryRef.setWS(feature.getWS());
}
if (feature.getArch() != null) {
binaryRef.setArch(feature.getArch());
}
sourceFeature.addFeatureRef(binaryRef);
}
return sourceFeature;
}
/**
* Returns the value for a field. In case the value is a reference to feature.properties, verify
* that the entry exist in the feature.properties file for source
*
* @param fieldValue
* @param sourceFeatureProperties
* @return
*/
private static String validateValue(String fieldValue, Properties sourceFeatureProperties)
throws MojoExecutionException {
if (fieldValue.startsWith("%")) {
String key = fieldValue.substring(1);
if (!sourceFeatureProperties.containsKey(key)) {
throw new MojoExecutionException("Source feature depends on '" + FEATURE_TEMPLATE_DIR
+ "/feature.properties', entry '" + key + "'. However, this key could not be found");
}
}
return fieldValue;
}
/**
* Added all references to sourceFeature, as deduced by feature and resolved by targetPlatform
*
* @param sourceFeature
* @param feature
* @param targetPlatform
* @throws MojoExecutionException
*/
private void fillReferences(Feature sourceFeature, Feature feature, TargetPlatform targetPlatform)
throws MojoExecutionException {
P2ResolverFactory factory = this.equinox.getService(P2ResolverFactory.class);
P2Resolver p2 = factory.createResolver(
new MavenLoggerAdapter(this.logger, DebugUtils.isDebugEnabled(this.session, this.project)));
List<PluginRef> missingSourcePlugins = new ArrayList<>();
List<FeatureRef> missingSourceFeatures = new ArrayList<>();
List<PluginRef> missingExtraPlugins = new ArrayList<>();
// include available source features
for (FeatureRef featureRef : feature.getIncludedFeatures()) {
if (excludedFeatures.contains(featureRef.getId())) {
continue;
}
String sourceId = featureRef.getId() + ".source";
// TODO 412416 either directly work on IUs (-> omit the "toResolutionResult" conversion), or ask for the Tycho artifact type ArtifactKey.TYPE_ECLIPSE_PLUGIN
P2ResolutionResult result = p2.resolveInstallableUnit(targetPlatform, sourceId + ".feature.jar",
toStrictVersionRange(featureRef.getVersion()));
if (result.getArtifacts().size() == 1) {
Entry entry = result.getArtifacts().iterator().next();
FeatureRef sourceRef = new FeatureRef(new Element("includes"));
sourceRef.setId(sourceId);
sourceRef.setVersion(entry.getVersion());
sourceFeature.addFeatureRef(sourceRef);
} else {
missingSourceFeatures.add(featureRef);
}
}
// include available source bundles
for (PluginRef pluginRef : feature.getPlugins()) {
if (excludedPlugins.contains(pluginRef.getId())) {
continue;
}
// version is expected to be fully expanded at this point
P2ResolutionResult result = p2.resolveInstallableUnit(targetPlatform, pluginRef.getId() + ".source",
toStrictVersionRange(pluginRef.getVersion()));
if (result.getArtifacts().size() == 1) {
addPlugin(sourceFeature, result, pluginRef);
} else {
missingSourcePlugins.add(pluginRef);
}
}
for (PluginRef pluginRef : extraPlugins) {
// version is expected to be fully expanded at this point
P2ResolutionResult result = p2.resolveInstallableUnit(targetPlatform, pluginRef.getId(),
pluginRef.getVersion());
if (result.getArtifacts().size() == 1) {
addPlugin(sourceFeature, result, pluginRef);
} else {
missingExtraPlugins.add(pluginRef);
}
}
if (!missingSourceFeatures.isEmpty() || !missingSourcePlugins.isEmpty() || !missingExtraPlugins.isEmpty()) {
StringBuilder sb = new StringBuilder();
sb.append("Could not generate source feature for project " + project.toString()).append("\n");
if (!missingSourcePlugins.isEmpty()) {
sb.append(" Missing sources for plugins " + missingSourcePlugins.toString()).append("\n");
}
if (!missingSourceFeatures.isEmpty()) {
sb.append(" Missing sources for features " + missingSourceFeatures.toString()).append("\n");
}
if (!missingExtraPlugins.isEmpty()) {
sb.append(" Missing extra plugins " + missingExtraPlugins.toString()).append("\n");
}
throw new MojoExecutionException(sb.toString());
}
}
protected String toStrictVersionRange(String version) {
return "[" + version + "," + version + "]";
}
protected void addPlugin(Feature sourceFeature, P2ResolutionResult result, PluginRef pluginRef) {
Entry sourceBundle = result.getArtifacts().iterator().next();
PluginRef sourceRef = new PluginRef("plugin");
sourceRef.setId(sourceBundle.getId());
sourceRef.setVersion(sourceBundle.getVersion());
sourceRef.setDownloadSize(0);
sourceRef.setInstallSize(0);
if (pluginRef.getOs() != null) {
sourceRef.setOs(pluginRef.getOs());
}
if (pluginRef.getWs() != null) {
sourceRef.setWs(pluginRef.getWs());
}
if (pluginRef.getArch() != null) {
sourceRef.setArch(pluginRef.getArch());
}
sourceRef.setUnpack(false);
sourceFeature.addPlugin(sourceRef);
}
protected File getOutputJarFile() {
String filename = finalName + "-" + SOURCES_FEATURE_CLASSIFIER + ".jar";
return new File(project.getBuild().getDirectory(), filename);
}
// this is called by maven to inject value of <excludes> configuration element
public void setExcludes(PlexusConfiguration excludes) {
for (PlexusConfiguration plugin : excludes.getChildren("plugin")) {
String id = getAttribute(plugin, "id");
if (id != null) {
excludedPlugins.add(id);
}
// TODO warn about elements with null id
}
for (PlexusConfiguration plugin : excludes.getChildren("feature")) {
String id = getAttribute(plugin, "id");
if (id != null) {
excludedFeatures.add(id);
}
// TODO warn about elements with null id
}
}
// this is called by maven to inject value of <excludes> configuration element
public void setPlugins(PlexusConfiguration bundles) {
for (PlexusConfiguration plugin : bundles.getChildren("plugin")) {
String id = getAttribute(plugin, "id");
if (id != null) {
String version = getAttribute(plugin, "version");
if (version == null) {
version = "0.0.0";
}
PluginRef ref = new PluginRef("plugin");
ref.setId(id);
ref.setVersion(version);
extraPlugins.add(ref);
}
// TODO fail if duplicate plugins
// TODO warn about elements with null id
}
}
private String getAttribute(PlexusConfiguration dom, String attrName) {
String attr = dom.getAttribute(attrName);
if (attr == null) {
return null;
}
attr = attr.trim();
if (attr.isEmpty()) {
return null;
}
return attr;
}
}