diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle
index e981387746..b7b4368d2a 100644
--- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle
+++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle
@@ -15,6 +15,7 @@ configurations {
dependencies {
api platform(project(':spring-boot-project:spring-boot-parent'))
+ api project(':spring-boot-project:spring-boot-tools:spring-boot-cloudnativebuildpack')
compileOnly 'org.apache.maven.plugin-tools:maven-plugin-annotations'
compileOnly 'org.sonatype.plexus:plexus-build-api'
diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java
new file mode 100644
index 0000000000..30c56e981c
--- /dev/null
+++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2012-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
+ *
+ * https://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.springframework.boot.maven;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.model.Dependency;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.annotations.Component;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.project.MavenProject;
+import org.apache.maven.project.MavenProjectHelper;
+import org.apache.maven.shared.artifact.filter.collection.ArtifactsFilter;
+import org.apache.maven.shared.artifact.filter.collection.ScopeFilter;
+
+import org.springframework.boot.loader.tools.Layout;
+import org.springframework.boot.loader.tools.LayoutFactory;
+import org.springframework.boot.loader.tools.Layouts.Expanded;
+import org.springframework.boot.loader.tools.Layouts.Jar;
+import org.springframework.boot.loader.tools.Layouts.LayeredJar;
+import org.springframework.boot.loader.tools.Layouts.None;
+import org.springframework.boot.loader.tools.Layouts.War;
+import org.springframework.boot.loader.tools.Libraries;
+import org.springframework.boot.loader.tools.Packager;
+
+/**
+ * Abstract base class for classes that work with an {@link Packager}.
+ *
+ * @author Phillip Webb
+ * @since 2.3.0
+ */
+public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo {
+
+ /**
+ * The Maven project.
+ * @since 1.0.0
+ */
+ @Parameter(defaultValue = "${project}", readonly = true, required = true)
+ protected MavenProject project;
+
+ /**
+ * Maven project helper utils.
+ * @since 1.0.0
+ */
+ @Component
+ protected MavenProjectHelper projectHelper;
+
+ /**
+ * The name of the main class. If not specified the first compiled class found that
+ * contains a 'main' method will be used.
+ * @since 1.0.0
+ */
+ @Parameter
+ private String mainClass;
+
+ /**
+ * The type of archive (which corresponds to how the dependencies are laid out inside
+ * it). Possible values are JAR, LAYERED_JAR, WAR, ZIP, DIR, NONE. Defaults to a guess
+ * based on the archive type.
+ * @since 1.0.0
+ */
+ @Parameter(property = "spring-boot.repackage.layout")
+ private LayoutType layout;
+
+ /**
+ * The layout factory that will be used to create the executable archive if no
+ * explicit layout is set. Alternative layouts implementations can be provided by 3rd
+ * parties.
+ * @since 1.5.0
+ */
+ @Parameter
+ private LayoutFactory layoutFactory;
+
+ /**
+ * Exclude Spring Boot devtools from the repackaged archive.
+ * @since 1.3.0
+ */
+ @Parameter(property = "spring-boot.repackage.excludeDevtools", defaultValue = "true")
+ private boolean excludeDevtools = true;
+
+ /**
+ * Include system scoped dependencies.
+ * @since 1.4.0
+ */
+ @Parameter(defaultValue = "false")
+ public boolean includeSystemScope;
+
+ /**
+ * Return a {@link Packager} configured for this MOJO.
+ * @param
the packager type
+ * @param supplier a packager supplier
+ * @return a configured packager
+ */
+ protected
P getConfiguredPackager(Supplier
supplier) {
+ P packager = supplier.get();
+ packager.setLayoutFactory(this.layoutFactory);
+ packager.addMainClassTimeoutWarningListener(new LoggingMainClassTimeoutWarningListener(this::getLog));
+ packager.setMainClass(this.mainClass);
+ if (this.layout != null) {
+ getLog().info("Layout: " + this.layout);
+ packager.setLayout(this.layout.layout());
+ }
+ return packager;
+ }
+
+ /**
+ * Return {@link Libraries} that the packager can use.
+ * @param unpacks any libraries that require unpack
+ * @return the libraries to use
+ * @throws MojoExecutionException on execution error
+ */
+ protected final Libraries getLibraries(Collection unpacks) throws MojoExecutionException {
+ Set artifacts = filterDependencies(this.project.getArtifacts(), getFilters(getAdditionalFilters()));
+ Libraries libraries = new ArtifactsLibraries(artifacts, unpacks, getLog());
+ return libraries;
+ }
+
+ private ArtifactsFilter[] getAdditionalFilters() {
+ List filters = new ArrayList<>();
+ if (this.excludeDevtools) {
+ Exclude exclude = new Exclude();
+ exclude.setGroupId("org.springframework.boot");
+ exclude.setArtifactId("spring-boot-devtools");
+ ExcludeFilter filter = new ExcludeFilter(exclude);
+ filters.add(filter);
+ }
+ if (!this.includeSystemScope) {
+ filters.add(new ScopeFilter(null, Artifact.SCOPE_SYSTEM));
+ }
+ return filters.toArray(new ArtifactsFilter[0]);
+ }
+
+ /**
+ * Archive layout types.
+ */
+ public enum LayoutType {
+
+ /**
+ * Jar Layout.
+ */
+ JAR(new Jar()),
+
+ /**
+ * Layered Jar Layout.
+ */
+ LAYERED_JAR(new LayeredJar()),
+
+ /**
+ * War Layout.
+ */
+ WAR(new War()),
+
+ /**
+ * Zip Layout.
+ */
+ ZIP(new Expanded()),
+
+ /**
+ * Dir Layout.
+ */
+ DIR(new Expanded()),
+
+ /**
+ * No Layout.
+ */
+ NONE(new None());
+
+ private final Layout layout;
+
+ LayoutType(Layout layout) {
+ this.layout = layout;
+ }
+
+ public Layout layout() {
+ return this.layout;
+ }
+
+ }
+
+}
diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java
new file mode 100644
index 0000000000..1632b87b27
--- /dev/null
+++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2012-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
+ *
+ * https://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.springframework.boot.maven;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.zip.ZipEntry;
+
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
+import org.apache.commons.compress.archivers.tar.TarConstants;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugin.logging.Log;
+import org.apache.maven.plugins.annotations.Execute;
+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.plugins.annotations.ResolutionScope;
+
+import org.springframework.boot.cloudnativebuildpack.build.AbstractBuildLog;
+import org.springframework.boot.cloudnativebuildpack.build.BuildLog;
+import org.springframework.boot.cloudnativebuildpack.build.BuildRequest;
+import org.springframework.boot.cloudnativebuildpack.build.Builder;
+import org.springframework.boot.cloudnativebuildpack.docker.TotalProgressEvent;
+import org.springframework.boot.cloudnativebuildpack.io.Owner;
+import org.springframework.boot.cloudnativebuildpack.io.TarArchive;
+import org.springframework.boot.loader.tools.EntryWriter;
+import org.springframework.boot.loader.tools.ImagePackager;
+import org.springframework.boot.loader.tools.Libraries;
+import org.springframework.util.StringUtils;
+
+/**
+ * Package an application into a OCI image using a
+ * buildpack.
+ *
+ * @author Phillip Webb
+ * @since 2.3.0
+ */
+@Mojo(name = "build-image", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true, threadSafe = true,
+ requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME,
+ requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME)
+@Execute(phase = LifecyclePhase.PACKAGE)
+public class BuildImageMojo extends AbstractPackagerMojo {
+
+ /**
+ * Directory containing the JAR.
+ * @since 2.3.0
+ */
+ @Parameter(defaultValue = "${project.build.directory}", required = true)
+ private File sourceDirectory;
+
+ /**
+ * Name of the JAR.
+ * @since 2.3.0
+ */
+ @Parameter(defaultValue = "${project.build.finalName}", readonly = true)
+ private String finalName;
+
+ /**
+ * Skip the execution.
+ * @since 2.3.0
+ */
+ @Parameter(property = "spring-boot.build-image.skip", defaultValue = "false")
+ private boolean skip;
+
+ /**
+ * Classifier used when finding the source jar.
+ * @since 2.3.0
+ */
+ @Parameter
+ private String classifier;
+
+ /**
+ * Image configuration operations.
+ * @since 2.3.0
+ */
+ @Parameter
+ private Image image;
+
+ @Override
+ public void execute() throws MojoExecutionException, MojoFailureException {
+ if (this.project.getPackaging().equals("pom")) {
+ getLog().debug("build-image goal could not be applied to pom project.");
+ return;
+ }
+ if (this.skip) {
+ getLog().debug("skipping build-image as per configuration.");
+ return;
+ }
+ buildImage();
+ }
+
+ private void buildImage() throws MojoExecutionException {
+ Libraries libraries = getLibraries(Collections.emptySet());
+ try {
+ Builder builder = new Builder(new MojoBuildLog(this::getLog));
+ BuildRequest request = getBuildRequest(libraries);
+ builder.build(request);
+ }
+ catch (IOException ex) {
+ throw new MojoExecutionException(ex.getMessage(), ex);
+ }
+ }
+
+ private BuildRequest getBuildRequest(Libraries libraries) {
+ Function content = (owner) -> getApplicationContent(owner, libraries);
+ return ((this.image != null) ? this.image : new Image()).getBuildRequest(this.project.getArtifact(), content);
+ }
+
+ private TarArchive getApplicationContent(Owner owner, Libraries libraries) {
+ ImagePackager packager = getConfiguredPackager(() -> new ImagePackager(getJarFile()));
+ return new PackagedTarArchive(owner, libraries, packager);
+ }
+
+ private File getJarFile() {
+ // We can use 'project.getArtifact().getFile()' because that was done in a
+ // forked lifecyle and is now null
+ StringBuilder name = new StringBuilder(this.finalName);
+ if (StringUtils.hasText(this.classifier)) {
+ name.append("-").append(this.classifier);
+ }
+ name.append(".jar");
+ return new File(this.sourceDirectory, name.toString());
+ }
+
+ /**
+ * {@link BuildLog} backed by Mojo logging.
+ */
+ private static class MojoBuildLog extends AbstractBuildLog {
+
+ private static final long THRESHOLD = Duration.ofSeconds(2).toMillis();
+
+ private final Supplier log;
+
+ MojoBuildLog(Supplier log) {
+ this.log = log;
+ }
+
+ @Override
+ protected void log(String message) {
+ this.log.get().info(message);
+ }
+
+ @Override
+ protected Consumer getProgressConsumer(String message) {
+ return new ProgressLog(message);
+ }
+
+ private class ProgressLog implements Consumer {
+
+ private final String message;
+
+ private long last;
+
+ ProgressLog(String message) {
+ this.message = message;
+ this.last = System.currentTimeMillis();
+ }
+
+ @Override
+ public void accept(TotalProgressEvent progress) {
+ log(progress.getPercent());
+ }
+
+ private void log(int percent) {
+ if (percent == 100 || (System.currentTimeMillis() - this.last) > THRESHOLD) {
+ MojoBuildLog.this.log.get().info(this.message + " " + percent + "%");
+ this.last = System.currentTimeMillis();
+ }
+ }
+
+ }
+
+ }
+
+ /**
+ * Adapter class to expose the packaged jar as a {@link TarArchive}.
+ */
+ static class PackagedTarArchive implements TarArchive {
+
+ static final long NORMALIZED_MOD_TIME = TarArchive.NORMALIZED_TIME.toEpochMilli();
+
+ private final Owner owner;
+
+ private final Libraries libraries;
+
+ private final ImagePackager packager;
+
+ PackagedTarArchive(Owner owner, Libraries libraries, ImagePackager packager) {
+ this.owner = owner;
+ this.libraries = libraries;
+ this.packager = packager;
+ }
+
+ @Override
+ public void writeTo(OutputStream outputStream) throws IOException {
+ TarArchiveOutputStream tar = new TarArchiveOutputStream(outputStream);
+ this.packager.packageImage(this.libraries, (entry, entryWriter) -> write(entry, entryWriter, tar));
+ }
+
+ private void write(ZipEntry jarEntry, EntryWriter entryWriter, TarArchiveOutputStream tar) {
+ try {
+ TarArchiveEntry tarEntry = convert(jarEntry);
+ tar.putArchiveEntry(tarEntry);
+ if (tarEntry.isFile()) {
+ entryWriter.write(tar);
+ }
+ tar.closeArchiveEntry();
+ }
+ catch (IOException ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+
+ private TarArchiveEntry convert(ZipEntry entry) {
+ byte linkFlag = (entry.isDirectory()) ? TarConstants.LF_DIR : TarConstants.LF_NORMAL;
+ TarArchiveEntry tarEntry = new TarArchiveEntry(entry.getName(), linkFlag, true);
+ tarEntry.setUserId(this.owner.getUid());
+ tarEntry.setGroupId(this.owner.getGid());
+ tarEntry.setModTime(NORMALIZED_MOD_TIME);
+ if (!entry.isDirectory()) {
+ tarEntry.setSize(entry.getSize());
+ }
+ return tarEntry;
+ }
+
+ }
+
+}
diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java
new file mode 100644
index 0000000000..de0eeb2d64
--- /dev/null
+++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2012-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
+ *
+ * https://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.springframework.boot.maven;
+
+import java.util.Map;
+import java.util.function.Function;
+
+import org.apache.maven.artifact.Artifact;
+
+import org.springframework.boot.cloudnativebuildpack.build.BuildRequest;
+import org.springframework.boot.cloudnativebuildpack.docker.type.ImageName;
+import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference;
+import org.springframework.boot.cloudnativebuildpack.io.Owner;
+import org.springframework.boot.cloudnativebuildpack.io.TarArchive;
+import org.springframework.util.StringUtils;
+
+/**
+ * Image configuration options.
+ *
+ * @author Phillip Webb
+ * @since 2.3.0
+ */
+public class Image {
+
+ /**
+ * The name of the created image.
+ */
+ String name;
+
+ /**
+ * The builder used to create the image.
+ */
+ String builder;
+
+ /**
+ * Environment properties that should be passed to the builder.
+ */
+ Map env;
+
+ /**
+ * If the cache should be cleaned before building.
+ */
+ boolean cleanCache;
+
+ /**
+ * If verbose logging is required.
+ */
+ boolean verboseLogging;
+
+ BuildRequest getBuildRequest(Artifact artifact, Function applicationContent) {
+ return customize(BuildRequest.of(getOrDeduceName(artifact), applicationContent));
+ }
+
+ private ImageReference getOrDeduceName(Artifact artifact) {
+ if (StringUtils.hasText(this.name)) {
+ return ImageReference.of(this.name);
+ }
+ ImageName imageName = ImageName.of(artifact.getArtifactId());
+ return ImageReference.of(imageName, artifact.getVersion());
+ }
+
+ private BuildRequest customize(BuildRequest request) {
+ if (StringUtils.hasText(this.builder)) {
+ request = request.withBuilder(ImageReference.of(this.builder));
+ }
+ if (this.env != null && !this.env.isEmpty()) {
+ request = request.withEnv(this.env);
+ }
+ request = request.withCleanCache(this.cleanCache);
+ request = request.withVerboseLogging(this.verboseLogging);
+ return request;
+ }
+
+}
diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/LayoutType.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/LayoutType.java
new file mode 100644
index 0000000000..7029c30bc9
--- /dev/null
+++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/LayoutType.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2012-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
+ *
+ * https://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.springframework.boot.maven;
+
+import org.springframework.boot.loader.tools.Layout;
+import org.springframework.boot.loader.tools.Layouts.Expanded;
+import org.springframework.boot.loader.tools.Layouts.Jar;
+import org.springframework.boot.loader.tools.Layouts.None;
+import org.springframework.boot.loader.tools.Layouts.War;
+
+/**
+ * Archive layout types.
+ *
+ * @author Phillip Webb
+ * @since 2.3.0
+ */
+public enum LayoutType {
+
+ /**
+ * Jar Layout.
+ */
+ JAR(new Jar()),
+
+ /**
+ * War Layout.
+ */
+ WAR(new War()),
+
+ /**
+ * Zip Layout.
+ */
+ ZIP(new Expanded()),
+
+ /**
+ * Dir Layout.
+ */
+ DIR(new Expanded()),
+
+ /**
+ * No Layout.
+ */
+ NONE(new None());
+
+ private final Layout layout;
+
+ LayoutType(Layout layout) {
+ this.layout = layout;
+ }
+
+ public Layout layout() {
+ return this.layout;
+ }
+
+}
diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/LoggingMainClassTimeoutWarningListener.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/LoggingMainClassTimeoutWarningListener.java
new file mode 100644
index 0000000000..7e2b8d4756
--- /dev/null
+++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/LoggingMainClassTimeoutWarningListener.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2012-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
+ *
+ * https://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.springframework.boot.maven;
+
+import java.util.function.Supplier;
+
+import org.apache.maven.plugin.logging.Log;
+
+import org.springframework.boot.loader.tools.Packager.MainClassTimeoutWarningListener;
+
+/**
+ * {@link MainClassTimeoutWarningListener} backed by a supplied Maven {@link Log}.
+ *
+ * @author Phillip Webb
+ */
+class LoggingMainClassTimeoutWarningListener implements MainClassTimeoutWarningListener {
+
+ private final Supplier log;
+
+ LoggingMainClassTimeoutWarningListener(Supplier log) {
+ this.log = log;
+ }
+
+ @Override
+ public void handleTimeoutWarning(long duration, String mainMethod) {
+ this.log.get().warn("Searching for the main-class is taking some time, "
+ + "consider using the mainClass configuration parameter");
+ }
+
+}
diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java
index 642cf96587..2c417df2d2 100644
--- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java
+++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java
@@ -18,38 +18,23 @@ package org.springframework.boot.maven;
import java.io.File;
import java.io.IOException;
-import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
-import java.util.Set;
import java.util.regex.Pattern;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.model.Dependency;
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.plugins.annotations.ResolutionScope;
-import org.apache.maven.project.MavenProject;
-import org.apache.maven.project.MavenProjectHelper;
-import org.apache.maven.shared.artifact.filter.collection.ArtifactsFilter;
-import org.apache.maven.shared.artifact.filter.collection.ScopeFilter;
import org.springframework.boot.loader.tools.DefaultLaunchScript;
import org.springframework.boot.loader.tools.LaunchScript;
-import org.springframework.boot.loader.tools.Layout;
-import org.springframework.boot.loader.tools.LayoutFactory;
-import org.springframework.boot.loader.tools.Layouts.Expanded;
-import org.springframework.boot.loader.tools.Layouts.Jar;
-import org.springframework.boot.loader.tools.Layouts.LayeredJar;
-import org.springframework.boot.loader.tools.Layouts.None;
-import org.springframework.boot.loader.tools.Layouts.War;
import org.springframework.boot.loader.tools.Libraries;
import org.springframework.boot.loader.tools.Repackager;
-import org.springframework.boot.loader.tools.Repackager.MainClassTimeoutWarningListener;
/**
* Repackage existing JAR and WAR archives so that they can be executed from the command
@@ -65,24 +50,10 @@ import org.springframework.boot.loader.tools.Repackager.MainClassTimeoutWarningL
@Mojo(name = "repackage", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true, threadSafe = true,
requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME,
requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME)
-public class RepackageMojo extends AbstractDependencyFilterMojo {
+public class RepackageMojo extends AbstractPackagerMojo {
private static final Pattern WHITE_SPACE_PATTERN = Pattern.compile("\\s+");
- /**
- * The Maven project.
- * @since 1.0.0
- */
- @Parameter(defaultValue = "${project}", readonly = true, required = true)
- private MavenProject project;
-
- /**
- * Maven project helper utils.
- * @since 1.0.0
- */
- @Component
- private MavenProjectHelper projectHelper;
-
/**
* Directory containing the generated archive.
* @since 1.0.0
@@ -131,32 +102,6 @@ public class RepackageMojo extends AbstractDependencyFilterMojo {
@Parameter(defaultValue = "true")
private boolean attach = true;
- /**
- * The name of the main class. If not specified the first compiled class found that
- * contains a 'main' method will be used.
- * @since 1.0.0
- */
- @Parameter
- private String mainClass;
-
- /**
- * The type of archive (which corresponds to how the dependencies are laid out inside
- * it). Possible values are JAR, LAYERED_JAR, WAR, ZIP, DIR, NONE. Defaults to a guess
- * based on the archive type.
- * @since 1.0.0
- */
- @Parameter(property = "spring-boot.repackage.layout")
- private LayoutType layout;
-
- /**
- * The layout factory that will be used to create the executable archive if no
- * explicit layout is set. Alternative layouts implementations can be provided by 3rd
- * parties.
- * @since 1.5.0
- */
- @Parameter
- private LayoutFactory layoutFactory;
-
/**
* A list of the libraries that must be unpacked from fat jars in order to run.
* Specify each library as a {@code } with a {@code } and a
@@ -195,20 +140,6 @@ public class RepackageMojo extends AbstractDependencyFilterMojo {
@Parameter
private Properties embeddedLaunchScriptProperties;
- /**
- * Exclude Spring Boot devtools from the repackaged archive.
- * @since 1.3.0
- */
- @Parameter(property = "spring-boot.repackage.excludeDevtools", defaultValue = "true")
- private boolean excludeDevtools = true;
-
- /**
- * Include system scoped dependencies.
- * @since 1.4.0
- */
- @Parameter(defaultValue = "false")
- public boolean includeSystemScope;
-
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
if (this.project.getPackaging().equals("pom")) {
@@ -226,8 +157,7 @@ public class RepackageMojo extends AbstractDependencyFilterMojo {
Artifact source = getSourceArtifact();
File target = getTargetFile();
Repackager repackager = getRepackager(source.getFile());
- Set artifacts = filterDependencies(this.project.getArtifacts(), getFilters(getAdditionalFilters()));
- Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack, getLog());
+ Libraries libraries = getLibraries(this.requiresUnpack);
try {
LaunchScript launchScript = getLaunchScript();
repackager.repackage(target, libraries, launchScript);
@@ -274,29 +204,7 @@ public class RepackageMojo extends AbstractDependencyFilterMojo {
}
private Repackager getRepackager(File source) {
- Repackager repackager = new Repackager(source, this.layoutFactory);
- repackager.addMainClassTimeoutWarningListener(new LoggingMainClassTimeoutWarningListener());
- repackager.setMainClass(this.mainClass);
- if (this.layout != null) {
- getLog().info("Layout: " + this.layout);
- repackager.setLayout(this.layout.layout());
- }
- return repackager;
- }
-
- private ArtifactsFilter[] getAdditionalFilters() {
- List filters = new ArrayList<>();
- if (this.excludeDevtools) {
- Exclude exclude = new Exclude();
- exclude.setGroupId("org.springframework.boot");
- exclude.setArtifactId("spring-boot-devtools");
- ExcludeFilter filter = new ExcludeFilter(exclude);
- filters.add(filter);
- }
- if (!this.includeSystemScope) {
- filters.add(new ScopeFilter(null, Artifact.SCOPE_SYSTEM));
- }
- return filters.toArray(new ArtifactsFilter[0]);
+ return getConfiguredPackager(() -> new Repackager(source));
}
private LaunchScript getLaunchScript() throws IOException {
@@ -361,61 +269,4 @@ public class RepackageMojo extends AbstractDependencyFilterMojo {
}
}
- private class LoggingMainClassTimeoutWarningListener implements MainClassTimeoutWarningListener {
-
- @Override
- public void handleTimeoutWarning(long duration, String mainMethod) {
- getLog().warn("Searching for the main-class is taking some time, "
- + "consider using the mainClass configuration parameter");
- }
-
- }
-
- /**
- * Archive layout types.
- */
- public enum LayoutType {
-
- /**
- * Jar Layout.
- */
- JAR(new Jar()),
-
- /**
- * Layered Jar Layout.
- */
- LAYERED_JAR(new LayeredJar()),
-
- /**
- * War Layout.
- */
- WAR(new War()),
-
- /**
- * Zip Layout.
- */
- ZIP(new Expanded()),
-
- /**
- * Dir Layout.
- */
- DIR(new Expanded()),
-
- /**
- * No Layout.
- */
- NONE(new None());
-
- private final Layout layout;
-
- LayoutType(Layout layout) {
- this.layout = layout;
- }
-
- public Layout layout() {
- return this.layout;
- }
-
- }
-
}
diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java
new file mode 100644
index 0000000000..84123cf86e
--- /dev/null
+++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2012-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
+ *
+ * https://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.springframework.boot.maven;
+
+import java.util.Collections;
+import java.util.function.Function;
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.artifact.DefaultArtifact;
+import org.apache.maven.artifact.handler.DefaultArtifactHandler;
+import org.apache.maven.artifact.versioning.VersionRange;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.cloudnativebuildpack.build.BuildRequest;
+import org.springframework.boot.cloudnativebuildpack.io.Owner;
+import org.springframework.boot.cloudnativebuildpack.io.TarArchive;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.entry;
+
+/**
+ * Tests for {@link Image}.
+ *
+ * @author Phillip Webb
+ */
+class ImageTests {
+
+ @Test
+ void getBuildRequestWhenNameIsNullDeducesName() {
+ BuildRequest request = new Image().getBuildRequest(createArtifact(), mockAplicationContent());
+ assertThat(request.getName().toString()).isEqualTo("docker.io/library/my-app:0.0.1-SNAPSHOT");
+ }
+
+ @Test
+ void getBuildEquestWhenNameIsSetUsesName() {
+ Image image = new Image();
+ image.name = "demo";
+ BuildRequest request = image.getBuildRequest(createArtifact(), mockAplicationContent());
+ assertThat(request.getName().toString()).isEqualTo("docker.io/library/demo:latest");
+ }
+
+ @Test
+ void getBuildRequestWhenNoCustomizationsUsesDefaults() {
+ BuildRequest request = new Image().getBuildRequest(createArtifact(), mockAplicationContent());
+ assertThat(request.getName().toString()).isEqualTo("docker.io/library/my-app:0.0.1-SNAPSHOT");
+ assertThat(request.getBuilder().toString()).isEqualTo("docker.io/cloudfoundry/cnb:0.0.43-bionic");
+ assertThat(request.getEnv()).isEmpty();
+ assertThat(request.isCleanCache()).isFalse();
+ assertThat(request.isVerboseLogging()).isFalse();
+ }
+
+ @Test
+ void getBuildRequestWhenHasBuilderUsesBuilder() {
+ Image image = new Image();
+ image.builder = "springboot/builder:2.2.x";
+ BuildRequest request = image.getBuildRequest(createArtifact(), mockAplicationContent());
+ assertThat(request.getBuilder().toString()).isEqualTo("docker.io/springboot/builder:2.2.x");
+ }
+
+ @Test
+ void getBuildRequestWhenHasEnvUsesEnv() {
+ Image image = new Image();
+ image.env = Collections.singletonMap("test", "test");
+ BuildRequest request = image.getBuildRequest(createArtifact(), mockAplicationContent());
+ assertThat(request.getEnv()).containsExactly(entry("test", "test"));
+ }
+
+ @Test
+ void getBuildRequestWhenHasCleanCacheUsesCleanCache() {
+ Image image = new Image();
+ image.cleanCache = true;
+ BuildRequest request = image.getBuildRequest(createArtifact(), mockAplicationContent());
+ assertThat(request.isCleanCache()).isTrue();
+ }
+
+ @Test
+ void getBuildRequestWhenHasVerboseLoggingUsesVerboseLogging() {
+ Image image = new Image();
+ image.verboseLogging = true;
+ BuildRequest request = image.getBuildRequest(createArtifact(), mockAplicationContent());
+ assertThat(request.isVerboseLogging()).isTrue();
+ }
+
+ private Artifact createArtifact() {
+ return new DefaultArtifact("com.example", "my-app", VersionRange.createFromVersion("0.0.1-SNAPSHOT"), "compile",
+ "jar", null, new DefaultArtifactHandler());
+ }
+
+ private Function mockAplicationContent() {
+ return (owner) -> null;
+ }
+
+}