Merge pull request #181 from gnodet/issue-114

Support for core extensions
This commit is contained in:
Guillaume Nodet
2020-11-02 16:53:39 +01:00
committed by GitHub
13 changed files with 386 additions and 33 deletions

View File

@@ -40,6 +40,10 @@
<groupId>org.jboss.fuse.mvnd</groupId>
<artifactId>mvnd-common</artifactId>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-embedder</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>

View File

@@ -222,7 +222,8 @@ public class DaemonConnector {
}
public DaemonClientConnection startDaemon(DaemonCompatibilitySpec constraint) {
final String daemon = startDaemon();
final String daemon = UUID.randomUUID().toString();
final Process process = startDaemon(daemon, constraint.getOptions());
LOGGER.debug("Started Maven daemon {}", daemon);
long start = System.currentTimeMillis();
do {
@@ -235,14 +236,12 @@ public class DaemonConnector {
} catch (InterruptedException e) {
throw new DaemonException.InterruptedException(e);
}
} while (System.currentTimeMillis() - start < DEFAULT_CONNECT_TIMEOUT);
} while (process.isAlive() && System.currentTimeMillis() - start < DEFAULT_CONNECT_TIMEOUT);
DaemonDiagnostics diag = new DaemonDiagnostics(daemon, layout);
throw new DaemonException.ConnectException("Timeout waiting to connect to the Maven daemon.\n" + diag.describe());
}
private String startDaemon() {
final String uid = UUID.randomUUID().toString();
private Process startDaemon(String uid, List<String> opts) {
final Path mavenHome = layout.mavenHome();
final Path workingDir = layout.userDir();
String command = "";
@@ -267,18 +266,19 @@ public class DaemonConnector {
args.add("-Xmx4g");
args.add(Environment.DAEMON_IDLE_TIMEOUT_MS.asCommandLineProperty(Integer.toString(layout.getIdleTimeoutMs())));
args.add(Environment.DAEMON_KEEP_ALIVE_MS.asCommandLineProperty(Integer.toString(layout.getKeepAliveMs())));
args.addAll(opts);
args.add(MavenDaemon.class.getName());
command = String.join(" ", args);
LOGGER.debug("Starting daemon process: uid = {}, workingDir = {}, daemonArgs: {}", uid, workingDir, command);
ProcessBuilder.Redirect redirect = ProcessBuilder.Redirect.appendTo(layout.daemonOutLog(uid).toFile());
new ProcessBuilder()
Process process = new ProcessBuilder()
.directory(workingDir.toFile())
.command(args)
.redirectOutput(redirect)
.redirectError(redirect)
.start();
return uid;
return process;
} catch (Exception e) {
throw new DaemonException.StartException(
String.format("Error starting daemon: uid = %s, workingDir = %s, daemonArgs: %s",

View File

@@ -15,14 +15,25 @@
*/
package org.jboss.fuse.mvnd.client;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.maven.cli.internal.extension.model.CoreExtension;
import org.apache.maven.cli.internal.extension.model.io.xpp3.CoreExtensionsXpp3Reader;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
import org.fusesource.jansi.Ansi;
import org.jboss.fuse.mvnd.common.BuildProperties;
import org.jboss.fuse.mvnd.common.DaemonCompatibilitySpec;
@@ -44,9 +55,13 @@ import org.slf4j.LoggerFactory;
public class DefaultClient implements Client {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultClient.class);
public static final int DEFAULT_PERIODIC_CHECK_INTERVAL_MILLIS = 10 * 1000;
public static final int CANCEL_TIMEOUT = 10 * 1000;
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultClient.class);
private static final String EXT_CLASS_PATH = "maven.ext.class.path";
private static final String EXTENSIONS_FILENAME = ".mvn/extensions.xml";
private final Supplier<ClientLayout> lazyLayout;
private final BuildProperties buildProperties;
@@ -198,8 +213,8 @@ public class DefaultClient implements Client {
args.add("-Dmaven.repo.local=" + localMavenRepository.toString());
}
List<String> opts = getDaemonOpts(layout);
final DaemonConnector connector = new DaemonConnector(layout, registry, buildProperties);
List<String> opts = new ArrayList<>();
try (DaemonClientConnection daemon = connector.connect(new DaemonCompatibilitySpec(javaHome, opts), output)) {
output.buildStatus("Connected to daemon");
@@ -245,6 +260,56 @@ public class DefaultClient implements Client {
}
}
private List<String> getDaemonOpts(ClientLayout layout) {
List<String> options = new ArrayList<>();
// Classpath
List<Path> jars = parseExtClasspath(layout);
if (!jars.isEmpty()) {
options.add(Environment.DAEMON_EXT_CLASSPATH.asCommandLineProperty(
jars.stream().map(Path::toString).collect(Collectors.joining(","))));
}
// Extensions
try {
List<CoreExtension> extensions = readCoreExtensionsDescriptor(layout);
if (!extensions.isEmpty()) {
options.add(Environment.DAEMON_CORE_EXTENSIONS.asCommandLineProperty(
extensions.stream().map(e -> e.getGroupId() + ":" + e.getArtifactId() + ":" + e.getVersion())
.collect(Collectors.joining(","))));
}
} catch (IOException | XmlPullParserException e) {
throw new RuntimeException("Unable to parse core extensions", e);
}
return options;
}
private List<Path> parseExtClasspath(ClientLayout layout) {
String extClassPath = System.getProperty(EXT_CLASS_PATH);
List<Path> jars = new ArrayList<>();
if (StringUtils.isNotEmpty(extClassPath)) {
for (String jar : StringUtils.split(extClassPath, File.pathSeparator)) {
Path path = layout.userDir().resolve(jar).toAbsolutePath();
jars.add(path);
}
}
return jars;
}
private List<CoreExtension> readCoreExtensionsDescriptor(ClientLayout layout)
throws IOException, XmlPullParserException {
Path multiModuleProjectDirectory = layout.multiModuleProjectDirectory();
if (multiModuleProjectDirectory == null) {
return Collections.emptyList();
}
Path extensionsFile = multiModuleProjectDirectory.resolve(EXTENSIONS_FILENAME);
if (!Files.exists(extensionsFile)) {
return Collections.emptyList();
}
CoreExtensionsXpp3Reader parser = new CoreExtensionsXpp3Reader();
try (InputStream is = Files.newInputStream(extensionsFile)) {
return parser.read(is).getExtensions();
}
}
static void setDefaultArgs(List<String> args, ClientLayout layout) {
if (args.stream().noneMatch(arg -> arg.startsWith("-T") || arg.equals("--threads"))) {
args.add("-T" + layout.getThreads());

View File

@@ -39,6 +39,14 @@ public class DaemonCompatibilitySpec {
this.options = Objects.requireNonNull(options, "options");
}
public Path getJavaHome() {
return javaHome;
}
public List<String> getOptions() {
return options;
}
public Result isSatisfiedBy(DaemonInfo daemon) {
if (!javaHomeMatches(daemon)) {
return new Result(false, () -> "Java home is different.\n" + diff(daemon));

View File

@@ -57,7 +57,15 @@ public enum Environment {
* line.
*/
MVND_THREADS("mvnd.threads", null),
DAEMON_UID("daemon.uid", null);
DAEMON_UID("daemon.uid", null),
/**
* Internal option to specify the maven extension classpath
*/
DAEMON_EXT_CLASSPATH("daemon.ext.classpath", null),
/**
* Internal option to specify the list of maven extension to register
*/
DAEMON_CORE_EXTENSIONS("daemon.core.extensions", null);
public static final int DEFAULT_IDLE_TIMEOUT = (int) TimeUnit.HOURS.toMillis(3);

View File

@@ -52,6 +52,8 @@ import org.apache.maven.building.Source;
import org.apache.maven.cli.configuration.ConfigurationProcessor;
import org.apache.maven.cli.configuration.SettingsXmlConfigurationProcessor;
import org.apache.maven.cli.event.ExecutionEventLogger;
import org.apache.maven.cli.internal.BootstrapCoreExtensionManager;
import org.apache.maven.cli.internal.extension.model.CoreExtension;
import org.apache.maven.cli.logging.Slf4jLoggerManager;
import org.apache.maven.cli.transfer.ConsoleMavenTransferListener;
import org.apache.maven.cli.transfer.QuietMavenTransferListener;
@@ -90,6 +92,7 @@ import org.codehaus.plexus.classworlds.realm.ClassRealm;
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
import org.codehaus.plexus.util.StringUtils;
import org.eclipse.aether.transfer.TransferListener;
import org.jboss.fuse.mvnd.common.Environment;
import org.jboss.fuse.mvnd.logging.smart.AbstractLoggingSpy;
import org.jboss.fuse.mvnd.plugin.CliPluginRealmCache;
import org.slf4j.ILoggerFactory;
@@ -121,6 +124,10 @@ public class DaemonMavenCli {
public static final File DEFAULT_GLOBAL_TOOLCHAINS_FILE = new File(System.getProperty("maven.conf"), "toolchains.xml");
private static final String EXT_CLASS_PATH = "maven.ext.class.path";
private static final String EXTENSIONS_FILENAME = ".mvn/extensions.xml";
private static final String MVN_MAVEN_CONFIG = ".mvn/maven.config";
public static final String STYLE_COLOR_PROPERTY = "style.color";
@@ -168,6 +175,7 @@ public class DaemonMavenCli {
cli(cliRequest);
properties(cliRequest);
logging(cliRequest);
container(cliRequest);
configure(cliRequest);
version(cliRequest);
toolchains(cliRequest);
@@ -390,6 +398,17 @@ public class DaemonMavenCli {
populateProperties(cliRequest.commandLine, cliRequest.systemProperties, cliRequest.userProperties);
}
void container(CliRequest cliRequest) {
Map<String, Object> data = new HashMap<>();
data.put("plexus", container);
data.put("workingDirectory", cliRequest.workingDirectory);
data.put("systemProperties", cliRequest.systemProperties);
data.put("userProperties", cliRequest.userProperties);
data.put("versionProperties", CLIReportingUtils.getBuildProperties());
eventSpyDispatcher.init(() -> data);
}
void container()
throws Exception {
ClassRealm coreRealm = classWorld.getClassRealm("plexus.core");
@@ -397,13 +416,28 @@ public class DaemonMavenCli {
coreRealm = classWorld.getRealms().iterator().next();
}
// List<File> extClassPath = parseExtClasspath( cliRequest );
List<File> extClassPath = Stream
.of(Environment.DAEMON_EXT_CLASSPATH.systemProperty().orDefault(() -> "").asString().split(","))
.map(File::new)
.collect(Collectors.toList());
CoreExtensionEntry coreEntry = CoreExtensionEntry.discoverFrom(coreRealm);
List<CoreExtensionEntry> extensions = Collections.emptyList();
// loadCoreExtensions( cliRequest, coreRealm, coreEntry.getExportedArtifacts() );
ClassRealm containerRealm = coreRealm;
List<CoreExtension> extensions = Stream
.of(Environment.DAEMON_CORE_EXTENSIONS.systemProperty().orDefault(() -> "").asString().split(","))
.filter(s -> s != null && !s.isEmpty())
.map(s -> {
String[] parts = s.split(":");
CoreExtension ce = new CoreExtension();
ce.setGroupId(parts[0]);
ce.setArtifactId(parts[1]);
ce.setVersion(parts[2]);
return ce;
})
.collect(Collectors.toList());
List<CoreExtensionEntry> extensionsEntries = loadCoreExtensions(extensions, coreRealm,
coreEntry.getExportedArtifacts());
ClassRealm containerRealm = setupContainerRealm(classWorld, coreRealm, extClassPath, extensionsEntries);
ContainerConfiguration cc = new DefaultContainerConfiguration().setClassWorld(classWorld)
.setRealm(containerRealm).setClassPathScanning(PlexusConstants.SCANNING_INDEX).setAutoWiring(true)
@@ -411,11 +445,10 @@ public class DaemonMavenCli {
Set<String> exportedArtifacts = new HashSet<>(coreEntry.getExportedArtifacts());
Set<String> exportedPackages = new HashSet<>(coreEntry.getExportedPackages());
// for ( CoreExtensionEntry extension : extensions )
// {
// exportedArtifacts.addAll( extension.getExportedArtifacts() );
// exportedPackages.addAll( extension.getExportedPackages() );
// }
for (CoreExtensionEntry extension : extensionsEntries) {
exportedArtifacts.addAll(extension.getExportedArtifacts());
exportedPackages.addAll(extension.getExportedPackages());
}
final CoreExports exports = new CoreExports(containerRealm, exportedArtifacts, exportedPackages);
final CliPluginRealmCache realmCache = new CliPluginRealmCache();
@@ -435,7 +468,7 @@ public class DaemonMavenCli {
container.setLoggerManager(plexusLoggerManager);
for (CoreExtensionEntry extension : extensions) {
for (CoreExtensionEntry extension : extensionsEntries) {
container.discoverComponents(extension.getClassRealm(), new SessionScopeModule(container),
new MojoExecutionScopeModule(container));
}
@@ -454,7 +487,108 @@ public class DaemonMavenCli {
toolchainsBuilder = container.lookup(ToolchainsBuilder.class);
dispatcher = (DefaultSecDispatcher) container.lookup(SecDispatcher.class, "maven");
}
private List<CoreExtensionEntry> loadCoreExtensions(List<CoreExtension> extensions, ClassRealm containerRealm,
Set<String> providedArtifacts) {
try {
if (extensions.isEmpty()) {
return Collections.emptyList();
}
ContainerConfiguration cc = new DefaultContainerConfiguration() //
.setClassWorld(classWorld) //
.setRealm(containerRealm) //
.setClassPathScanning(PlexusConstants.SCANNING_INDEX) //
.setAutoWiring(true) //
.setJSR250Lifecycle(true) //
.setName("maven");
DefaultPlexusContainer container = new DefaultPlexusContainer(cc, new AbstractModule() {
@Override
protected void configure() {
bind(ILoggerFactory.class).toInstance(slf4jLoggerFactory);
}
});
try {
CliRequest cliRequest = new CliRequest(new String[0], classWorld);
cliRequest.commandLine = new CommandLine.Builder().build();
container.setLookupRealm(null);
container.setLoggerManager(plexusLoggerManager);
container.getLoggerManager().setThresholds(cliRequest.request.getLoggingLevel());
Thread.currentThread().setContextClassLoader(container.getContainerRealm());
executionRequestPopulator = container.lookup(MavenExecutionRequestPopulator.class);
configurationProcessors = container.lookupMap(ConfigurationProcessor.class);
configure(cliRequest);
populateRequest(cliRequest, cliRequest.request);
executionRequestPopulator.populateDefaults(cliRequest.request);
BootstrapCoreExtensionManager resolver = container.lookup(BootstrapCoreExtensionManager.class);
return Collections
.unmodifiableList(resolver.loadCoreExtensions(cliRequest.request, providedArtifacts, extensions));
} finally {
executionRequestPopulator = null;
container.dispose();
}
} catch (RuntimeException e) {
// runtime exceptions are most likely bugs in maven, let them bubble up to the user
throw e;
} catch (Exception e) {
slf4jLogger.warn("Failed to load extensions descriptor {}: {}", extensions, e.getMessage());
}
return Collections.emptyList();
}
private ClassRealm setupContainerRealm(ClassWorld classWorld, ClassRealm coreRealm, List<File> extClassPath,
List<CoreExtensionEntry> extensions) throws Exception {
if (!extClassPath.isEmpty() || !extensions.isEmpty()) {
ClassRealm extRealm = classWorld.newRealm("maven.ext", null);
extRealm.setParentRealm(coreRealm);
slf4jLogger.debug("Populating class realm {}", extRealm.getId());
for (File file : extClassPath) {
extRealm.addURL(file.toURI().toURL());
}
for (CoreExtensionEntry entry : reverse(extensions)) {
Set<String> exportedPackages = entry.getExportedPackages();
ClassRealm realm = entry.getClassRealm();
for (String exportedPackage : exportedPackages) {
extRealm.importFrom(realm, exportedPackage);
}
if (exportedPackages.isEmpty()) {
// sisu uses realm imports to establish component visibility
extRealm.importFrom(realm, realm.getId());
}
}
return extRealm;
}
return coreRealm;
}
private static <T> List<T> reverse(List<T> list) {
List<T> copy = new ArrayList<>(list);
Collections.reverse(copy);
return copy;
}
private List<File> parseExtClasspath(CliRequest cliRequest) {
String extClassPath = cliRequest.userProperties.getProperty(EXT_CLASS_PATH);
if (extClassPath == null) {
extClassPath = cliRequest.systemProperties.getProperty(EXT_CLASS_PATH);
}
List<File> jars = new ArrayList<>();
if (StringUtils.isNotEmpty(extClassPath)) {
for (String jar : StringUtils.split(extClassPath, File.pathSeparator)) {
File file = resolveFile(new File(jar), cliRequest.workingDirectory);
slf4jLogger.debug(" Included {}", file);
jars.add(file);
}
}
return jars;
}
//
@@ -669,14 +803,6 @@ public class DaemonMavenCli {
//
cliRequest.request.setEventSpyDispatcher(eventSpyDispatcher);
Map<String, Object> data = new HashMap<>();
data.put("plexus", container);
data.put("workingDirectory", cliRequest.workingDirectory);
data.put("systemProperties", cliRequest.systemProperties);
data.put("userProperties", cliRequest.userProperties);
data.put("versionProperties", CLIReportingUtils.getBuildProperties());
eventSpyDispatcher.init(() -> data);
//
// We expect at most 2 implementations to be available. The SettingsXmlConfigurationProcessor implementation
// is always available in the core and likely always will be, but we may have another ConfigurationProcessor
@@ -797,7 +923,10 @@ public class DaemonMavenCli {
}
private void populateRequest(CliRequest cliRequest) {
MavenExecutionRequest request = cliRequest.request;
populateRequest(cliRequest, cliRequest.request);
}
private void populateRequest(CliRequest cliRequest, MavenExecutionRequest request) {
CommandLine commandLine = cliRequest.commandLine;
String workingDirectory = cliRequest.workingDirectory;
boolean quiet = cliRequest.quiet;

View File

@@ -101,6 +101,10 @@ public class Server implements AutoCloseable, Runnable {
strategy = DaemonExpiration.master();
List<String> opts = new ArrayList<>();
Environment.DAEMON_EXT_CLASSPATH.systemProperty().asOptional()
.ifPresent(s -> opts.add(Environment.DAEMON_EXT_CLASSPATH.asCommandLineProperty(s)));
Environment.DAEMON_CORE_EXTENSIONS.systemProperty().asOptional()
.ifPresent(s -> opts.add(Environment.DAEMON_CORE_EXTENSIONS.asCommandLineProperty(s)));
long cur = System.currentTimeMillis();
final Path javaHome = Paths.get(System.getProperty("mvnd.java.home"));
info = new DaemonInfo(uid, javaHome.toString(), layout.mavenHome().toString(),

View File

@@ -61,7 +61,7 @@
<exclusion id="*:plexus-utils"/>
</artifact>
<artifact id="org.jboss.fuse.mvnd:mvnd-client:${project.version}">
<exclusion id="*:slf4j-simple"/>
<exclusion id="*:*"/>
</artifact>
</artifactSet>

View File

@@ -47,9 +47,8 @@ public class DistroIT {
.collect(Collectors.toList());
final String msg = mavenHome.resolve("mvn/lib/ext") + " contains duplicates available in "
+ mavenHome.resolve("mvn/lib")
+ " or " + mavenHome.resolve("mvn/lib");
Assertions.assertEquals(new ArrayList<String>(), dups, msg);
+ mavenHome.resolve("mvn/lib") + " or " + mavenHome.resolve("mvn/boot");
Assertions.assertEquals(new ArrayList<Avc>(), dups, msg);
}
@Test

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed 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.jboss.fuse.mvnd.it;
import java.io.IOException;
import java.util.Collections;
import javax.inject.Inject;
import org.assertj.core.api.Assertions;
import org.jboss.fuse.mvnd.client.Client;
import org.jboss.fuse.mvnd.client.ClientLayout;
import org.jboss.fuse.mvnd.common.DaemonInfo;
import org.jboss.fuse.mvnd.common.logging.ClientOutput;
import org.jboss.fuse.mvnd.junit.MvndNativeTest;
import org.jboss.fuse.mvnd.junit.TestRegistry;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static org.junit.jupiter.api.Assertions.assertEquals;
@MvndNativeTest(projectDir = "src/test/projects/extensions")
public class ExtensionsNativeIT {
@Inject
Client client;
@Inject
ClientLayout layout;
@Inject
TestRegistry registry;
@Test
void version() throws IOException, InterruptedException {
registry.killAll();
Assertions.assertThat(registry.getAll().size()).isEqualTo(0);
final ClientOutput o = Mockito.mock(ClientOutput.class);
client.execute(o, "-v").assertSuccess();
Assertions.assertThat(registry.getAll().size()).isEqualTo(1);
DaemonInfo daemon = registry.getAll().iterator().next();
assertEquals(Collections.singletonList("-Ddaemon.core.extensions=io.takari.aether:takari-local-repository:0.11.3"),
daemon.getOptions());
client.execute(o, "-v").assertSuccess();
Assertions.assertThat(registry.getAll().size()).isEqualTo(1);
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed 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.jboss.fuse.mvnd.it;
import org.jboss.fuse.mvnd.junit.MvndTest;
@MvndTest(projectDir = "src/test/projects/extensions")
public class ExtensionsTest extends ExtensionsNativeIT {
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2020 the original author or authors.
Licensed 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.
-->
<extensions>
<extension>
<groupId>io.takari.aether</groupId>
<artifactId>takari-local-repository</artifactId>
<version>0.11.3</version>
</extension>
</extensions>

View File

@@ -0,0 +1,27 @@
<!--
Copyright 2019 the original author or authors.
Licensed 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.
-->
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.jboss.fuse.mvnd.test.extensions</groupId>
<artifactId>extensions</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
</project>