From 09f19aa7310c9c2b75c37069cf4d79109db8e2ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Fri, 3 May 2024 19:18:01 +0200 Subject: [PATCH] Filter use-constraint violating providers of packages If a provider is chosen for a given requirement this can imply that also other providers are now become invalid, currently the resolver simply checks the whole state and adds new permutations what is rather expensive. This now adds an additional local filtering step that checks for the chosen provider for the requirement and discards any incompatible provider for other packages in the same resource. As a result for a small testcase this saves 3 out of 23 (~ 10%) uses permutations. --- .../META-INF/MANIFEST.MF | 2 +- bundles/org.eclipse.osgi.tests/pom.xml | 2 +- .../tests/container/TestModuleContainer.java | 7 +- bundles/org.eclipse.osgi/META-INF/MANIFEST.MF | 2 +- .../org/apache/felix/resolver/Candidates.java | 109 +++++++++++---- .../felix/resolver/ProblemReduction.java | 121 ++++++++++++++++ .../src/org/apache/felix/resolver/Util.java | 132 +++++++++++++++++- bundles/org.eclipse.osgi/pom.xml | 2 +- .../feature.xml | 2 +- .../org.eclipse.equinox.core.sdk/feature.xml | 2 +- .../feature.xml | 2 +- 11 files changed, 346 insertions(+), 37 deletions(-) create mode 100644 bundles/org.eclipse.osgi/felix/src/org/apache/felix/resolver/ProblemReduction.java diff --git a/bundles/org.eclipse.osgi.tests/META-INF/MANIFEST.MF b/bundles/org.eclipse.osgi.tests/META-INF/MANIFEST.MF index 48e1b899f23..14ce523fdbf 100644 --- a/bundles/org.eclipse.osgi.tests/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.osgi.tests/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: Core OSGi Tests Bundle-SymbolicName: org.eclipse.osgi.tests;singleton:=true -Bundle-Version: 3.19.100.qualifier +Bundle-Version: 3.19.200.qualifier Bundle-Vendor: Eclipse.org Require-Bundle: org.eclipse.core.runtime;bundle-version="[3.29.0,4.0.0)", diff --git a/bundles/org.eclipse.osgi.tests/pom.xml b/bundles/org.eclipse.osgi.tests/pom.xml index d3e9d4bab10..beabd708d5f 100644 --- a/bundles/org.eclipse.osgi.tests/pom.xml +++ b/bundles/org.eclipse.osgi.tests/pom.xml @@ -19,7 +19,7 @@ org.eclipse.osgi org.eclipse.osgi.tests - 3.19.100-SNAPSHOT + 3.19.200-SNAPSHOT eclipse-test-plugin diff --git a/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/container/TestModuleContainer.java b/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/container/TestModuleContainer.java index 4bd75eee64c..22f0c87774d 100644 --- a/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/container/TestModuleContainer.java +++ b/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/container/TestModuleContainer.java @@ -3959,7 +3959,8 @@ protected void assertNotMoreThanPermutationCreated(ResolutionReport report, fail("Maximum of " + max + " permutations expected but was " + permutations); } else if (permutations < max) { System.out.println( - "## Permutations are below the threshold, consider adjusting the testcase to assert the lower count!"); + "## Permutations (" + permutations + ") are below the threshold (" + max + + "), consider adjusting the testcase to assert the lower count!"); } return; } @@ -4367,8 +4368,8 @@ private static void assertWires(List required, List... p public void testLocalUseConstraintViolations() throws Exception { ResolutionReport result = resolveTestSet("set1"); // TODO get down permutation count! - assertSucessfulWith(result, 52); - assertNotMoreThanPermutationCreated(result, ResolutionReport::getSubstitutionPermutations, 23); + assertSucessfulWith(result, 49); + assertNotMoreThanPermutationCreated(result, ResolutionReport::getSubstitutionPermutations, 20); } private ResolutionReport resolveTestSet(String name) throws Exception { diff --git a/bundles/org.eclipse.osgi/META-INF/MANIFEST.MF b/bundles/org.eclipse.osgi/META-INF/MANIFEST.MF index 5c4ecd9f507..d5482329f91 100644 --- a/bundles/org.eclipse.osgi/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.osgi/META-INF/MANIFEST.MF @@ -107,7 +107,7 @@ Bundle-Activator: org.eclipse.osgi.internal.framework.SystemBundleActivator Bundle-Description: %systemBundle Bundle-Copyright: %copyright Bundle-Vendor: %eclipse.org -Bundle-Version: 3.20.0.qualifier +Bundle-Version: 3.20.100.qualifier Bundle-Localization: systembundle Bundle-DocUrl: http://www.eclipse.org Eclipse-ExtensibleAPI: true diff --git a/bundles/org.eclipse.osgi/felix/src/org/apache/felix/resolver/Candidates.java b/bundles/org.eclipse.osgi/felix/src/org/apache/felix/resolver/Candidates.java index 0b59f6dac0b..f4dc399f28f 100644 --- a/bundles/org.eclipse.osgi/felix/src/org/apache/felix/resolver/Candidates.java +++ b/bundles/org.eclipse.osgi/felix/src/org/apache/felix/resolver/Candidates.java @@ -18,6 +18,7 @@ */ package org.apache.felix.resolver; +import java.io.PrintStream; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.atomic.AtomicBoolean; @@ -34,6 +35,8 @@ class Candidates { + private static final boolean FILTER_USES = Boolean + .parseBoolean(System.getProperty("felix.resolver.candidates.filteruses", "true")); static class PopulateResult { boolean success; ResolutionError error; @@ -709,7 +712,7 @@ public Capability getFirstCandidate(Requirement req) return null; } - public void removeFirstCandidate(Requirement req) + public Capability removeFirstCandidate(Requirement req) { CandidateSelector candidates = m_candidateMap.get(req); // Remove the conflicting candidate. @@ -721,6 +724,7 @@ public void removeFirstCandidate(Requirement req) // Update the delta with the removed capability CopyOnWriteSet capPath = m_delta.getOrCompute(req); capPath.add(cap); + return cap; } public CandidateSelector clearMultipleCardinalityCandidates(Requirement req, Collection caps) @@ -1145,7 +1149,15 @@ public Candidates copy() m_delta.deepClone()); } - public void dump(ResolveContext rc) + /** + * Dump the current candidate set to system out + * + * @param rc the resolve context that should be used to look for existing + * wirings + * @param all if true all requirements are printed, if false only those that + * have more than one provider + */ + public void dump(ResolveContext rc, boolean all, PrintStream printStream) { // Create set of all revisions from requirements. Set resources = new CopyOnWriteSet(); @@ -1155,36 +1167,59 @@ public void dump(ResolveContext rc) resources.add(entry.getKey().getResource()); } // Now dump the revisions. - System.out.println("=== BEGIN CANDIDATE MAP ==="); + printStream.println("=== BEGIN CANDIDATE MAP ==="); for (Resource resource : resources) { - Wiring wiring = rc.getWirings().get(resource); - System.out.println(" " + resource - + " (" + ((wiring != null) ? "RESOLVED)" : "UNRESOLVED)")); - List reqs = (wiring != null) - ? wiring.getResourceRequirements(null) + dumpResource(resource, rc, all, printStream); + } + printStream.println("=== END CANDIDATE MAP ==="); + } + + protected void dumpResource(Resource resource, ResolveContext rc, boolean all, PrintStream printStream) { + Wiring wiring = rc == null ? null : rc.getWirings().get(resource); + List reqs = (wiring != null) ? wiring.getResourceRequirements(null) : resource.getRequirements(null); - for (Requirement req : reqs) - { - CandidateSelector candidates = m_candidateMap.get(req); - if ((candidates != null) && (!candidates.isEmpty())) - { - System.out.println(" " + req + ": " + candidates); + List dreqs = (wiring != null) ? Util.getDynamicRequirements(wiring.getResourceRequirements(null)) + : Util.getDynamicRequirements(resource.getRequirements(null)); + boolean hasMulti = hasMulti(reqs); + printStream.println(" " + (hasMulti ? "[?]" : "[!]") + Util.getResourceName(resource) + " (" + + ((wiring != null) ? "RESOLVED)" : "UNRESOLVED)")); + if (all || hasMulti) { + printRe(reqs, printStream, all); + printRe(dreqs, printStream, all); + } + } + + private boolean hasMulti(List reqs) { + for (Requirement req : reqs) { + CandidateSelector candidates = m_candidateMap.get(req); + if ((candidates != null) && (!candidates.isEmpty())) { + List remaining = candidates.getRemainingCandidates(); + if (remaining.size() > 1) { + return true; } } - reqs = (wiring != null) - ? Util.getDynamicRequirements(wiring.getResourceRequirements(null)) - : Util.getDynamicRequirements(resource.getRequirements(null)); - for (Requirement req : reqs) - { - CandidateSelector candidates = m_candidateMap.get(req); - if ((candidates != null) && (!candidates.isEmpty())) - { - System.out.println(" " + req + ": " + candidates); + } + return false; + } + + protected int printRe(List reqs, PrintStream printStream, boolean all) { + int dup = 0; + for (Requirement req : reqs) { + CandidateSelector candidates = m_candidateMap.get(req); + if ((candidates != null) && (!candidates.isEmpty())) { + List remaining = candidates.getRemainingCandidates(); + boolean hasMulti = remaining.size() > 1; + if (all || hasMulti) { + dup++; + printStream.println(" " + (hasMulti ? "[?]" : "[!]") + Util.toString(req) + ": "); + for (Capability cap : remaining) { + printStream.println(" " + Util.toString(cap)); + } } } } - System.out.println("=== END CANDIDATE MAP ==="); + return dup; } public Candidates permutate(Requirement req) @@ -1192,7 +1227,10 @@ public Candidates permutate(Requirement req) if (!Util.isMultiple(req) && canRemoveCandidate(req)) { Candidates perm = copy(); - perm.removeFirstCandidate(req); + Capability removed = perm.removeFirstCandidate(req); + if (FILTER_USES) { + ProblemReduction.removeUsesViolations(perm, req); + } return perm; } return null; @@ -1341,4 +1379,25 @@ public ResolutionException toException() { } + /** + * Returns the current provided {@link Capability} for the given resource if it + * is a candidate for the {@link Requirement} + * + * @param resource the resource to check + * @param requirement the requirement to check + * @return the {@link Capability} this Resource currently provides for the given + * {@link Requirement} or null if none is provided. + */ + public Capability getCapability(Resource resource, Requirement requirement) { + List providers = getCandidates(requirement); + if (providers != null) { + for (Capability capability : providers) { + if (capability.getResource().equals(resource)) { + return capability; + } + } + } + return null; + } + } diff --git a/bundles/org.eclipse.osgi/felix/src/org/apache/felix/resolver/ProblemReduction.java b/bundles/org.eclipse.osgi/felix/src/org/apache/felix/resolver/ProblemReduction.java new file mode 100644 index 00000000000..d1c954126ff --- /dev/null +++ b/bundles/org.eclipse.osgi/felix/src/org/apache/felix/resolver/ProblemReduction.java @@ -0,0 +1,121 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.felix.resolver; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import org.eclipse.osgi.container.ModuleContainer; +import org.osgi.framework.namespace.PackageNamespace; +import org.osgi.resource.Capability; +import org.osgi.resource.Requirement; +import org.osgi.resource.Resource; + +/** + * The idea of the {@link ProblemReduction} class is to strike out + * {@link Capability}s that might satisfy {@link Requirement}s but violates some + * contracts that would lead to a guaranteed unresolvable state. + */ +class ProblemReduction { + + private static final Capability[] EMPTY_CAPABILITIES = new Capability[0]; + + private static final boolean DEBUG_VIOLATES = false; + + /** + * Removes all violating providers for a given {@link Requirement} and + * {@link Candidates} in a local search, that is if the requirement has any uses + * it checks if there are other packages used by this one and removes any + * offending providers from the top of the list. + * + * @param candidates candidates to filter + * @param requirement the requirement where the search should start + * @return a list of Candidates that where dropped as part of the filtering + */ + static List removeUsesViolations(Candidates candidates, Requirement requirement) { + Resource targetResource = requirement.getResource(); + // fetch the current candidate for this requirement + Capability currentCandidate = candidates.getFirstCandidate(requirement); + Resource candidateResource = currentCandidate.getResource(); + // now check if it has any uses constraints + Set uses = new TreeSet<>(Util.getUses(currentCandidate)); + if (uses.isEmpty()) { + // there is nothing this one can conflict in this current set of candidates + return Collections.emptyList(); + } + if (DEBUG_VIOLATES) { + System.out.println("=== remove uses violations for " + ModuleContainer.toString(requirement)); + System.out.println("== current candidate is " + ModuleContainer.toString(currentCandidate)); + candidates.dumpResource(targetResource, null, true, System.out); + } + boolean repeat; + int round = 0; + List dropped = new ArrayList<>(); + do { + repeat = false; + round++; + if (DEBUG_VIOLATES) { + System.out.println("Round " + round + ":"); + for (String usedPackage : uses) { + System.out.println(" uses: " + usedPackage); + } + } + // now look at all other imports of the target resource if it is a package that + // is part of a used package + for (Requirement packageRequirement : targetResource.getRequirements(PackageNamespace.PACKAGE_NAMESPACE)) { + if (packageRequirement == requirement) { + continue; + } + Capability providedPackage = candidates.getCapability(candidateResource, packageRequirement); + if (providedPackage == null) { + // we do not provide anything for this package + continue; + } + if (uses.contains(Util.getPackageName(providedPackage))) { + // this is a package where we are a candidate and that has a uses constraint, so + // this package must be provided by us as well or we run into a uses-violation + // later on! + Capability capability = removeViolators(candidates, candidateResource, packageRequirement, dropped); + // if we have added any additional uses we need to reiterate... + repeat |= uses.addAll(Util.getUses(capability)); + } + } + } while (repeat); + if (DEBUG_VIOLATES && !dropped.isEmpty()) { + System.out.println(); + System.out.println("After removal (" + dropped.size() + " dropped)"); + candidates.dumpResource(targetResource, null, true, System.out); + System.out.println(); + } + return dropped; + } + + private static Capability removeViolators(Candidates candidates, Resource candidateResource, + Requirement packageRequirement, List dropped) { + Capability capability; + while ((capability = candidates.getFirstCandidate(packageRequirement)).getResource() != candidateResource) { + dropped.add(candidates.copy()); + candidates.removeFirstCandidate(packageRequirement); + } + return capability; + } + +} diff --git a/bundles/org.eclipse.osgi/felix/src/org/apache/felix/resolver/Util.java b/bundles/org.eclipse.osgi/felix/src/org/apache/felix/resolver/Util.java index 973cd5b72c6..31487d8f97b 100644 --- a/bundles/org.eclipse.osgi/felix/src/org/apache/felix/resolver/Util.java +++ b/bundles/org.eclipse.osgi/felix/src/org/apache/felix/resolver/Util.java @@ -19,9 +19,19 @@ package org.apache.felix.resolver; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.eclipse.osgi.internal.framework.FilterImpl; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.Version; import org.osgi.framework.namespace.BundleNamespace; +import org.osgi.framework.namespace.HostNamespace; import org.osgi.framework.namespace.IdentityNamespace; import org.osgi.framework.namespace.PackageNamespace; import org.osgi.resource.Capability; @@ -58,6 +68,14 @@ public static Version getVersion(Resource resource) return null; } + public static String getResourceName(Resource resource) { + String symbolicName = getSymbolicName(resource); + if (symbolicName != null) { + return symbolicName + " " + getVersion(resource); + } + return resource.toString(); + } + public static boolean isFragment(Resource resource) { List caps = resource.getCapabilities(null); @@ -81,13 +99,13 @@ public static boolean isOptional(Requirement req) public static boolean isMultiple(Requirement req) { - return Namespace.CARDINALITY_MULTIPLE.equals(req.getDirectives() + return Namespace.CARDINALITY_MULTIPLE.equals(req.getDirectives() .get(Namespace.REQUIREMENT_CARDINALITY_DIRECTIVE)) && !isDynamic(req); } public static boolean isDynamic(Requirement req) { - return PackageNamespace.RESOLUTION_DYNAMIC.equals(req.getDirectives() + return PackageNamespace.RESOLUTION_DYNAMIC.equals(req.getDirectives() .get(Namespace.REQUIREMENT_RESOLUTION_DIRECTIVE)); } @@ -115,4 +133,114 @@ public static List getDynamicRequirements(List reqs) } return result; } + + public static String getPackageName(Capability capability) { + if (capability != null && PackageNamespace.PACKAGE_NAMESPACE.equals(capability.getNamespace())) { + Object object = capability.getAttributes().get(PackageNamespace.PACKAGE_NAMESPACE); + if (object instanceof String) { + return (String) object; + } + } + return ""; + } + + public static Set getUses(Capability capability) { + if (capability != null && PackageNamespace.PACKAGE_NAMESPACE.equals(capability.getNamespace())) { + String uses = capability.getDirectives().get(PackageNamespace.CAPABILITY_USES_DIRECTIVE); + if (uses != null && !uses.isEmpty()) { + return Arrays.stream(uses.split(",")).map(String::trim).collect(Collectors.toSet()); + } + } + return Collections.emptySet(); + } + + static String toString(Requirement requirement) { + if (PackageNamespace.PACKAGE_NAMESPACE.equals(requirement.getNamespace())) { + return Constants.IMPORT_PACKAGE + ": " //$NON-NLS-1$ + + createOSGiRequirement(requirement, PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE, + PackageNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE); + } else if (BundleNamespace.BUNDLE_NAMESPACE.equals(requirement.getNamespace())) { + return Constants.REQUIRE_BUNDLE + ": " //$NON-NLS-1$ + + createOSGiRequirement(requirement, BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE); + } else if (HostNamespace.HOST_NAMESPACE.equals(requirement.getNamespace())) { + return Constants.FRAGMENT_HOST + ": " //$NON-NLS-1$ + + createOSGiRequirement(requirement, HostNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE); + } + return Constants.REQUIRE_CAPABILITY + ": " + requirement.toString(); //$NON-NLS-1$ + } + + private static String createOSGiRequirement(Requirement requirement, String... versions) { + Map directives = new HashMap<>(requirement.getDirectives()); + String filter = directives.remove(Namespace.REQUIREMENT_FILTER_DIRECTIVE); + if (filter == null) + throw new IllegalArgumentException("No filter directive found:" + requirement); //$NON-NLS-1$ + FilterImpl filterImpl; + try { + filterImpl = FilterImpl.newInstance(filter); + } catch (InvalidSyntaxException e) { + throw new IllegalArgumentException("Invalid filter directive", e); //$NON-NLS-1$ + } + Map matchingAttributes = filterImpl.getStandardOSGiAttributes(versions); + String name = matchingAttributes.remove(requirement.getNamespace()); + if (name == null) + throw new IllegalArgumentException("Invalid requirement: " + requirement); //$NON-NLS-1$ + return name + toString(matchingAttributes, false, true) + toString(directives, true, true); + } + + static String toString(Map map, boolean directives) { + return toString(map, directives, false); + } + + static String toString(Map map, boolean directives, boolean stringsOnly) { + if (map.size() == 0) + return ""; //$NON-NLS-1$ + String assignment = directives ? ":=" : "="; //$NON-NLS-1$ //$NON-NLS-2$ + Set> set = map.entrySet(); + StringBuilder sb = new StringBuilder(); + for (java.util.Map.Entry entry : set) { + sb.append("; "); //$NON-NLS-1$ + String key = entry.getKey(); + Object value = entry.getValue(); + if (value instanceof List) { + @SuppressWarnings("unchecked") + List list = (List) value; + if (list.isEmpty()) + continue; + Object component = list.get(0); + String className = component.getClass().getName(); + String type = className.substring(className.lastIndexOf('.') + 1); + sb.append(key).append(':').append("List<").append(type).append(">").append(assignment).append('"'); //$NON-NLS-1$ //$NON-NLS-2$ + for (Object object : list) + sb.append(object).append(','); + sb.setLength(sb.length() - 1); + sb.append('"'); + } else { + String type = ""; //$NON-NLS-1$ + if (!(value instanceof String) && !stringsOnly) { + String className = value.getClass().getName(); + type = ":" + className.substring(className.lastIndexOf('.') + 1); //$NON-NLS-1$ + } + sb.append(key).append(type).append(assignment).append('"').append(value).append('"'); + } + } + return sb.toString(); + } + + static String toString(Capability capability) { + if (PackageNamespace.PACKAGE_NAMESPACE.equals(capability.getNamespace())) { + return Constants.EXPORT_PACKAGE + ": " + createOSGiCapability(capability); //$NON-NLS-1$ + } else if (BundleNamespace.BUNDLE_NAMESPACE.equals(capability.getNamespace())) { + return Constants.BUNDLE_SYMBOLICNAME + ": " + createOSGiCapability(capability); //$NON-NLS-1$ + } else if (HostNamespace.HOST_NAMESPACE.equals(capability.getNamespace())) { + return Constants.BUNDLE_SYMBOLICNAME + ": " + createOSGiCapability(capability); //$NON-NLS-1$ + } + return Constants.PROVIDE_CAPABILITY + ": " + capability.toString(); //$NON-NLS-1$ + } + + private static String createOSGiCapability(Capability cap) { + Map attributes = new HashMap<>(cap.getAttributes()); + Map directives = cap.getDirectives(); + String name = String.valueOf(attributes.remove(cap.getNamespace())); + return name + toString(attributes, false, true) + toString(directives, true, true); + } } \ No newline at end of file diff --git a/bundles/org.eclipse.osgi/pom.xml b/bundles/org.eclipse.osgi/pom.xml index 80c0d324026..6375514ebd3 100644 --- a/bundles/org.eclipse.osgi/pom.xml +++ b/bundles/org.eclipse.osgi/pom.xml @@ -19,7 +19,7 @@ org.eclipse.osgi org.eclipse.osgi - 3.20.0-SNAPSHOT + 3.20.100-SNAPSHOT eclipse-plugin diff --git a/features/org.eclipse.equinox.core.feature/feature.xml b/features/org.eclipse.equinox.core.feature/feature.xml index c426c66b33e..9b30abc82ff 100644 --- a/features/org.eclipse.equinox.core.feature/feature.xml +++ b/features/org.eclipse.equinox.core.feature/feature.xml @@ -2,7 +2,7 @@ diff --git a/features/org.eclipse.equinox.core.sdk/feature.xml b/features/org.eclipse.equinox.core.sdk/feature.xml index f07279b4f74..8c08f312424 100644 --- a/features/org.eclipse.equinox.core.sdk/feature.xml +++ b/features/org.eclipse.equinox.core.sdk/feature.xml @@ -2,7 +2,7 @@ diff --git a/features/org.eclipse.equinox.server.core/feature.xml b/features/org.eclipse.equinox.server.core/feature.xml index 1a10f83fdf6..0d4b17fe006 100644 --- a/features/org.eclipse.equinox.server.core/feature.xml +++ b/features/org.eclipse.equinox.server.core/feature.xml @@ -2,7 +2,7 @@