| /******************************************************************************* |
| * Copyright (c) 2006, 2021 IBM Corporation 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: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.pde.internal.build; |
| |
| import java.io.*; |
| import java.util.*; |
| import org.eclipse.core.runtime.*; |
| import org.eclipse.equinox.internal.p2.publisher.eclipse.ProductFile; |
| import org.eclipse.equinox.p2.publisher.eclipse.FeatureEntry; |
| import org.eclipse.osgi.service.resolver.BundleDescription; |
| import org.eclipse.osgi.service.resolver.ResolverError; |
| import org.eclipse.osgi.util.NLS; |
| import org.eclipse.pde.build.Constants; |
| import org.eclipse.pde.internal.build.site.*; |
| import org.osgi.framework.Filter; |
| import org.osgi.framework.Version; |
| |
| public class FeatureGenerator extends AbstractScriptGenerator { |
| |
| private static class Entry { |
| private final String id; |
| private String version = "0.0.0"; //$NON-NLS-1$ |
| private Map<String, String> attributes; |
| |
| public Entry(String id) { |
| this.id = id; |
| } |
| |
| public Entry(String id, String version) { |
| this.id = id; |
| this.version = version; |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (obj instanceof Entry) { |
| Entry objEntry = (Entry) obj; |
| if (!(id.equals(((Entry) obj).id) && version.equals(objEntry.version))) |
| return false; |
| return getAttributes().equals(objEntry.getAttributes()); |
| } |
| |
| return id.equals(obj); |
| } |
| |
| @Override |
| public int hashCode() { |
| return id.hashCode() + version.hashCode() + getAttributes().hashCode(); |
| } |
| |
| public Map<String, String> getAttributes() { |
| if (attributes != null) |
| return attributes; |
| return Collections.emptyMap(); |
| } |
| |
| public void addAttribute(String key, String value) { |
| if (VERSION.equals(key)) { |
| if (value != null && value.length() > 0) |
| version = value; |
| return; |
| } |
| if (attributes == null) |
| attributes = new LinkedHashMap<>(); |
| attributes.put(key, value); |
| } |
| |
| public String getId() { |
| return id; |
| } |
| |
| public String getVersion() { |
| return version; |
| } |
| |
| @Override |
| public String toString() { |
| return id + '_' + version; |
| } |
| } |
| |
| private String featureId = null; |
| private String version = null; |
| private String nestedInclusions = null; |
| private String productFile = null; |
| private String[] pluginList = null; |
| private String[] fragmentList = null; |
| private String[] featureList = null; |
| |
| private boolean includeLaunchers = true; |
| |
| private ProductFile product = null; |
| |
| private boolean verify = false; |
| |
| private Properties antProperties; |
| private Properties buildProperties; |
| |
| private static Set<Entry> createSet(String[] contents) { |
| if (contents == null) |
| return new LinkedHashSet<>(0); |
| Set<Entry> result = new LinkedHashSet<>(contents.length); |
| for (String content : contents) |
| if (content != null) { |
| StringTokenizer tokenizer = new StringTokenizer(content, ";"); //$NON-NLS-1$ |
| Entry entry = new Entry(tokenizer.nextToken()); |
| while (tokenizer.hasMoreTokens()) { |
| String token = tokenizer.nextToken(); |
| int idx = token.indexOf('='); |
| if (idx != -1) { |
| String value = token.substring(idx + 1, token.length()).trim(); |
| if (value.startsWith("\"") && value.endsWith("\"")) //$NON-NLS-1$ //$NON-NLS-2$ |
| value = value.substring(1, value.length() - 1); //trim off " because FeatureWriter adds them |
| entry.addAttribute(token.substring(0, idx), value); |
| } |
| } |
| result.add(entry); |
| } |
| return result; |
| } |
| |
| @Override |
| public void generate() throws CoreException { |
| AbstractScriptGenerator.setStaticAntProperties(antProperties); |
| try { |
| initialize(); |
| |
| Set<Entry> plugins = null; |
| Set<Entry> features = null; |
| Set<Entry> fragments = null; |
| if (shouldNestInclusions()) { |
| features = createSet(new String[] {generateNestedRequirements()}); |
| fragments = new LinkedHashSet<>(); |
| plugins = new LinkedHashSet<>(); |
| } else { |
| plugins = createSet(pluginList); |
| features = createSet(featureList); |
| fragments = createSet(fragmentList); |
| } |
| |
| if (product != null) { |
| List<FeatureEntry> entries = product.getProductEntries(); |
| for (FeatureEntry featureEntry : entries) { |
| Entry newEntry = new Entry(featureEntry.getId(), featureEntry.getVersion()); |
| if (featureEntry.unpackSet()) |
| newEntry.addAttribute(Utils.EXTRA_UNPACK, String.valueOf(featureEntry.isUnpack())); |
| if (featureEntry.isFragment()) |
| fragments.add(newEntry); |
| else if (featureEntry.isPlugin()) |
| plugins.add(newEntry); |
| else |
| features.add(newEntry); |
| } |
| } |
| try { |
| createFeature(featureId, plugins, fragments, features); |
| } catch (FileNotFoundException e) { |
| IStatus status = new Status(IStatus.ERROR, IPDEBuildConstants.PI_PDEBUILD, EXCEPTION_PRODUCT_FORMAT, NLS.bind(Messages.error_creatingFeature, e.getLocalizedMessage()), e); |
| throw new CoreException(status); |
| } |
| } finally { |
| AbstractScriptGenerator.setStaticAntProperties(null); |
| } |
| } |
| |
| private boolean shouldNestInclusions() { |
| if (nestedInclusions == null || nestedInclusions.equalsIgnoreCase(FALSE)) |
| return false; |
| |
| if (product != null) { |
| //will need to generate a .eclipseproduct file |
| if (buildProperties == null) |
| buildProperties = new Properties(); |
| buildProperties.put(IBuildPropertiesConstants.PROPERTY_GENERATE_ECLIPSEPRODUCT, TRUE); |
| } |
| |
| //make sure there's actually something to nest |
| if ((pluginList == null || pluginList.length == 0) && (fragmentList == null || fragmentList.length == 0) && (featureList == null || featureList.length == 0) && (buildProperties == null || buildProperties.size() == 0)) |
| return false; |
| |
| // use the product-id to generate a name if nestedRequirements==true |
| if (nestedInclusions.equalsIgnoreCase(TRUE)) |
| return product != null; |
| |
| //else nestedRequirements specifies the name to use for the nested feature |
| return true; |
| } |
| |
| private String generateNestedRequirements() throws CoreException { |
| String nestedId = null; |
| String nestedVersion = null; |
| String productKey = null; |
| if (product != null) { |
| nestedId = product.getId() + ".root.feature"; //$NON-NLS-1$ |
| nestedVersion = product.getVersion(); |
| productKey = PRODUCT_PREFIX + product.getId(); |
| if (!buildProperties.containsKey(PROPERTY_GENERATED_FEATURE_LABEL) && product.getProductName() != null) |
| buildProperties.put(PROPERTY_GENERATED_FEATURE_LABEL, product.getProductName() + " Root Files"); //$NON-NLS-1$ |
| } else { |
| nestedId = nestedInclusions; |
| nestedVersion = version != null ? version : "1.0.0.qualifier"; //$NON-NLS-1$ |
| } |
| |
| String extraRequires = null; |
| if (buildProperties != null && productKey != null) |
| extraRequires = (String) buildProperties.remove(productKey); |
| |
| FeatureGenerator generator = new FeatureGenerator(); |
| generator.setVerify(verify); |
| generator.setPluginList(pluginList); |
| generator.setFeatureList(featureList); |
| generator.setBuildProperties(buildProperties); |
| generator.setIncludeLaunchers(false); |
| generator.setBuildSiteFactory(siteFactory); |
| generator.setFeatureId(nestedId); |
| generator.setVersion(nestedVersion); |
| generator.setPluginPath(pluginPath); |
| generator.generate(); |
| |
| //get the siteFactory back from the nested generator so we don't need to recreate it |
| if (siteFactory == null) |
| siteFactory = generator.siteFactory; |
| |
| if (productKey != null) { |
| buildProperties = new Properties(); |
| extraRequires = (extraRequires == null) ? "" : extraRequires + ","; //$NON-NLS-1$ //$NON-NLS-2$ |
| extraRequires += "feature@" + nestedId + ";version=" + nestedVersion; //$NON-NLS-1$ //$NON-NLS-2$ |
| buildProperties.put(productKey, extraRequires); |
| } else { |
| buildProperties = null; |
| } |
| |
| return nestedId + ";version=" + nestedVersion; //$NON-NLS-1$ |
| } |
| |
| public void setProductFile(String productFile) { |
| this.productFile = productFile; |
| } |
| |
| public void setPluginList(String[] pluginList) { |
| this.pluginList = pluginList; |
| } |
| |
| public void setFeatureList(String[] featureList) { |
| this.featureList = featureList; |
| } |
| |
| public void setFragmentList(String[] fragmentList) { |
| this.fragmentList = fragmentList; |
| } |
| |
| public void setFeatureId(String featureId) { |
| this.featureId = featureId; |
| } |
| |
| public void setVersion(String version) { |
| this.version = version; |
| } |
| |
| public void setIncludeLaunchers(boolean includeLaunchers) { |
| this.includeLaunchers = includeLaunchers; |
| } |
| |
| private void initialize() throws CoreException { |
| //get rid of old feature that we will be overwriting, we don't want it in the state accidently. |
| File dir = new File(getWorkingDirectory(), IPDEBuildConstants.DEFAULT_FEATURE_LOCATION + '/' + featureId); |
| File xml = new File(dir, Constants.FEATURE_FILENAME_DESCRIPTOR); |
| if (xml.exists()) { |
| xml.delete(); |
| } |
| |
| product = loadProduct(productFile); |
| } |
| |
| /* |
| * Based on the version of OSGi that we have in our state, add the appropriate plug-ins/fragments/features |
| * for the launcher. |
| */ |
| @SuppressWarnings("unlikely-arg-type") |
| private void addLauncher(PDEState state, Set<Entry> plugins, Set<Entry> fragments, Set<Entry> features) { |
| BundleDescription bundle = state.getResolvedBundle(BUNDLE_OSGI); |
| if (bundle == null) |
| return; |
| Version osgiVersion = bundle.getVersion(); |
| if (osgiVersion.compareTo(new Version("3.3")) < 0) { //$NON-NLS-1$ |
| // we have an OSGi version that is less than 3.3 so add the old launcher |
| if (!features.contains(FEATURE_PLATFORM_LAUNCHERS)) |
| features.add(new Entry(FEATURE_PLATFORM_LAUNCHERS)); |
| } else { |
| // we have OSGi version 3.3 or greater so add the executable feature |
| // and the launcher plug-in and fragments |
| BuildTimeFeature executableFeature = null; |
| try { |
| executableFeature = getSite(false).findFeature(FEATURE_EQUINOX_EXECUTABLE, null, false); |
| } catch (CoreException e) { |
| BundleHelper.getDefault().getLog().log(e.getStatus()); |
| } |
| if (executableFeature != null) { |
| /* the executable feature includes the launcher and fragments already */ |
| if (!features.contains(FEATURE_EQUINOX_EXECUTABLE)) |
| features.add(new Entry(FEATURE_EQUINOX_EXECUTABLE)); |
| } else { |
| // We don't have the executable feature, at least try and get the launcher jar and fragments |
| plugins.add(new Entry(BUNDLE_EQUINOX_LAUNCHER)); |
| List<Config> configs = getConfigInfos(); |
| // only include the fragments for the platforms we are attempting to build, since the others |
| // probably aren't around |
| for (Config config : configs) { |
| String fragment = BUNDLE_EQUINOX_LAUNCHER + '.' + config.getWs() + '.' + config.getOs(); |
| //macosx doesn't have the arch on its fragment |
| if (config.getOs().compareToIgnoreCase("macosx") != 0 || config.getArch().equals("x86_64")) //$NON-NLS-1$ //$NON-NLS-2$ |
| fragment += '.' + config.getArch(); |
| |
| if (!fragments.contains(fragment)) { |
| Entry entry = new Entry(fragment); |
| entry.addAttribute("unpack", "true"); //$NON-NLS-1$//$NON-NLS-2$ |
| fragments.add(entry); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Generate a feature that includes the given plug-ins, fragments and features. |
| * Feature order matters at compile time if there is dependencies between the features' contents. |
| * Make sure to pass an ordered set if this matters. |
| * @param feature - Name of the feature to generate |
| * @param plugins - plug-ins to include |
| * @param fragments - fragments to include |
| * @param features - An ordered set of features to include |
| * @throws CoreException |
| * @throws FileNotFoundException |
| */ |
| protected void createFeature(String feature, Set<Entry> plugins, Set<Entry> fragments, Set<Entry> features) throws CoreException, FileNotFoundException { |
| String location = IPDEBuildConstants.DEFAULT_FEATURE_LOCATION + '/' + feature; |
| File directory = new File(getWorkingDirectory(), location); |
| if (!directory.exists()) |
| directory.mkdirs(); |
| |
| PDEState state = verify ? getSite(false).getRegistry() : null; |
| BundleHelper helper = BundleHelper.getDefault(); |
| |
| if (verify && includeLaunchers) |
| addLauncher(state, plugins, fragments, features); |
| |
| String featureName = buildProperties != null ? (String) buildProperties.get(PROPERTY_GENERATED_FEATURE_LABEL) : null; |
| |
| //Create feature.xml |
| File file = new File(directory, Constants.FEATURE_FILENAME_DESCRIPTOR); |
| OutputStream output = new BufferedOutputStream(new FileOutputStream(file)); |
| XMLWriter writer = new XMLWriter(output); |
| try { |
| Map<String, String> parameters = new LinkedHashMap<>(); |
| Dictionary<String, String> environment = new Hashtable<>(3); |
| |
| parameters.put(ID, feature); |
| parameters.put(VERSION, version != null ? version : "1.0.0"); //$NON-NLS-1$ |
| if (featureName != null) |
| parameters.put(LABEL, featureName); |
| writer.startTag(FEATURE, parameters, true); |
| |
| boolean fragment = false; |
| List<Config> configs = new ArrayList<>(getConfigInfos()); |
| //we do the generic config first as a special case |
| configs.remove(Config.genericConfig()); |
| Iterator<Config> configIterator = configs.iterator(); |
| Iterator<Entry> listIter = plugins.iterator(); |
| if (!listIter.hasNext()) { |
| // no plugins, do fragments |
| fragment = true; |
| listIter = fragments.iterator(); |
| } |
| for (Config currentConfig = Config.genericConfig(); currentConfig != null; currentConfig = configIterator.next()) { |
| environment.put("osgi.os", currentConfig.getOs()); //$NON-NLS-1$ |
| environment.put("osgi.ws", currentConfig.getWs()); //$NON-NLS-1$ |
| environment.put("osgi.arch", currentConfig.getArch()); //$NON-NLS-1$ |
| for (; listIter.hasNext();) { |
| Entry entry = listIter.next(); |
| String name = entry.getId(); |
| String bundleVersion = entry.getVersion(); |
| boolean guessedUnpack = true; |
| boolean writeBundle = !verify; |
| if (verify) { |
| BundleDescription bundle = state.getResolvedBundle(name, bundleVersion); |
| if (bundle != null) { |
| //Bundle resolved, write it out if it matches the current config |
| Filter filter = helper.getFilter(bundle); |
| if (filter == null || filter.match(environment)) { |
| writeBundle = true; |
| guessedUnpack = Utils.guessUnpack(bundle, state.getExtraData().get(Long.valueOf(bundle.getBundleId()))); |
| if (currentConfig.equals(Config.genericConfig())) { |
| listIter.remove(); |
| } |
| } |
| } else { |
| //Bundle did not resolve, only ok if it was because of the platform filter |
| if (bundleVersion != null) |
| bundle = state.getBundle(name, bundleVersion, false); |
| else { |
| //There are no resolved bundles with this name, if there is more than one unresolved just use the first |
| BundleDescription[] bundles = state.getState().getBundles(name); |
| bundle = (bundles != null && bundles.length > 0) ? bundles[0] : null; |
| } |
| if (bundle != null) { |
| ResolverError[] errors = state.getState().getResolverErrors(bundle); |
| //ok if we didn't match the config |
| if (!BuildTimeSite.isConfigError(bundle, errors, configs)) { |
| BuildTimeSite.missingPlugin(bundle, errors, null, true); //throws CoreException |
| } |
| } else { |
| //throw error |
| String message = NLS.bind(Messages.exception_missingPlugin, bundleVersion != null ? name + "_" + bundleVersion : name); //$NON-NLS-1$ |
| throw new CoreException(new Status(IStatus.ERROR, PI_PDEBUILD, EXCEPTION_PLUGIN_MISSING, message, null)); |
| } |
| } |
| } |
| |
| if (writeBundle) { |
| parameters.clear(); |
| |
| parameters.put(ID, name); |
| parameters.put(VERSION, bundleVersion); |
| parameters.put("unpack", guessedUnpack ? "true" : "false"); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$ |
| if (!currentConfig.equals(Config.genericConfig())) { |
| parameters.put("os", currentConfig.getOs()); //$NON-NLS-1$ |
| parameters.put("ws", currentConfig.getWs()); //$NON-NLS-1$ |
| parameters.put("arch", currentConfig.getArch()); //$NON-NLS-1$ |
| } |
| if (fragment) |
| parameters.put(FRAGMENT, "true"); //$NON-NLS-1$ |
| |
| //add the attributes from the entry, these override values we set above |
| parameters.putAll(entry.getAttributes()); |
| |
| writer.printTag(PLUGIN, parameters, true, true, true); |
| } |
| |
| if (!fragment && !listIter.hasNext() && fragments.size() > 0) { |
| //finished the list of plugins, do the fragments now |
| fragment = true; |
| listIter = fragments.iterator(); |
| } |
| } |
| if (!verify || !configIterator.hasNext()) { |
| break; |
| } else if (plugins.size() > 0) { |
| fragment = false; |
| listIter = plugins.iterator(); |
| } else { |
| listIter = fragments.iterator(); |
| } |
| } |
| |
| for (Entry entry : features) { |
| String name = entry.getId(); |
| String featureVersion = entry.getVersion(); |
| if (verify) { |
| //this will throw an exception if the feature is not found. |
| boolean exception = true; |
| if (buildProperties != null && buildProperties.containsKey("generate.feature@" + name)) //$NON-NLS-1$ |
| exception = false; |
| getSite(false).findFeature(name, featureVersion, exception); |
| } |
| parameters.clear(); |
| parameters.put(ID, name); |
| parameters.put(VERSION, featureVersion); |
| parameters.putAll(entry.getAttributes()); |
| writer.printTag("includes", parameters, true, true, true); //$NON-NLS-1$ |
| } |
| writer.endTag(FEATURE); |
| } finally { |
| writer.close(); |
| } |
| |
| createBuildProperties(directory); |
| getSite(false).addFeatureReferenceModel(directory); |
| } |
| |
| protected void createBuildProperties(File directory) { |
| File file; |
| //create build.properties |
| file = new File(directory, IPDEBuildConstants.PROPERTIES_FILE); |
| if (buildProperties == null) { |
| buildProperties = new Properties(); |
| buildProperties.put("pde", "marker"); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| try (OutputStream stream = new BufferedOutputStream(new FileOutputStream(file))) { |
| buildProperties.store(stream, ""); //$NON-NLS-1$ |
| stream.flush(); |
| } catch (IOException e) { |
| // nothing for now |
| } |
| } |
| |
| public void setBuildProperties(String file) { |
| buildProperties = new Properties(); |
| |
| File propertiesFile = new File(file); |
| if (propertiesFile.exists()) { |
| try (InputStream input = new BufferedInputStream(new FileInputStream(file))) { |
| buildProperties.load(input); |
| } catch (IOException e) { |
| // nothing |
| } |
| } |
| } |
| |
| public void setBuildProperties(Properties properties) { |
| this.buildProperties = properties; |
| } |
| |
| public void setVerify(boolean verify) { |
| this.verify = verify; |
| reportResolutionErrors = verify; |
| } |
| |
| public void setImmutableAntProperties(Properties properties) { |
| antProperties = properties; |
| } |
| |
| public void setNestedInclusions(String nested) { |
| this.nestedInclusions = nested; |
| } |
| } |