| /******************************************************************************* |
| * 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><originalFeatureId>.source</code></li> |
| * <li>Declares feature label <code>"<originalFeatureLabel> Developer Resources"</code></li> |
| * <li>If present, reuses feature provider, description, copyright and license as well as respective |
| * URLs from <originalFeature></li> |
| * <li>Includes all plugins included by <originalFeature>, but each with <code>.source</code> |
| * appended to each plugin id</li> |
| * <li>Includes all features included by <originalFeature>, 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><originalFeature>/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> |
| * <excludes> |
| * <plugin id="plugin.nosource"/> |
| * <feature id="feature.nosource"/> |
| * </excludes> |
| * </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 <extraRequirements> 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; |
| } |
| |
| } |