Implement extract and list-layers command
Adds a new jarmode called 'tools'. This provides two commands, 'extract' and 'list-layers'. list-layers is the same as list from the layertools. extract is able to extract the JAR in four different modes: - CDS compatible extraction with libraries in a lib folder and a runner .jar - CDS compatible as above, but with layers - Launcher based - Launcher based with layers. This is essentially the same as extract from the layertools The commands in layertools have been deprecated in favor of the commands in 'tools'. This also changes the behavior of layers.enabled from the Gradle and Maven plugin: before this commit, layers.enabled prevents the inclusion of the layer index file as well as the layertools JAR. After this commit, layers.enabled only prevents the inclusion of the layer index file. layer.includeLayerTools have been deprecated in favor of includeTools, and the layertools JAR has been renamed to tools. Closes gh-38276
This commit is contained in:
parent
2c4fb5baaa
commit
793aca60d2
@ -57,7 +57,7 @@ include "spring-boot-project:spring-boot-tools:spring-boot-configuration-metadat
|
||||
include "spring-boot-project:spring-boot-tools:spring-boot-configuration-processor"
|
||||
include "spring-boot-project:spring-boot-tools:spring-boot-gradle-plugin"
|
||||
include "spring-boot-project:spring-boot-tools:spring-boot-gradle-test-support"
|
||||
include "spring-boot-project:spring-boot-tools:spring-boot-jarmode-layertools"
|
||||
include "spring-boot-project:spring-boot-tools:spring-boot-jarmode-tools"
|
||||
include "spring-boot-project:spring-boot-tools:spring-boot-loader"
|
||||
include "spring-boot-project:spring-boot-tools:spring-boot-loader-classic"
|
||||
include "spring-boot-project:spring-boot-tools:spring-boot-loader-tools"
|
||||
|
@ -1612,7 +1612,7 @@ bom {
|
||||
"spring-boot-configuration-processor",
|
||||
"spring-boot-devtools",
|
||||
"spring-boot-docker-compose",
|
||||
"spring-boot-jarmode-layertools",
|
||||
"spring-boot-jarmode-tools",
|
||||
"spring-boot-loader",
|
||||
"spring-boot-loader-classic",
|
||||
"spring-boot-loader-tools",
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@ -39,6 +39,7 @@ import org.springframework.boot.loader.tools.LoaderImplementation;
|
||||
* A Spring Boot "fat" archive task.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Moritz Halbritter
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public interface BootArchive extends Task {
|
||||
@ -144,4 +145,13 @@ public interface BootArchive extends Task {
|
||||
@Optional
|
||||
Property<LoaderImplementation> getLoaderImplementation();
|
||||
|
||||
/**
|
||||
* Returns whether the JAR tools should be included as a dependency in the layered
|
||||
* archive.
|
||||
* @return whether the JAR tools should be included
|
||||
* @since 3.3.0
|
||||
*/
|
||||
@Input
|
||||
Property<Boolean> getIncludeTools();
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@ -129,7 +129,7 @@ class BootArchiveSupport {
|
||||
|
||||
CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies,
|
||||
LoaderImplementation loaderImplementation, boolean supportsSignatureFile, LayerResolver layerResolver,
|
||||
String layerToolsLocation) {
|
||||
String jarmodeToolsLocation) {
|
||||
File output = jar.getArchiveFile().get().getAsFile();
|
||||
Manifest manifest = jar.getManifest();
|
||||
boolean preserveFileTimestamps = jar.isPreserveFileTimestamps();
|
||||
@ -143,7 +143,7 @@ class BootArchiveSupport {
|
||||
Function<FileCopyDetails, ZipCompression> compressionResolver = this.compressionResolver;
|
||||
String encoding = jar.getMetadataCharset();
|
||||
CopyAction action = new BootZipCopyAction(output, manifest, preserveFileTimestamps, dirMode, fileMode,
|
||||
includeDefaultLoader, layerToolsLocation, requiresUnpack, exclusions, launchScript, librarySpec,
|
||||
includeDefaultLoader, jarmodeToolsLocation, requiresUnpack, exclusions, launchScript, librarySpec,
|
||||
compressionResolver, encoding, resolvedDependencies, supportsSignatureFile, layerResolver,
|
||||
loaderImplementation);
|
||||
return jar.isReproducibleFileOrder() ? new ReproducibleOrderingCopyAction(action) : action;
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@ -88,6 +88,7 @@ public abstract class BootJar extends Jar implements BootArchive {
|
||||
this.projectName = project.provider(project::getName);
|
||||
this.projectVersion = project.provider(project::getVersion);
|
||||
this.resolvedDependencies = new ResolvedDependencies(project);
|
||||
getIncludeTools().convention(true);
|
||||
}
|
||||
|
||||
private void configureBootInfSpec(CopySpec bootInfSpec) {
|
||||
@ -144,13 +145,21 @@ public abstract class BootJar extends Jar implements BootArchive {
|
||||
@Override
|
||||
protected CopyAction createCopyAction() {
|
||||
LoaderImplementation loaderImplementation = getLoaderImplementation().getOrElse(LoaderImplementation.DEFAULT);
|
||||
LayerResolver layerResolver = null;
|
||||
if (!isLayeredDisabled()) {
|
||||
LayerResolver layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary);
|
||||
String layerToolsLocation = this.layered.getIncludeLayerTools().get() ? LIB_DIRECTORY : null;
|
||||
return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, true,
|
||||
layerResolver, layerToolsLocation);
|
||||
layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary);
|
||||
}
|
||||
return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, true);
|
||||
String jarmodeToolsLocation = isIncludeJarmodeTools() ? LIB_DIRECTORY : null;
|
||||
return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, true, layerResolver,
|
||||
jarmodeToolsLocation);
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
private boolean isIncludeJarmodeTools() {
|
||||
if (!this.getIncludeTools().get()) {
|
||||
return false;
|
||||
}
|
||||
return this.layered.getIncludeLayerTools().get();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@ -87,6 +87,7 @@ public abstract class BootWar extends War implements BootArchive {
|
||||
this.projectName = project.provider(project::getName);
|
||||
this.projectVersion = project.provider(project::getVersion);
|
||||
this.resolvedDependencies = new ResolvedDependencies(project);
|
||||
getIncludeTools().convention(true);
|
||||
}
|
||||
|
||||
private Object getProvidedLibFiles() {
|
||||
@ -118,13 +119,21 @@ public abstract class BootWar extends War implements BootArchive {
|
||||
@Override
|
||||
protected CopyAction createCopyAction() {
|
||||
LoaderImplementation loaderImplementation = getLoaderImplementation().getOrElse(LoaderImplementation.DEFAULT);
|
||||
LayerResolver layerResolver = null;
|
||||
if (!isLayeredDisabled()) {
|
||||
LayerResolver layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary);
|
||||
String layerToolsLocation = this.layered.getIncludeLayerTools().get() ? LIB_DIRECTORY : null;
|
||||
return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, false,
|
||||
layerResolver, layerToolsLocation);
|
||||
layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary);
|
||||
}
|
||||
return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, false);
|
||||
String jarmodeToolsLocation = isIncludeJarmodeTools() ? LIB_DIRECTORY : null;
|
||||
return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, false,
|
||||
layerResolver, jarmodeToolsLocation);
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
private boolean isIncludeJarmodeTools() {
|
||||
if (!this.getIncludeTools().get()) {
|
||||
return false;
|
||||
}
|
||||
return this.layered.getIncludeLayerTools().get();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -96,7 +96,7 @@ class BootZipCopyAction implements CopyAction {
|
||||
|
||||
private final boolean includeDefaultLoader;
|
||||
|
||||
private final String layerToolsLocation;
|
||||
private final String jarmodeToolsLocation;
|
||||
|
||||
private final Spec<FileTreeElement> requiresUnpack;
|
||||
|
||||
@ -119,7 +119,7 @@ class BootZipCopyAction implements CopyAction {
|
||||
private final LoaderImplementation loaderImplementation;
|
||||
|
||||
BootZipCopyAction(File output, Manifest manifest, boolean preserveFileTimestamps, Integer dirMode, Integer fileMode,
|
||||
boolean includeDefaultLoader, String layerToolsLocation, Spec<FileTreeElement> requiresUnpack,
|
||||
boolean includeDefaultLoader, String jarmodeToolsLocation, Spec<FileTreeElement> requiresUnpack,
|
||||
Spec<FileTreeElement> exclusions, LaunchScriptConfiguration launchScript, Spec<FileCopyDetails> librarySpec,
|
||||
Function<FileCopyDetails, ZipCompression> compressionResolver, String encoding,
|
||||
ResolvedDependencies resolvedDependencies, boolean supportsSignatureFile, LayerResolver layerResolver,
|
||||
@ -130,7 +130,7 @@ class BootZipCopyAction implements CopyAction {
|
||||
this.dirMode = dirMode;
|
||||
this.fileMode = fileMode;
|
||||
this.includeDefaultLoader = includeDefaultLoader;
|
||||
this.layerToolsLocation = layerToolsLocation;
|
||||
this.jarmodeToolsLocation = jarmodeToolsLocation;
|
||||
this.requiresUnpack = requiresUnpack;
|
||||
this.exclusions = exclusions;
|
||||
this.launchScript = launchScript;
|
||||
@ -342,8 +342,8 @@ class BootZipCopyAction implements CopyAction {
|
||||
}
|
||||
|
||||
private void writeJarToolsIfNecessary() throws IOException {
|
||||
if (BootZipCopyAction.this.layerToolsLocation != null) {
|
||||
writeJarModeLibrary(BootZipCopyAction.this.layerToolsLocation, JarModeLibrary.LAYER_TOOLS);
|
||||
if (BootZipCopyAction.this.jarmodeToolsLocation != null) {
|
||||
writeJarModeLibrary(BootZipCopyAction.this.jarmodeToolsLocation, JarModeLibrary.TOOLS);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@ -72,8 +72,10 @@ public abstract class LayeredSpec {
|
||||
* archive.
|
||||
* @return whether the layer tools should be included
|
||||
* @since 3.0.0
|
||||
* @deprecated since 3.3.0 for removal in 3.5.0 in favor of {@code includeTools}.
|
||||
*/
|
||||
@Input
|
||||
@Deprecated(since = "3.3.0", forRemoval = true)
|
||||
public abstract Property<Boolean> getIncludeLayerTools();
|
||||
|
||||
/**
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@ -66,6 +66,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
* @author Andy Wilkinson
|
||||
* @author Madhura Bhave
|
||||
* @author Scott Frederick
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
abstract class AbstractBootArchiveIntegrationTests {
|
||||
|
||||
@ -332,6 +333,18 @@ abstract class AbstractBootArchiveIntegrationTests {
|
||||
.getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void notUpToDateWhenBuiltWithToolsAndThenWithoutTools() {
|
||||
assertThat(this.gradleBuild.scriptProperty("includeTools", "")
|
||||
.build(this.taskName)
|
||||
.task(":" + this.taskName)
|
||||
.getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
assertThat(this.gradleBuild.scriptProperty("includeTools", "includeTools = false")
|
||||
.build(this.taskName)
|
||||
.task(":" + this.taskName)
|
||||
.getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void layersWithCustomSourceSet() {
|
||||
assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome())
|
||||
@ -345,7 +358,7 @@ abstract class AbstractBootArchiveIntegrationTests {
|
||||
assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome())
|
||||
.isEqualTo(TaskOutcome.SUCCESS);
|
||||
Map<String, List<String>> indexedLayers;
|
||||
String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName();
|
||||
String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName();
|
||||
try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) {
|
||||
assertThat(jarFile.getEntry(layerToolsJar)).isNotNull();
|
||||
assertThat(jarFile.getEntry(this.libPath + "commons-lang3-3.9.jar")).isNotNull();
|
||||
@ -397,7 +410,7 @@ abstract class AbstractBootArchiveIntegrationTests {
|
||||
assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome())
|
||||
.isEqualTo(TaskOutcome.SUCCESS);
|
||||
Map<String, List<String>> indexedLayers;
|
||||
String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName();
|
||||
String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName();
|
||||
try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) {
|
||||
assertThat(jarFile.getEntry(layerToolsJar)).isNotNull();
|
||||
assertThat(jarFile.getEntry(this.libPath + "alpha-1.2.3.jar")).isNotNull();
|
||||
@ -443,7 +456,7 @@ abstract class AbstractBootArchiveIntegrationTests {
|
||||
BuildResult build = this.gradleBuild.build(this.taskName);
|
||||
assertThat(build.task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
Map<String, List<String>> indexedLayers;
|
||||
String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName();
|
||||
String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName();
|
||||
try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) {
|
||||
assertThat(jarFile.getEntry(layerToolsJar)).isNotNull();
|
||||
assertThat(jarFile.getEntry(this.libPath + "commons-lang3-3.9.jar")).isNotNull();
|
||||
@ -490,7 +503,7 @@ abstract class AbstractBootArchiveIntegrationTests {
|
||||
BuildResult build = this.gradleBuild.build(this.taskName);
|
||||
assertThat(build.task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
Map<String, List<String>> indexedLayers;
|
||||
String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName();
|
||||
String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName();
|
||||
try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) {
|
||||
assertThat(jarFile.getEntry(layerToolsJar)).isNotNull();
|
||||
assertThat(jarFile.getEntry(this.libPath + "alpha-1.2.3.jar")).isNotNull();
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@ -80,6 +80,7 @@ import static org.mockito.Mockito.mock;
|
||||
* @param <T> the type of the concrete BootArchive implementation
|
||||
* @author Andy Wilkinson
|
||||
* @author Scott Frederick
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
|
||||
|
||||
@ -496,7 +497,7 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
|
||||
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Lib")).isEqualTo(this.libPath);
|
||||
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Layers-Index"))
|
||||
.isEqualTo(this.indexPath + "layers.idx");
|
||||
assertThat(getEntryNames(jarFile)).contains(this.libPath + JarModeLibrary.LAYER_TOOLS.getName());
|
||||
assertThat(getEntryNames(jarFile)).contains(this.libPath + JarModeLibrary.TOOLS.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@ -530,7 +531,7 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
|
||||
List<String> index = entryLines(jarFile, this.indexPath + "layers.idx");
|
||||
assertThat(getLayerNames(index)).containsExactly("dependencies", "spring-boot-loader",
|
||||
"snapshot-dependencies", "application");
|
||||
String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName();
|
||||
String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName();
|
||||
List<String> expected = new ArrayList<>();
|
||||
expected.add("- \"dependencies\":");
|
||||
expected.add(" - \"" + this.libPath + "first-library.jar\"");
|
||||
@ -584,7 +585,7 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
|
||||
List<String> index = entryLines(jarFile, this.indexPath + "layers.idx");
|
||||
assertThat(getLayerNames(index)).containsExactly("my-deps", "my-internal-deps", "my-snapshot-deps",
|
||||
"resources", "application");
|
||||
String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName();
|
||||
String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName();
|
||||
List<String> expected = new ArrayList<>();
|
||||
expected.add("- \"my-deps\":");
|
||||
expected.add(" - \"" + layerToolsJar + "\"");
|
||||
@ -614,15 +615,32 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
|
||||
@Test
|
||||
void whenArchiveIsLayeredThenLayerToolsAreAddedToTheJar() throws IOException {
|
||||
List<String> entryNames = getEntryNames(createLayeredJar());
|
||||
assertThat(entryNames).contains(this.libPath + JarModeLibrary.LAYER_TOOLS.getName());
|
||||
assertThat(entryNames).contains(this.libPath + JarModeLibrary.TOOLS.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAddToolsToTheJar() throws IOException {
|
||||
this.task.getMainClass().set("com.example.Main");
|
||||
executeTask();
|
||||
List<String> entryNames = getEntryNames(this.task.getArchiveFile().get().getAsFile());
|
||||
assertThat(entryNames).isNotEmpty().contains(this.libPath + JarModeLibrary.TOOLS.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("removal")
|
||||
void whenArchiveIsLayeredAndIncludeLayerToolsIsFalseThenLayerToolsAreNotAddedToTheJar() throws IOException {
|
||||
List<String> entryNames = getEntryNames(
|
||||
createLayeredJar((configuration) -> configuration.getIncludeLayerTools().set(false)));
|
||||
assertThat(entryNames).isNotEmpty()
|
||||
.doesNotContain(this.indexPath + "layers/dependencies/lib/spring-boot-jarmode-layertools.jar");
|
||||
assertThat(entryNames).isNotEmpty().doesNotContain(this.libPath + JarModeLibrary.TOOLS.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenIncludeToolsIsFalseThenToolsAreNotAddedToTheJar() throws IOException {
|
||||
this.task.getIncludeTools().set(false);
|
||||
this.task.getMainClass().set("com.example.Main");
|
||||
executeTask();
|
||||
List<String> entryNames = getEntryNames(this.task.getArchiveFile().get().getAsFile());
|
||||
assertThat(entryNames).isNotEmpty().doesNotContain(this.libPath + JarModeLibrary.TOOLS.getName());
|
||||
}
|
||||
|
||||
protected File jarFile(String name) throws IOException {
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@ -67,7 +67,7 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
|
||||
assertThat(output).containsPattern("1\\. .*classes");
|
||||
assertThat(output).containsPattern("2\\. .*library-1.0-SNAPSHOT.jar");
|
||||
assertThat(output).containsPattern("3\\. .*commons-lang3-3.9.jar");
|
||||
assertThat(output).containsPattern("4\\. .*spring-boot-jarmode-layertools.*.jar");
|
||||
assertThat(output).containsPattern("4\\. .*spring-boot-jarmode-tools.*.jar");
|
||||
assertThat(output).doesNotContain("5. ");
|
||||
}
|
||||
|
||||
@ -77,7 +77,7 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
|
||||
BuildResult result = this.gradleBuild.build("launch");
|
||||
String output = result.getOutput();
|
||||
assertThat(output).containsPattern("1\\. .*classes");
|
||||
assertThat(output).containsPattern("2\\. .*spring-boot-jarmode-layertools.*.jar");
|
||||
assertThat(output).containsPattern("2\\. .*spring-boot-jarmode-tools.*.jar");
|
||||
assertThat(output).containsPattern("3\\. .*library-1.0-SNAPSHOT.jar");
|
||||
assertThat(output).containsPattern("4\\. .*commons-lang3-3.9.jar");
|
||||
assertThat(output).doesNotContain("5. ");
|
||||
|
@ -38,12 +38,12 @@ dependencies {
|
||||
|
||||
task listLayers(type: JavaExec) {
|
||||
classpath = bootJar.outputs.files
|
||||
systemProperties = [ "jarmode": "layertools" ]
|
||||
args "list"
|
||||
systemProperties = [ "jarmode": "tools" ]
|
||||
args "list-layers"
|
||||
}
|
||||
|
||||
task extractLayers(type: JavaExec) {
|
||||
classpath = bootJar.outputs.files
|
||||
systemProperties = [ "jarmode": "layertools" ]
|
||||
args "extract"
|
||||
systemProperties = [ "jarmode": "tools" ]
|
||||
args "extract", "--layers", "--launcher"
|
||||
}
|
||||
|
@ -17,7 +17,5 @@ dependencies {
|
||||
}
|
||||
|
||||
bootJar {
|
||||
layered {
|
||||
enabled = false
|
||||
}
|
||||
includeTools = false
|
||||
}
|
||||
|
@ -18,7 +18,5 @@ dependencies {
|
||||
}
|
||||
|
||||
bootJar {
|
||||
layered {
|
||||
enabled = false
|
||||
}
|
||||
includeTools = false
|
||||
}
|
||||
|
@ -21,7 +21,5 @@ bootJar {
|
||||
}
|
||||
|
||||
bootJar {
|
||||
layered {
|
||||
enabled = false
|
||||
}
|
||||
includeTools = false
|
||||
}
|
||||
|
@ -21,12 +21,12 @@ dependencies {
|
||||
|
||||
task listLayers(type: JavaExec) {
|
||||
classpath = bootJar.outputs.files
|
||||
systemProperties = [ "jarmode": "layertools" ]
|
||||
args "list"
|
||||
systemProperties = [ "jarmode": "tools" ]
|
||||
args "list-layers"
|
||||
}
|
||||
|
||||
task extractLayers(type: JavaExec) {
|
||||
classpath = bootJar.outputs.files
|
||||
systemProperties = [ "jarmode": "layertools" ]
|
||||
args "extract"
|
||||
systemProperties = [ "jarmode": "tools" ]
|
||||
args "extract", "--layers", "--launcher"
|
||||
}
|
||||
|
@ -19,7 +19,5 @@ dependencies {
|
||||
}
|
||||
|
||||
bootJar {
|
||||
layered {
|
||||
enabled = false
|
||||
}
|
||||
includeTools = false
|
||||
}
|
||||
|
@ -24,12 +24,12 @@ dependencies {
|
||||
|
||||
task listLayers(type: JavaExec) {
|
||||
classpath = bootJar.outputs.files
|
||||
systemProperties = [ "jarmode": "layertools" ]
|
||||
args "list"
|
||||
systemProperties = [ "jarmode": "tools" ]
|
||||
args "list-layers"
|
||||
}
|
||||
|
||||
task extractLayers(type: JavaExec) {
|
||||
classpath = bootJar.outputs.files
|
||||
systemProperties = [ "jarmode": "layertools" ]
|
||||
args "extract"
|
||||
systemProperties = [ "jarmode": "tools" ]
|
||||
args "extract", "--layers", "--launcher"
|
||||
}
|
||||
|
@ -55,12 +55,12 @@ dependencies {
|
||||
|
||||
task listLayers(type: JavaExec) {
|
||||
classpath = bootJar.outputs.files
|
||||
systemProperties = [ "jarmode": "layertools" ]
|
||||
args "list"
|
||||
systemProperties = [ "jarmode": "tools" ]
|
||||
args "list-layers"
|
||||
}
|
||||
|
||||
task extractLayers(type: JavaExec) {
|
||||
classpath = bootJar.outputs.files
|
||||
systemProperties = [ "jarmode": "layertools" ]
|
||||
args "extract"
|
||||
systemProperties = [ "jarmode": "tools" ]
|
||||
args "extract", "--layers", "--launcher"
|
||||
}
|
||||
|
@ -33,12 +33,12 @@ dependencies {
|
||||
|
||||
task listLayers(type: JavaExec) {
|
||||
classpath = bootJar.outputs.files
|
||||
systemProperties = [ "jarmode": "layertools" ]
|
||||
args "list"
|
||||
systemProperties = [ "jarmode": "tools" ]
|
||||
args "list-layers"
|
||||
}
|
||||
|
||||
task extractLayers(type: JavaExec) {
|
||||
classpath = bootJar.outputs.files
|
||||
systemProperties = [ "jarmode": "layertools" ]
|
||||
args "extract"
|
||||
systemProperties = [ "jarmode": "tools" ]
|
||||
args "extract", "--layers", "--launcher"
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '{version}'
|
||||
}
|
||||
|
||||
bootJar {
|
||||
mainClass = 'com.example.Application'
|
||||
{includeTools}
|
||||
}
|
@ -18,7 +18,5 @@ dependencies {
|
||||
}
|
||||
|
||||
bootJar {
|
||||
layered {
|
||||
enabled = false
|
||||
}
|
||||
includeTools = false
|
||||
}
|
||||
|
@ -21,7 +21,5 @@ bootJar {
|
||||
}
|
||||
|
||||
bootJar {
|
||||
layered {
|
||||
enabled = false
|
||||
}
|
||||
includeTools = false
|
||||
}
|
||||
|
@ -39,12 +39,12 @@ dependencies {
|
||||
|
||||
task listLayers(type: JavaExec) {
|
||||
classpath = bootWar.outputs.files
|
||||
systemProperties = [ "jarmode": "layertools" ]
|
||||
args "list"
|
||||
systemProperties = [ "jarmode": "tools" ]
|
||||
args "list-layers"
|
||||
}
|
||||
|
||||
task extractLayers(type: JavaExec) {
|
||||
classpath = bootWar.outputs.files
|
||||
systemProperties = [ "jarmode": "layertools" ]
|
||||
args "extract"
|
||||
systemProperties = [ "jarmode": "tools" ]
|
||||
args "extract", "--layers", "--launcher"
|
||||
}
|
||||
|
@ -18,7 +18,5 @@ dependencies {
|
||||
}
|
||||
|
||||
bootWar {
|
||||
layered {
|
||||
enabled = false
|
||||
}
|
||||
includeTools = false
|
||||
}
|
||||
|
@ -21,7 +21,5 @@ bootWar {
|
||||
}
|
||||
|
||||
bootWar {
|
||||
layered {
|
||||
enabled = false
|
||||
}
|
||||
includeTools = false
|
||||
}
|
||||
|
@ -22,12 +22,12 @@ dependencies {
|
||||
|
||||
task listLayers(type: JavaExec) {
|
||||
classpath = bootWar.outputs.files
|
||||
systemProperties = [ "jarmode": "layertools" ]
|
||||
args "list"
|
||||
systemProperties = [ "jarmode": "tools" ]
|
||||
args "list-layers"
|
||||
}
|
||||
|
||||
task extractLayers(type: JavaExec) {
|
||||
classpath = bootWar.outputs.files
|
||||
systemProperties = [ "jarmode": "layertools" ]
|
||||
args "extract"
|
||||
systemProperties = [ "jarmode": "tools" ]
|
||||
args "extract", "--layers", "--launcher"
|
||||
}
|
||||
|
@ -19,7 +19,5 @@ dependencies {
|
||||
}
|
||||
|
||||
bootWar {
|
||||
layered {
|
||||
enabled = false
|
||||
}
|
||||
includeTools = false
|
||||
}
|
||||
|
@ -25,12 +25,12 @@ dependencies {
|
||||
|
||||
task listLayers(type: JavaExec) {
|
||||
classpath = bootWar.outputs.files
|
||||
systemProperties = [ "jarmode": "layertools" ]
|
||||
args "list"
|
||||
systemProperties = [ "jarmode": "tools" ]
|
||||
args "list-layers"
|
||||
}
|
||||
|
||||
task extractLayers(type: JavaExec) {
|
||||
classpath = bootWar.outputs.files
|
||||
systemProperties = [ "jarmode": "layertools" ]
|
||||
args "extract"
|
||||
systemProperties = [ "jarmode": "tools" ]
|
||||
args "extract", "--layers", "--launcher"
|
||||
}
|
||||
|
@ -56,12 +56,12 @@ dependencies {
|
||||
|
||||
task listLayers(type: JavaExec) {
|
||||
classpath = bootWar.outputs.files
|
||||
systemProperties = [ "jarmode": "layertools" ]
|
||||
args "list"
|
||||
systemProperties = [ "jarmode": "tools" ]
|
||||
args "list-layers"
|
||||
}
|
||||
|
||||
task extractLayers(type: JavaExec) {
|
||||
classpath = bootWar.outputs.files
|
||||
systemProperties = [ "jarmode": "layertools" ]
|
||||
args "extract"
|
||||
systemProperties = [ "jarmode": "tools" ]
|
||||
args "extract", "--layers", "--launcher"
|
||||
}
|
||||
|
@ -34,12 +34,12 @@ dependencies {
|
||||
|
||||
task listLayers(type: JavaExec) {
|
||||
classpath = bootWar.outputs.files
|
||||
systemProperties = [ "jarmode": "layertools" ]
|
||||
args "list"
|
||||
systemProperties = [ "jarmode": "tools" ]
|
||||
args "list-layers"
|
||||
}
|
||||
|
||||
task extractLayers(type: JavaExec) {
|
||||
classpath = bootWar.outputs.files
|
||||
systemProperties = [ "jarmode": "layertools" ]
|
||||
args "extract"
|
||||
systemProperties = [ "jarmode": "tools" ]
|
||||
args "extract", "--layers", "--launcher"
|
||||
}
|
||||
|
@ -0,0 +1,10 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '{version}'
|
||||
id 'war'
|
||||
}
|
||||
|
||||
bootWar {
|
||||
mainClass = 'com.example.Application'
|
||||
{includeTools}
|
||||
}
|
@ -18,7 +18,5 @@ dependencies {
|
||||
}
|
||||
|
||||
bootWar {
|
||||
layered {
|
||||
enabled = false
|
||||
}
|
||||
includeTools = false
|
||||
}
|
||||
|
@ -21,7 +21,5 @@ bootWar {
|
||||
}
|
||||
|
||||
bootWar {
|
||||
layered {
|
||||
enabled = false
|
||||
}
|
||||
includeTools = false
|
||||
}
|
||||
|
@ -1,119 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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.jarmode.layertools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.BasicFileAttributeView;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
/**
|
||||
* The {@code 'extract'} tools command.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ExtractCommand extends Command {
|
||||
|
||||
static final Option DESTINATION_OPTION = Option.of("destination", "string", "The destination to extract files to");
|
||||
|
||||
private final Context context;
|
||||
|
||||
private final Layers layers;
|
||||
|
||||
ExtractCommand(Context context) {
|
||||
this(context, Layers.get(context));
|
||||
}
|
||||
|
||||
ExtractCommand(Context context, Layers layers) {
|
||||
super("extract", "Extracts layers from the jar for image creation", Options.of(DESTINATION_OPTION),
|
||||
Parameters.of("[<layer>...]"));
|
||||
this.context = context;
|
||||
this.layers = layers;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run(Map<Option, String> options, List<String> parameters) {
|
||||
try {
|
||||
File destination = options.containsKey(DESTINATION_OPTION) ? new File(options.get(DESTINATION_OPTION))
|
||||
: this.context.getWorkingDir();
|
||||
for (String layer : this.layers) {
|
||||
if (parameters.isEmpty() || parameters.contains(layer)) {
|
||||
mkDirs(new File(destination, layer));
|
||||
}
|
||||
}
|
||||
try (ZipInputStream zip = new ZipInputStream(new FileInputStream(this.context.getArchiveFile()))) {
|
||||
ZipEntry entry = zip.getNextEntry();
|
||||
Assert.state(entry != null, "File '" + this.context.getArchiveFile().toString()
|
||||
+ "' is not compatible with layertools; ensure jar file is valid and launch script is not enabled");
|
||||
while (entry != null) {
|
||||
if (!entry.isDirectory()) {
|
||||
String layer = this.layers.getLayer(entry);
|
||||
if (parameters.isEmpty() || parameters.contains(layer)) {
|
||||
write(zip, entry, new File(destination, layer));
|
||||
}
|
||||
}
|
||||
entry = zip.getNextEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void write(ZipInputStream zip, ZipEntry entry, File destination) throws IOException {
|
||||
String canonicalOutputPath = destination.getCanonicalPath() + File.separator;
|
||||
File file = new File(destination, entry.getName());
|
||||
String canonicalEntryPath = file.getCanonicalPath();
|
||||
Assert.state(canonicalEntryPath.startsWith(canonicalOutputPath),
|
||||
() -> "Entry '" + entry.getName() + "' would be written to '" + canonicalEntryPath
|
||||
+ "'. This is outside the output location of '" + canonicalOutputPath
|
||||
+ "'. Verify the contents of your archive.");
|
||||
mkParentDirs(file);
|
||||
try (OutputStream out = new FileOutputStream(file)) {
|
||||
StreamUtils.copy(zip, out);
|
||||
}
|
||||
try {
|
||||
Files.getFileAttributeView(file.toPath(), BasicFileAttributeView.class)
|
||||
.setTimes(entry.getLastModifiedTime(), entry.getLastAccessTime(), entry.getCreationTime());
|
||||
}
|
||||
catch (IOException ex) {
|
||||
// File system does not support setting time attributes. Continue.
|
||||
}
|
||||
}
|
||||
|
||||
private void mkParentDirs(File file) throws IOException {
|
||||
mkDirs(file.getParentFile());
|
||||
}
|
||||
|
||||
private void mkDirs(File file) throws IOException {
|
||||
if (!file.exists() && !file.mkdirs()) {
|
||||
throw new IOException("Unable to create directory " + file);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
/*
|
||||
* 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.jarmode.layertools;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.loader.jarmode.JarMode;
|
||||
|
||||
/**
|
||||
* {@link JarMode} providing {@code "layertools"} support.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class LayerToolsJarMode implements JarMode {
|
||||
|
||||
@Override
|
||||
public boolean accepts(String mode) {
|
||||
return "layertools".equalsIgnoreCase(mode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(String mode, String[] args) {
|
||||
try {
|
||||
new Runner().run(args);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
static class Runner {
|
||||
|
||||
static Context contextOverride;
|
||||
|
||||
private final List<Command> commands;
|
||||
|
||||
private final HelpCommand help;
|
||||
|
||||
Runner() {
|
||||
Context context = (contextOverride != null) ? contextOverride : new Context();
|
||||
this.commands = getCommands(context);
|
||||
this.help = new HelpCommand(context, this.commands);
|
||||
}
|
||||
|
||||
private void run(String[] args) {
|
||||
run(dequeOf(args));
|
||||
}
|
||||
|
||||
private void run(Deque<String> args) {
|
||||
if (!args.isEmpty()) {
|
||||
String commandName = args.removeFirst();
|
||||
Command command = Command.find(this.commands, commandName);
|
||||
if (command != null) {
|
||||
runCommand(command, args);
|
||||
return;
|
||||
}
|
||||
printError("Unknown command \"" + commandName + "\"");
|
||||
}
|
||||
this.help.run(args);
|
||||
}
|
||||
|
||||
private void runCommand(Command command, Deque<String> args) {
|
||||
try {
|
||||
command.run(args);
|
||||
}
|
||||
catch (UnknownOptionException ex) {
|
||||
printError("Unknown option \"" + ex.getMessage() + "\" for the " + command.getName() + " command");
|
||||
this.help.run(dequeOf(command.getName()));
|
||||
}
|
||||
catch (MissingValueException ex) {
|
||||
printError("Option \"" + ex.getMessage() + "\" for the " + command.getName()
|
||||
+ " command requires a value");
|
||||
this.help.run(dequeOf(command.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
private void printError(String errorMessage) {
|
||||
System.out.println("Error: " + errorMessage);
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
private Deque<String> dequeOf(String... args) {
|
||||
return new ArrayDeque<>(Arrays.asList(args));
|
||||
}
|
||||
|
||||
static List<Command> getCommands(Context context) {
|
||||
List<Command> commands = new ArrayList<>();
|
||||
commands.add(new ListCommand(context));
|
||||
commands.add(new ExtractCommand(context));
|
||||
return Collections.unmodifiableList(commands);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
# Jar Modes
|
||||
org.springframework.boot.loader.jarmode.JarMode=\
|
||||
org.springframework.boot.jarmode.layertools.LayerToolsJarMode
|
@ -1,103 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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.jarmode.layertools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link HelpCommand}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class HelpCommandTests {
|
||||
|
||||
private HelpCommand command;
|
||||
|
||||
private TestPrintStream out;
|
||||
|
||||
@TempDir
|
||||
File temp;
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws Exception {
|
||||
Context context = mock(Context.class);
|
||||
given(context.getArchiveFile()).willReturn(createJarFile("test.jar"));
|
||||
this.command = new HelpCommand(context, LayerToolsJarMode.Runner.getCommands(context));
|
||||
this.out = new TestPrintStream(this);
|
||||
}
|
||||
|
||||
@Test
|
||||
void runWhenHasNoParametersPrintsUsage() {
|
||||
this.command.run(this.out, Collections.emptyList());
|
||||
assertThat(this.out).hasSameContentAsResource("help-output.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void runWhenHasNoCommandParameterPrintsUsage() {
|
||||
this.command.run(this.out, Arrays.asList("extract"));
|
||||
System.out.println(this.out);
|
||||
assertThat(this.out).hasSameContentAsResource("help-extract-output.txt");
|
||||
}
|
||||
|
||||
private File createJarFile(String name) throws Exception {
|
||||
File file = new File(this.temp, name);
|
||||
try (ZipOutputStream jarOutputStream = new ZipOutputStream(new FileOutputStream(file))) {
|
||||
jarOutputStream.putNextEntry(new JarEntry("META-INF/MANIFEST.MF"));
|
||||
jarOutputStream.write(getFile("test-manifest.MF").getBytes());
|
||||
jarOutputStream.closeEntry();
|
||||
JarEntry indexEntry = new JarEntry("BOOT-INF/layers.idx");
|
||||
jarOutputStream.putNextEntry(indexEntry);
|
||||
Writer writer = new OutputStreamWriter(jarOutputStream, StandardCharsets.UTF_8);
|
||||
writer.write("- \"0001\":\n");
|
||||
writer.write(" - \"BOOT-INF/lib/a.jar\"\n");
|
||||
writer.write(" - \"BOOT-INF/lib/b.jar\"\n");
|
||||
writer.write("- \"0002\":\n");
|
||||
writer.write(" - \"BOOT-INF/lib/c.jar\"\n");
|
||||
writer.write("- \"0003\":\n");
|
||||
writer.write(" - \"BOOT-INF/lib/d.jar\"\n");
|
||||
writer.flush();
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
private String getFile(String fileName) throws Exception {
|
||||
ClassPathResource resource = new ClassPathResource(fileName, getClass());
|
||||
InputStreamReader reader = new InputStreamReader(resource.getInputStream());
|
||||
return FileCopyUtils.copyToString(reader);
|
||||
}
|
||||
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
Extracts layers from the jar for image creation
|
||||
|
||||
Usage:
|
||||
java -Djarmode=layertools -jar test.jar extract [options] [<layer>...]
|
||||
|
||||
Options:
|
||||
--destination string The destination to extract files to
|
@ -4,7 +4,7 @@ plugins {
|
||||
id "org.springframework.boot.deployed"
|
||||
}
|
||||
|
||||
description = "Spring Boot Layers Tools"
|
||||
description = "Spring Boot Jarmode Tools"
|
||||
|
||||
dependencies {
|
||||
implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-classic"))
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@ -14,8 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.jarmode.layertools;
|
||||
package org.springframework.boot.jarmode.tools;
|
||||
|
||||
import java.io.PrintStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
@ -24,13 +25,15 @@ import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* A command that can be launched from the layertools jarmode.
|
||||
* A command that can be launched.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
abstract class Command {
|
||||
|
||||
@ -90,9 +93,10 @@ abstract class Command {
|
||||
|
||||
/**
|
||||
* Run the command by processing the remaining arguments.
|
||||
* @param out stream for command output
|
||||
* @param args a mutable deque of the remaining arguments
|
||||
*/
|
||||
final void run(Deque<String> args) {
|
||||
final void run(PrintStream out, Deque<String> args) {
|
||||
List<String> parameters = new ArrayList<>();
|
||||
Map<Option, String> options = new HashMap<>();
|
||||
while (!args.isEmpty()) {
|
||||
@ -105,15 +109,32 @@ abstract class Command {
|
||||
parameters.add(arg);
|
||||
}
|
||||
}
|
||||
run(options, parameters);
|
||||
run(out, options, parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the actual command.
|
||||
* @param out stream for command output
|
||||
* @param options any options extracted from the arguments
|
||||
* @param parameters any parameters extracted from the arguments
|
||||
*/
|
||||
protected abstract void run(Map<Option, String> options, List<String> parameters);
|
||||
abstract void run(PrintStream out, Map<Option, String> options, List<String> parameters);
|
||||
|
||||
/**
|
||||
* Whether the command is deprecated.
|
||||
* @return whether the command is deprecated
|
||||
*/
|
||||
boolean isDeprecated() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the deprecation message.
|
||||
* @return the deprecation message
|
||||
*/
|
||||
String getDeprecationMessage() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method that can be used to find a single command from a collection.
|
||||
@ -133,7 +154,7 @@ abstract class Command {
|
||||
/**
|
||||
* Parameters that the command accepts.
|
||||
*/
|
||||
protected static final class Parameters {
|
||||
static final class Parameters {
|
||||
|
||||
private final List<String> descriptions;
|
||||
|
||||
@ -158,7 +179,7 @@ abstract class Command {
|
||||
* Factory method used if there are no expected parameters.
|
||||
* @return a new {@link Parameters} instance
|
||||
*/
|
||||
protected static Parameters none() {
|
||||
static Parameters none() {
|
||||
return of();
|
||||
}
|
||||
|
||||
@ -168,7 +189,7 @@ abstract class Command {
|
||||
* @param descriptions the parameter descriptions
|
||||
* @return a new {@link Parameters} instance with the given descriptions
|
||||
*/
|
||||
protected static Parameters of(String... descriptions) {
|
||||
static Parameters of(String... descriptions) {
|
||||
return new Parameters(descriptions);
|
||||
}
|
||||
|
||||
@ -177,7 +198,7 @@ abstract class Command {
|
||||
/**
|
||||
* Options that the command accepts.
|
||||
*/
|
||||
protected static final class Options {
|
||||
static final class Options {
|
||||
|
||||
private final Option[] values;
|
||||
|
||||
@ -218,7 +239,7 @@ abstract class Command {
|
||||
* Factory method used if there are no expected options.
|
||||
* @return a new {@link Options} instance
|
||||
*/
|
||||
protected static Options none() {
|
||||
static Options none() {
|
||||
return of();
|
||||
}
|
||||
|
||||
@ -228,7 +249,7 @@ abstract class Command {
|
||||
* @param values the option values
|
||||
* @return a new {@link Options} instance with the given values
|
||||
*/
|
||||
protected static Options of(Option... values) {
|
||||
static Options of(Option... values) {
|
||||
return new Options(values);
|
||||
}
|
||||
|
||||
@ -237,9 +258,9 @@ abstract class Command {
|
||||
/**
|
||||
* An individual option that the command can accepts. Can either be an option with a
|
||||
* value (e.g. {@literal --log debug}) or a flag (e.g. {@literal
|
||||
* --verbose}).
|
||||
* --verbose}). It also can be both if the value is marked as optional.
|
||||
*/
|
||||
protected static final class Option {
|
||||
static final class Option {
|
||||
|
||||
private final String name;
|
||||
|
||||
@ -247,10 +268,13 @@ abstract class Command {
|
||||
|
||||
private final String description;
|
||||
|
||||
private Option(String name, String valueDescription, String description) {
|
||||
private final boolean optionalValue;
|
||||
|
||||
private Option(String name, String valueDescription, String description, boolean optionalValue) {
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.valueDescription = valueDescription;
|
||||
this.optionalValue = optionalValue;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -287,13 +311,24 @@ abstract class Command {
|
||||
}
|
||||
|
||||
private String claimArg(Deque<String> args) {
|
||||
if (this.valueDescription != null) {
|
||||
if (args.isEmpty()) {
|
||||
throw new MissingValueException(this.name);
|
||||
if (this.valueDescription == null) {
|
||||
return null;
|
||||
}
|
||||
if (this.optionalValue) {
|
||||
String nextArg = args.peek();
|
||||
if (nextArg == null || nextArg.startsWith("--")) {
|
||||
return null;
|
||||
}
|
||||
return args.removeFirst();
|
||||
}
|
||||
return null;
|
||||
else {
|
||||
try {
|
||||
return args.removeFirst();
|
||||
}
|
||||
catch (NoSuchElementException ex) {
|
||||
throw new MissingValueException(this.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -323,8 +358,8 @@ abstract class Command {
|
||||
* @param description a description of the option
|
||||
* @return a new {@link Option} instance
|
||||
*/
|
||||
protected static Option flag(String name, String description) {
|
||||
return new Option(name, null, description);
|
||||
static Option flag(String name, String description) {
|
||||
return new Option(name, null, description, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -334,8 +369,20 @@ abstract class Command {
|
||||
* @param description a description of the option
|
||||
* @return a new {@link Option} instance
|
||||
*/
|
||||
protected static Option of(String name, String valueDescription, String description) {
|
||||
return new Option(name, valueDescription, description);
|
||||
static Option of(String name, String valueDescription, String description) {
|
||||
return new Option(name, valueDescription, description, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create value option.
|
||||
* @param name the name of the option
|
||||
* @param valueDescription a description of the expected value
|
||||
* @param description a description of the option
|
||||
* @param optionalValue whether the value is optional
|
||||
* @return a new {@link Option} instance
|
||||
*/
|
||||
static Option of(String name, String valueDescription, String description, boolean optionalValue) {
|
||||
return new Option(name, valueDescription, description, optionalValue);
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.jarmode.layertools;
|
||||
package org.springframework.boot.jarmode.tools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
@ -0,0 +1,436 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.jarmode.tools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.BasicFileAttributeView;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarOutputStream;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import org.springframework.boot.jarmode.tools.JarStructure.Entry;
|
||||
import org.springframework.boot.jarmode.tools.JarStructure.Entry.Type;
|
||||
import org.springframework.boot.jarmode.tools.Layers.LayersNotEnabledException;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* The {@code 'extract'} tools command.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class ExtractCommand extends Command {
|
||||
|
||||
/**
|
||||
* Option to create a launcher.
|
||||
*/
|
||||
static final Option LAUNCHER_OPTION = Option.of("launcher", null, "Whether to extract the Spring Boot launcher");
|
||||
|
||||
/**
|
||||
* Option to extract layers.
|
||||
*/
|
||||
static final Option LAYERS_OPTION = Option.of("layers", "string list", "Layers to extract", true);
|
||||
|
||||
/**
|
||||
* Option to specify the destination to write to.
|
||||
*/
|
||||
static final Option DESTINATION_OPTION = Option.of("destination", "string",
|
||||
"Directory to extract files to. Defaults to the current working directory");
|
||||
|
||||
private static final Option LIBRARIES_DIRECTORY_OPTION = Option.of("libraries", "string",
|
||||
"Name of the libraries directory. Only applicable when not using --launcher. Defaults to lib/");
|
||||
|
||||
private static final Option RUNNER_FILENAME_OPTION = Option.of("runner-filename", "string",
|
||||
"Name of the runner JAR file. Only applicable when not using --launcher. Defaults to runner.jar");
|
||||
|
||||
private final Context context;
|
||||
|
||||
private final Layers layers;
|
||||
|
||||
ExtractCommand(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
ExtractCommand(Context context, Layers layers) {
|
||||
super("extract", "Extract the contents from the jar", Options.of(LAUNCHER_OPTION, LAYERS_OPTION,
|
||||
DESTINATION_OPTION, LIBRARIES_DIRECTORY_OPTION, RUNNER_FILENAME_OPTION), Parameters.none());
|
||||
this.context = context;
|
||||
this.layers = layers;
|
||||
}
|
||||
|
||||
@Override
|
||||
void run(PrintStream out, Map<Option, String> options, List<String> parameters) {
|
||||
try {
|
||||
checkJarCompatibility();
|
||||
File destination = getWorkingDirectory(options);
|
||||
FileResolver fileResolver = getFileResolver(destination, options);
|
||||
fileResolver.createDirectories();
|
||||
if (options.containsKey(LAUNCHER_OPTION)) {
|
||||
extractArchive(fileResolver);
|
||||
}
|
||||
else {
|
||||
JarStructure jarStructure = getJarStructure();
|
||||
extractLibraries(fileResolver, jarStructure, options);
|
||||
createRunner(jarStructure, fileResolver, options);
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
catch (LayersNotEnabledException ex) {
|
||||
printError(out, "Layers are not enabled");
|
||||
}
|
||||
}
|
||||
|
||||
private void checkJarCompatibility() throws IOException {
|
||||
File file = this.context.getArchiveFile();
|
||||
try (ZipInputStream stream = new ZipInputStream(new FileInputStream(file))) {
|
||||
ZipEntry entry = stream.getNextEntry();
|
||||
Assert.state(entry != null,
|
||||
() -> "File '%s' is not compatible; ensure jar file is valid and launch script is not enabled"
|
||||
.formatted(file));
|
||||
}
|
||||
}
|
||||
|
||||
private void printError(PrintStream out, String message) {
|
||||
out.println("Error: " + message);
|
||||
out.println();
|
||||
}
|
||||
|
||||
private void extractLibraries(FileResolver fileResolver, JarStructure jarStructure, Map<Option, String> options)
|
||||
throws IOException {
|
||||
String librariesDirectory = getLibrariesDirectory(options);
|
||||
extractArchive(fileResolver, (zipEntry) -> {
|
||||
Entry entry = jarStructure.resolve(zipEntry);
|
||||
if (isType(entry, Type.LIBRARY)) {
|
||||
return librariesDirectory + entry.location();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private static String getLibrariesDirectory(Map<Option, String> options) {
|
||||
if (options.containsKey(LIBRARIES_DIRECTORY_OPTION)) {
|
||||
String value = options.get(LIBRARIES_DIRECTORY_OPTION);
|
||||
if (value.endsWith("/")) {
|
||||
return value;
|
||||
}
|
||||
return value + "/";
|
||||
}
|
||||
return "lib/";
|
||||
}
|
||||
|
||||
private FileResolver getFileResolver(File destination, Map<Option, String> options) {
|
||||
String runnerFilename = getRunnerFilename(options);
|
||||
if (!options.containsKey(LAYERS_OPTION)) {
|
||||
return new NoLayersFileResolver(destination, runnerFilename);
|
||||
}
|
||||
Layers layers = getLayers();
|
||||
Set<String> layersToExtract = StringUtils.commaDelimitedListToSet(options.get(LAYERS_OPTION));
|
||||
return new LayersFileResolver(destination, layers, layersToExtract, runnerFilename);
|
||||
}
|
||||
|
||||
private File getWorkingDirectory(Map<Option, String> options) {
|
||||
if (options.containsKey(DESTINATION_OPTION)) {
|
||||
return new File(options.get(DESTINATION_OPTION));
|
||||
}
|
||||
return this.context.getWorkingDir();
|
||||
}
|
||||
|
||||
private JarStructure getJarStructure() {
|
||||
IndexedJarStructure jarStructure = IndexedJarStructure.get(this.context.getArchiveFile());
|
||||
Assert.state(jarStructure != null, "Couldn't read classpath index");
|
||||
return jarStructure;
|
||||
}
|
||||
|
||||
private void extractArchive(FileResolver fileResolver) throws IOException {
|
||||
extractArchive(fileResolver, ZipEntry::getName);
|
||||
}
|
||||
|
||||
private void extractArchive(FileResolver fileResolver, EntryNameTransformer entryNameTransformer)
|
||||
throws IOException {
|
||||
withZipEntries(this.context.getArchiveFile(), (stream, zipEntry) -> {
|
||||
if (zipEntry.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
String name = entryNameTransformer.getName(zipEntry);
|
||||
if (name == null) {
|
||||
return;
|
||||
}
|
||||
File file = fileResolver.resolve(zipEntry, name);
|
||||
if (file != null) {
|
||||
extractEntry(stream, zipEntry, file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Layers getLayers() {
|
||||
if (this.layers != null) {
|
||||
return this.layers;
|
||||
}
|
||||
return Layers.get(this.context);
|
||||
}
|
||||
|
||||
private void createRunner(JarStructure jarStructure, FileResolver fileResolver, Map<Option, String> options)
|
||||
throws IOException {
|
||||
File file = fileResolver.resolveRunner();
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
String librariesDirectory = getLibrariesDirectory(options);
|
||||
Manifest manifest = jarStructure.createLauncherManifest((library) -> librariesDirectory + library);
|
||||
mkDirs(file.getParentFile());
|
||||
try (JarOutputStream output = new JarOutputStream(new FileOutputStream(file), manifest)) {
|
||||
withZipEntries(this.context.getArchiveFile(), ((stream, zipEntry) -> {
|
||||
Entry entry = jarStructure.resolve(zipEntry);
|
||||
if (isType(entry, Type.APPLICATION_CLASS_OR_RESOURCE) && StringUtils.hasLength(entry.location())) {
|
||||
JarEntry jarEntry = createJarEntry(entry.location(), zipEntry);
|
||||
output.putNextEntry(jarEntry);
|
||||
StreamUtils.copy(stream, output);
|
||||
output.closeEntry();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private String getRunnerFilename(Map<Option, String> options) {
|
||||
if (options.containsKey(RUNNER_FILENAME_OPTION)) {
|
||||
return options.get(RUNNER_FILENAME_OPTION);
|
||||
}
|
||||
return "runner.jar";
|
||||
}
|
||||
|
||||
private static boolean isType(Entry entry, Type type) {
|
||||
if (entry == null) {
|
||||
return false;
|
||||
}
|
||||
return entry.type() == type;
|
||||
}
|
||||
|
||||
private static void extractEntry(ZipInputStream zip, ZipEntry entry, File file) throws IOException {
|
||||
mkDirs(file.getParentFile());
|
||||
try (OutputStream out = new FileOutputStream(file)) {
|
||||
StreamUtils.copy(zip, out);
|
||||
}
|
||||
try {
|
||||
Files.getFileAttributeView(file.toPath(), BasicFileAttributeView.class)
|
||||
.setTimes(entry.getLastModifiedTime(), entry.getLastAccessTime(), entry.getCreationTime());
|
||||
}
|
||||
catch (IOException ex) {
|
||||
// File system does not support setting time attributes. Continue.
|
||||
}
|
||||
}
|
||||
|
||||
private static void mkDirs(File file) throws IOException {
|
||||
if (!file.exists() && !file.mkdirs()) {
|
||||
throw new IOException("Unable to create directory " + file);
|
||||
}
|
||||
}
|
||||
|
||||
private static JarEntry createJarEntry(String location, ZipEntry originalEntry) {
|
||||
JarEntry jarEntry = new JarEntry(location);
|
||||
FileTime lastModifiedTime = originalEntry.getLastModifiedTime();
|
||||
if (lastModifiedTime != null) {
|
||||
jarEntry.setLastModifiedTime(lastModifiedTime);
|
||||
}
|
||||
FileTime lastAccessTime = originalEntry.getLastAccessTime();
|
||||
if (lastAccessTime != null) {
|
||||
jarEntry.setLastAccessTime(lastAccessTime);
|
||||
}
|
||||
FileTime creationTime = originalEntry.getCreationTime();
|
||||
if (creationTime != null) {
|
||||
jarEntry.setCreationTime(creationTime);
|
||||
}
|
||||
return jarEntry;
|
||||
}
|
||||
|
||||
private static void withZipEntries(File file, ThrowingConsumer callback) throws IOException {
|
||||
try (ZipInputStream stream = new ZipInputStream(new FileInputStream(file))) {
|
||||
ZipEntry entry = stream.getNextEntry();
|
||||
while (entry != null) {
|
||||
if (StringUtils.hasLength(entry.getName())) {
|
||||
callback.accept(stream, entry);
|
||||
}
|
||||
entry = stream.getNextEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static File assertFileIsContainedInDirectory(File directory, File file, String name) throws IOException {
|
||||
String canonicalOutputPath = directory.getCanonicalPath() + File.separator;
|
||||
String canonicalEntryPath = file.getCanonicalPath();
|
||||
Assert.state(canonicalEntryPath.startsWith(canonicalOutputPath),
|
||||
() -> "Entry '%s' would be written to '%s'. This is outside the output location of '%s'. Verify the contents of your archive."
|
||||
.formatted(name, canonicalEntryPath, canonicalOutputPath));
|
||||
return file;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface EntryNameTransformer {
|
||||
|
||||
String getName(ZipEntry entry);
|
||||
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface ThrowingConsumer {
|
||||
|
||||
void accept(ZipInputStream stream, ZipEntry entry) throws IOException;
|
||||
|
||||
}
|
||||
|
||||
private interface FileResolver {
|
||||
|
||||
/**
|
||||
* Creates needed directories.
|
||||
* @throws IOException if something went wrong
|
||||
*/
|
||||
void createDirectories() throws IOException;
|
||||
|
||||
/**
|
||||
* Resolves the given {@link ZipEntry} to a file.
|
||||
* @param entry the zip entry
|
||||
* @param newName the new name of the file
|
||||
* @return file where the contents should be written or {@code null} if this entry
|
||||
* should be skipped
|
||||
* @throws IOException if something went wrong
|
||||
*/
|
||||
default File resolve(ZipEntry entry, String newName) throws IOException {
|
||||
return resolve(entry.getName(), newName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the given name to a file.
|
||||
* @param originalName the original name of the file
|
||||
* @param newName the new name of the file
|
||||
* @return file where the contents should be written or {@code null} if this name
|
||||
* should be skipped
|
||||
* @throws IOException if something went wrong
|
||||
*/
|
||||
File resolve(String originalName, String newName) throws IOException;
|
||||
|
||||
/**
|
||||
* Resolves the file for the runner.
|
||||
* @return the file for the runner or {@code null} if the runner should be skipped
|
||||
* @throws IOException if something went wrong
|
||||
*/
|
||||
File resolveRunner() throws IOException;
|
||||
|
||||
}
|
||||
|
||||
private static final class NoLayersFileResolver implements FileResolver {
|
||||
|
||||
private final File directory;
|
||||
|
||||
private final String runnerFilename;
|
||||
|
||||
private NoLayersFileResolver(File directory, String runnerFilename) {
|
||||
this.directory = directory;
|
||||
this.runnerFilename = runnerFilename;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createDirectories() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public File resolve(String originalName, String newName) throws IOException {
|
||||
return assertFileIsContainedInDirectory(this.directory, new File(this.directory, newName), newName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public File resolveRunner() throws IOException {
|
||||
return resolve(this.runnerFilename, this.runnerFilename);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class LayersFileResolver implements FileResolver {
|
||||
|
||||
private final Layers layers;
|
||||
|
||||
private final Set<String> layersToExtract;
|
||||
|
||||
private final File directory;
|
||||
|
||||
private final String runnerFilename;
|
||||
|
||||
LayersFileResolver(File directory, Layers layers, Set<String> layersToExtract, String runnerFilename) {
|
||||
this.layers = layers;
|
||||
this.layersToExtract = layersToExtract;
|
||||
this.directory = directory;
|
||||
this.runnerFilename = runnerFilename;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createDirectories() throws IOException {
|
||||
for (String layer : this.layers) {
|
||||
if (shouldExtractLayer(layer)) {
|
||||
mkDirs(getLayerDirectory(layer));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public File resolve(String originalName, String newName) throws IOException {
|
||||
String layer = this.layers.getLayer(originalName);
|
||||
if (shouldExtractLayer(layer)) {
|
||||
File directory = getLayerDirectory(layer);
|
||||
return assertFileIsContainedInDirectory(directory, new File(directory, newName), newName);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File resolveRunner() throws IOException {
|
||||
String layer = this.layers.getApplicationLayerName();
|
||||
if (shouldExtractLayer(layer)) {
|
||||
File directory = getLayerDirectory(layer);
|
||||
return assertFileIsContainedInDirectory(directory, new File(directory, this.runnerFilename),
|
||||
this.runnerFilename);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private File getLayerDirectory(String layer) {
|
||||
return new File(this.directory, layer);
|
||||
}
|
||||
|
||||
private boolean shouldExtractLayer(String layer) {
|
||||
if (this.layersToExtract.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
return this.layersToExtract.contains(layer);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.jarmode.tools;
|
||||
|
||||
import java.io.PrintStream;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* The {@code 'extract'} tools command.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ExtractLayersCommand extends Command {
|
||||
|
||||
static final Option DESTINATION_OPTION = Option.of("destination", "string", "The destination to extract files to");
|
||||
|
||||
private final ExtractCommand delegate;
|
||||
|
||||
ExtractLayersCommand(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
ExtractLayersCommand(Context context, Layers layers) {
|
||||
super("extract", "Extracts layers from the jar for image creation", Options.of(DESTINATION_OPTION),
|
||||
Parameters.of("[<layer>...]"));
|
||||
this.delegate = new ExtractCommand(context, layers);
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isDeprecated() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
String getDeprecationMessage() {
|
||||
return "Use '-Djarmode=tools extract --layers --launcher' instead.";
|
||||
}
|
||||
|
||||
@Override
|
||||
void run(PrintStream out, Map<Option, String> options, List<String> parameters) {
|
||||
Map<Option, String> rewrittenOptions = new HashMap<>();
|
||||
if (options.containsKey(DESTINATION_OPTION)) {
|
||||
rewrittenOptions.put(ExtractCommand.DESTINATION_OPTION, options.get(DESTINATION_OPTION));
|
||||
}
|
||||
rewrittenOptions.put(ExtractCommand.LAYERS_OPTION, StringUtils.collectionToCommaDelimitedString(parameters));
|
||||
rewrittenOptions.put(ExtractCommand.LAUNCHER_OPTION, null);
|
||||
this.delegate.run(out, rewrittenOptions, Collections.emptyList());
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.jarmode.layertools;
|
||||
package org.springframework.boot.jarmode.tools;
|
||||
|
||||
import java.io.PrintStream;
|
||||
import java.util.List;
|
||||
@ -25,6 +25,7 @@ import java.util.stream.Stream;
|
||||
* Implicit {@code 'help'} command.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class HelpCommand extends Command {
|
||||
|
||||
@ -32,27 +33,47 @@ class HelpCommand extends Command {
|
||||
|
||||
private final List<Command> commands;
|
||||
|
||||
private final String jarMode;
|
||||
|
||||
HelpCommand(Context context, List<Command> commands) {
|
||||
super("help", "Help about any command", Options.none(), Parameters.of("[<command]"));
|
||||
this(context, commands, System.getProperty("jarmode"));
|
||||
}
|
||||
|
||||
HelpCommand(Context context, List<Command> commands, String jarMode) {
|
||||
super("help", "Help about any command", Options.none(), Parameters.of("[<command>]"));
|
||||
this.context = context;
|
||||
this.commands = commands;
|
||||
this.jarMode = (jarMode != null) ? jarMode : "tools";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run(Map<Option, String> options, List<String> parameters) {
|
||||
run(System.out, parameters);
|
||||
void run(PrintStream out, Map<Option, String> options, List<String> parameters) {
|
||||
run(out, parameters);
|
||||
}
|
||||
|
||||
void run(PrintStream out, List<String> parameters) {
|
||||
Command command = (!parameters.isEmpty()) ? Command.find(this.commands, parameters.get(0)) : null;
|
||||
if (command != null) {
|
||||
printCommandHelp(out, command);
|
||||
String commandName = (parameters.isEmpty()) ? null : parameters.get(0);
|
||||
if (commandName == null) {
|
||||
printUsageAndCommands(out);
|
||||
return;
|
||||
}
|
||||
printUsageAndCommands(out);
|
||||
if (getName().equals(commandName)) {
|
||||
printCommandHelp(out, this, true);
|
||||
return;
|
||||
}
|
||||
Command command = Command.find(this.commands, commandName);
|
||||
if (command == null) {
|
||||
printError(out, "Unknown command \"%s\"".formatted(commandName));
|
||||
printUsageAndCommands(out);
|
||||
return;
|
||||
}
|
||||
printCommandHelp(out, command, true);
|
||||
}
|
||||
|
||||
private void printCommandHelp(PrintStream out, Command command) {
|
||||
void printCommandHelp(PrintStream out, Command command, boolean printDeprecationWarning) {
|
||||
if (command.isDeprecated() && printDeprecationWarning) {
|
||||
printWarning(out, "This command is deprecated. " + command.getDeprecationMessage());
|
||||
}
|
||||
out.println(command.getDescription());
|
||||
out.println();
|
||||
out.println("Usage:");
|
||||
@ -85,8 +106,17 @@ class HelpCommand extends Command {
|
||||
out.println();
|
||||
out.println("Available commands:");
|
||||
int maxNameLength = getMaxLength(getName().length(), this.commands.stream().map(Command::getName));
|
||||
this.commands.forEach((command) -> printCommandSummary(out, command, maxNameLength));
|
||||
this.commands.stream()
|
||||
.filter((command) -> !command.isDeprecated())
|
||||
.forEach((command) -> printCommandSummary(out, command, maxNameLength));
|
||||
printCommandSummary(out, this, maxNameLength);
|
||||
List<Command> deprecatedCommands = this.commands.stream().filter(Command::isDeprecated).toList();
|
||||
if (!deprecatedCommands.isEmpty()) {
|
||||
out.println("Deprecated commands:");
|
||||
for (Command command : deprecatedCommands) {
|
||||
printCommandSummary(out, command, maxNameLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int getMaxLength(int minimum, Stream<String> strings) {
|
||||
@ -98,7 +128,17 @@ class HelpCommand extends Command {
|
||||
}
|
||||
|
||||
private String getJavaCommand() {
|
||||
return "java -Djarmode=layertools -jar " + this.context.getArchiveFile().getName();
|
||||
return "java -Djarmode=" + this.jarMode + " -jar " + this.context.getArchiveFile().getName();
|
||||
}
|
||||
|
||||
private void printError(PrintStream out, String errorMessage) {
|
||||
out.println("Error: " + errorMessage);
|
||||
out.println();
|
||||
}
|
||||
|
||||
private void printWarning(PrintStream out, String errorMessage) {
|
||||
out.println("Warning: " + errorMessage);
|
||||
out.println();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.jarmode.tools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.UnaryOperator;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.Attributes.Name;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
import org.springframework.boot.jarmode.tools.JarStructure.Entry.Type;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link JarStructure} implementation backed by a {@code classpath.idx} file.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class IndexedJarStructure implements JarStructure {
|
||||
|
||||
private static final List<String> MANIFEST_DENY_LIST = List.of("Start-Class", "Spring-Boot-Classes",
|
||||
"Spring-Boot-Lib", "Spring-Boot-Classpath-Index", "Spring-Boot-Layers-Index");
|
||||
|
||||
private final Manifest originalManifest;
|
||||
|
||||
private final String libLocation;
|
||||
|
||||
private final String classesLocation;
|
||||
|
||||
private final List<String> classpathEntries;
|
||||
|
||||
IndexedJarStructure(Manifest originalManifest, String indexFile) {
|
||||
this.originalManifest = originalManifest;
|
||||
this.libLocation = getLocation(originalManifest, "Spring-Boot-Lib");
|
||||
this.classesLocation = getLocation(originalManifest, "Spring-Boot-Classes");
|
||||
this.classpathEntries = readIndexFile(indexFile);
|
||||
}
|
||||
|
||||
private static String getLocation(Manifest manifest, String attribute) {
|
||||
String location = getMandatoryAttribute(manifest, attribute);
|
||||
if (!location.endsWith("/")) {
|
||||
location = location + "/";
|
||||
}
|
||||
return location;
|
||||
}
|
||||
|
||||
private static List<String> readIndexFile(String indexFile) {
|
||||
String[] lines = Arrays.stream(indexFile.split("\n"))
|
||||
.map((line) -> line.replace("\r", ""))
|
||||
.filter(StringUtils::hasText)
|
||||
.toArray(String[]::new);
|
||||
List<String> classpathEntries = new ArrayList<>();
|
||||
for (String line : lines) {
|
||||
if (line.startsWith("- ")) {
|
||||
classpathEntries.add(line.substring(3, line.length() - 1));
|
||||
}
|
||||
else {
|
||||
throw new IllegalStateException("Classpath index file is malformed");
|
||||
}
|
||||
}
|
||||
Assert.state(!classpathEntries.isEmpty(), "Empty classpath index file loaded");
|
||||
return classpathEntries;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClassesLocation() {
|
||||
return this.classesLocation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entry resolve(String name) {
|
||||
if (this.classpathEntries.contains(name)) {
|
||||
return new Entry(name, toStructureDependency(name), Type.LIBRARY);
|
||||
}
|
||||
else if (name.startsWith(this.classesLocation)) {
|
||||
return new Entry(name, name.substring(this.classesLocation.length()), Type.APPLICATION_CLASS_OR_RESOURCE);
|
||||
}
|
||||
else if (name.startsWith("org/springframework/boot/loader")) {
|
||||
return new Entry(name, name, Type.LOADER);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Manifest createLauncherManifest(UnaryOperator<String> libraryTransformer) {
|
||||
Manifest manifest = new Manifest(this.originalManifest);
|
||||
Attributes attributes = manifest.getMainAttributes();
|
||||
for (String denied : MANIFEST_DENY_LIST) {
|
||||
attributes.remove(new Name(denied));
|
||||
}
|
||||
attributes.put(Name.MAIN_CLASS, getMandatoryAttribute(this.originalManifest, "Start-Class"));
|
||||
attributes.put(Name.CLASS_PATH,
|
||||
this.classpathEntries.stream()
|
||||
.map(this::toStructureDependency)
|
||||
.map(libraryTransformer)
|
||||
.collect(Collectors.joining(" ")));
|
||||
return manifest;
|
||||
}
|
||||
|
||||
private String toStructureDependency(String libEntryName) {
|
||||
Assert.state(libEntryName.startsWith(this.libLocation), "Invalid library location " + libEntryName);
|
||||
return libEntryName.substring(this.libLocation.length());
|
||||
}
|
||||
|
||||
private static String getMandatoryAttribute(Manifest manifest, String attribute) {
|
||||
String value = manifest.getMainAttributes().getValue(attribute);
|
||||
Assert.state(value != null, "Manifest attribute '" + attribute + "' is mandatory");
|
||||
return value;
|
||||
}
|
||||
|
||||
static IndexedJarStructure get(File file) {
|
||||
try {
|
||||
try (JarFile jarFile = new JarFile(file)) {
|
||||
Manifest manifest = jarFile.getManifest();
|
||||
String location = getMandatoryAttribute(manifest, "Spring-Boot-Classpath-Index");
|
||||
ZipEntry entry = jarFile.getEntry(location);
|
||||
if (entry != null) {
|
||||
String indexFile = StreamUtils.copyToString(jarFile.getInputStream(entry), StandardCharsets.UTF_8);
|
||||
return new IndexedJarStructure(manifest, indexFile);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
catch (FileNotFoundException | NoSuchFileException ex) {
|
||||
return null;
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.jarmode.layertools;
|
||||
package org.springframework.boot.jarmode.tools;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
@ -39,12 +39,16 @@ import org.springframework.util.StringUtils;
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Madhura Bhave
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class IndexedLayers implements Layers {
|
||||
|
||||
private final Map<String, List<String>> layers = new LinkedHashMap<>();
|
||||
|
||||
IndexedLayers(String indexFile) {
|
||||
private final String classesLocation;
|
||||
|
||||
IndexedLayers(String indexFile, String classesLocation) {
|
||||
this.classesLocation = classesLocation;
|
||||
String[] lines = Arrays.stream(indexFile.split("\n"))
|
||||
.map((line) -> line.replace("\r", ""))
|
||||
.filter(StringUtils::hasText)
|
||||
@ -56,6 +60,7 @@ class IndexedLayers implements Layers {
|
||||
this.layers.put(line.substring(3, line.length() - 2), contents);
|
||||
}
|
||||
else if (line.startsWith(" - ")) {
|
||||
Assert.notNull(contents, "Contents must not be null. Check if the index file is malformed!");
|
||||
contents.add(line.substring(5, line.length() - 1));
|
||||
}
|
||||
else {
|
||||
@ -65,17 +70,18 @@ class IndexedLayers implements Layers {
|
||||
Assert.state(!this.layers.isEmpty(), "Empty layer index file loaded");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getApplicationLayerName() {
|
||||
return getLayer(this.classesLocation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<String> iterator() {
|
||||
return this.layers.keySet().iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLayer(ZipEntry entry) {
|
||||
return getLayer(entry.getName());
|
||||
}
|
||||
|
||||
private String getLayer(String name) {
|
||||
public String getLayer(String name) {
|
||||
for (Map.Entry<String, List<String>> entry : this.layers.entrySet()) {
|
||||
for (String candidate : entry.getValue()) {
|
||||
if (candidate.equals(name) || (candidate.endsWith("/") && name.startsWith(candidate))) {
|
||||
@ -100,7 +106,8 @@ class IndexedLayers implements Layers {
|
||||
ZipEntry entry = (location != null) ? jarFile.getEntry(location) : null;
|
||||
if (entry != null) {
|
||||
String indexFile = StreamUtils.copyToString(jarFile.getInputStream(entry), StandardCharsets.UTF_8);
|
||||
return new IndexedLayers(indexFile);
|
||||
String classesLocation = manifest.getMainAttributes().getValue("Spring-Boot-Classes");
|
||||
return new IndexedLayers(indexFile, classesLocation);
|
||||
}
|
||||
}
|
||||
return null;
|
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.jarmode.tools;
|
||||
|
||||
import java.util.function.UnaryOperator;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
/**
|
||||
* Provide information about a fat jar structure that is meant to be extracted.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
interface JarStructure {
|
||||
|
||||
/**
|
||||
* Resolve the specified {@link ZipEntry}, return {@code null} if the entry should not
|
||||
* be handled.
|
||||
* @param entry the entry to handle
|
||||
* @return the resolved {@link Entry}
|
||||
*/
|
||||
default Entry resolve(ZipEntry entry) {
|
||||
return resolve(entry.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the entry with the specified name, return {@code null} if the entry should
|
||||
* not be handled.
|
||||
* @param name the name of the entry to handle
|
||||
* @return the resolved {@link Entry}
|
||||
*/
|
||||
Entry resolve(String name);
|
||||
|
||||
/**
|
||||
* Create the {@link Manifest} for the launcher jar, applying the specified operator
|
||||
* on each classpath entry.
|
||||
* @param libraryTransformer the operator to apply on each classpath entry
|
||||
* @return the manifest to use for the launcher jar
|
||||
*/
|
||||
Manifest createLauncherManifest(UnaryOperator<String> libraryTransformer);
|
||||
|
||||
/**
|
||||
* Return the location of the application classes.
|
||||
* @return the location of the application classes
|
||||
*/
|
||||
String getClassesLocation();
|
||||
|
||||
/**
|
||||
* An entry to handle in the exploded structure.
|
||||
*
|
||||
* @param originalLocation the original location
|
||||
* @param location the relative location
|
||||
* @param type of the entry
|
||||
*/
|
||||
record Entry(String originalLocation, String location, Type type) {
|
||||
enum Type {
|
||||
|
||||
LIBRARY, APPLICATION_CLASS_OR_RESOURCE, LOADER
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.jarmode.tools;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.loader.jarmode.JarMode;
|
||||
|
||||
/**
|
||||
* {@link JarMode} providing {@code "layertools"} support.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class LayerToolsJarMode implements JarMode {
|
||||
|
||||
static Context contextOverride;
|
||||
|
||||
@Override
|
||||
public boolean accepts(String mode) {
|
||||
return "layertools".equalsIgnoreCase(mode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(String mode, String[] args) {
|
||||
try {
|
||||
Context context = (contextOverride != null) ? contextOverride : new Context();
|
||||
new Runner(System.out, context, getCommands(context)).run(args);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
static List<Command> getCommands(Context context) {
|
||||
return List.of(new ListCommand(context), new ExtractLayersCommand(context));
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.jarmode.layertools;
|
||||
package org.springframework.boot.jarmode.tools;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.zip.ZipEntry;
|
||||
@ -23,6 +23,7 @@ import java.util.zip.ZipEntry;
|
||||
* Provides information about the jar layers.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Moritz Halbritter
|
||||
* @see ExtractCommand
|
||||
* @see ListCommand
|
||||
*/
|
||||
@ -40,19 +41,43 @@ interface Layers extends Iterable<String> {
|
||||
* @param entry the entry to check
|
||||
* @return the layer that the entry is in
|
||||
*/
|
||||
String getLayer(ZipEntry entry);
|
||||
default String getLayer(ZipEntry entry) {
|
||||
return getLayer(entry.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the layer that the entry with the given name is in.
|
||||
* @param entryName the name of the entry to check
|
||||
* @return the layer that the entry is in
|
||||
*/
|
||||
String getLayer(String entryName);
|
||||
|
||||
/**
|
||||
* Return the name of the application layer.
|
||||
* @return the name of the application layer
|
||||
*/
|
||||
String getApplicationLayerName();
|
||||
|
||||
/**
|
||||
* Return a {@link Layers} instance for the currently running application.
|
||||
* @param context the command context
|
||||
* @return a new layers instance
|
||||
* @throws LayersNotEnabledException if layers are not enabled
|
||||
*/
|
||||
static Layers get(Context context) {
|
||||
IndexedLayers indexedLayers = IndexedLayers.get(context);
|
||||
if (indexedLayers == null) {
|
||||
throw new IllegalStateException("Failed to load layers.idx which is required by layertools");
|
||||
throw new LayersNotEnabledException();
|
||||
}
|
||||
return indexedLayers;
|
||||
}
|
||||
|
||||
final class LayersNotEnabledException extends RuntimeException {
|
||||
|
||||
LayersNotEnabledException() {
|
||||
super("Layers not enabled: Failed to load layer index file");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.jarmode.layertools;
|
||||
package org.springframework.boot.jarmode.tools;
|
||||
|
||||
import java.io.PrintStream;
|
||||
import java.util.List;
|
||||
@ -23,24 +23,37 @@ import java.util.Map;
|
||||
/**
|
||||
* The {@code 'list'} tools command.
|
||||
*
|
||||
* Delegates the actual work to {@link ListLayersCommand}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class ListCommand extends Command {
|
||||
|
||||
private final Context context;
|
||||
private final ListLayersCommand delegate;
|
||||
|
||||
ListCommand(Context context) {
|
||||
super("list", "List layers from the jar that can be extracted", Options.none(), Parameters.none());
|
||||
this.context = context;
|
||||
this.delegate = new ListLayersCommand(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run(Map<Option, String> options, List<String> parameters) {
|
||||
printLayers(Layers.get(this.context), System.out);
|
||||
boolean isDeprecated() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
String getDeprecationMessage() {
|
||||
return "Use '-Djarmode=tools list-layers' instead.";
|
||||
}
|
||||
|
||||
@Override
|
||||
void run(PrintStream out, Map<Option, String> options, List<String> parameters) {
|
||||
this.delegate.run(out, options, parameters);
|
||||
}
|
||||
|
||||
void printLayers(Layers layers, PrintStream out) {
|
||||
layers.forEach(out::println);
|
||||
this.delegate.printLayers(out, layers);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.jarmode.tools;
|
||||
|
||||
import java.io.PrintStream;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.jarmode.tools.Layers.LayersNotEnabledException;
|
||||
|
||||
/**
|
||||
* The {@code 'list-layers'} tools command.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class ListLayersCommand extends Command {
|
||||
|
||||
private final Context context;
|
||||
|
||||
ListLayersCommand(Context context) {
|
||||
super("list-layers", "List layers from the jar that can be extracted", Options.none(), Parameters.none());
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
void run(PrintStream out, Map<Option, String> options, List<String> parameters) {
|
||||
try {
|
||||
Layers layers = Layers.get(this.context);
|
||||
printLayers(out, layers);
|
||||
}
|
||||
catch (LayersNotEnabledException ex) {
|
||||
printError(out, "Layers are not enabled");
|
||||
}
|
||||
}
|
||||
|
||||
void printLayers(PrintStream out, Layers layers) {
|
||||
layers.forEach(out::println);
|
||||
}
|
||||
|
||||
private void printError(PrintStream out, String message) {
|
||||
out.println("Error: " + message);
|
||||
out.println();
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.jarmode.layertools;
|
||||
package org.springframework.boot.jarmode.tools;
|
||||
|
||||
/**
|
||||
* Exception thrown when a required value is not provided for an option.
|
@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.jarmode.tools;
|
||||
|
||||
import java.io.PrintStream;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Runs commands.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class Runner {
|
||||
|
||||
private final PrintStream out;
|
||||
|
||||
private final List<Command> commands = new ArrayList<>();
|
||||
|
||||
private final HelpCommand help;
|
||||
|
||||
Runner(PrintStream out, Context context, List<Command> commands) {
|
||||
this.out = out;
|
||||
this.commands.addAll(commands);
|
||||
this.help = new HelpCommand(context, commands);
|
||||
this.commands.add(this.help);
|
||||
}
|
||||
|
||||
void run(String... args) {
|
||||
run(dequeOf(args));
|
||||
}
|
||||
|
||||
private void run(Deque<String> args) {
|
||||
if (!args.isEmpty()) {
|
||||
String commandName = args.removeFirst();
|
||||
Command command = Command.find(this.commands, commandName);
|
||||
if (command != null) {
|
||||
runCommand(command, args);
|
||||
return;
|
||||
}
|
||||
printError("Unknown command \"" + commandName + "\"");
|
||||
}
|
||||
this.help.run(this.out, args);
|
||||
}
|
||||
|
||||
private void runCommand(Command command, Deque<String> args) {
|
||||
if (command.isDeprecated()) {
|
||||
printWarning("This command is deprecated. " + command.getDeprecationMessage());
|
||||
}
|
||||
try {
|
||||
command.run(this.out, args);
|
||||
}
|
||||
catch (UnknownOptionException ex) {
|
||||
printError("Unknown option \"" + ex.getMessage() + "\" for the " + command.getName() + " command");
|
||||
this.help.printCommandHelp(this.out, command, false);
|
||||
}
|
||||
catch (MissingValueException ex) {
|
||||
printError("Option \"" + ex.getMessage() + "\" for the " + command.getName() + " command requires a value");
|
||||
this.help.printCommandHelp(this.out, command, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void printWarning(String message) {
|
||||
this.out.println("Warning: " + message);
|
||||
this.out.println();
|
||||
}
|
||||
|
||||
private void printError(String message) {
|
||||
this.out.println("Error: " + message);
|
||||
this.out.println();
|
||||
}
|
||||
|
||||
private Deque<String> dequeOf(String... args) {
|
||||
return new ArrayDeque<>(Arrays.asList(args));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.jarmode.tools;
|
||||
|
||||
import java.io.PrintStream;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.loader.jarmode.JarMode;
|
||||
|
||||
/**
|
||||
* {@link JarMode} providing {@code "tools"} support.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
* @since 3.3.0
|
||||
*/
|
||||
public class ToolsJarMode implements JarMode {
|
||||
|
||||
private final Context context;
|
||||
|
||||
private final PrintStream out;
|
||||
|
||||
public ToolsJarMode() {
|
||||
this(null, null);
|
||||
}
|
||||
|
||||
public ToolsJarMode(Context context, PrintStream out) {
|
||||
this.context = (context != null) ? context : new Context();
|
||||
this.out = (out != null) ? out : System.out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accepts(String mode) {
|
||||
return "tools".equalsIgnoreCase(mode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(String mode, String[] args) {
|
||||
try {
|
||||
new Runner(this.out, this.context, getCommands(this.context)).run(args);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
static List<Command> getCommands(Context context) {
|
||||
return List.of(new ExtractCommand(context), new ListLayersCommand(context));
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.jarmode.layertools;
|
||||
package org.springframework.boot.jarmode.tools;
|
||||
|
||||
/**
|
||||
* Exception thrown when an unrecognized option is encountered.
|
@ -15,6 +15,6 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* JarMode support for layertools.
|
||||
* JarMode support for layertools and tools.
|
||||
*/
|
||||
package org.springframework.boot.jarmode.layertools;
|
||||
package org.springframework.boot.jarmode.tools;
|
@ -0,0 +1,4 @@
|
||||
# Jar Modes
|
||||
org.springframework.boot.loader.jarmode.JarMode=\
|
||||
org.springframework.boot.jarmode.tools.LayerToolsJarMode,\
|
||||
org.springframework.boot.jarmode.tools.ToolsJarMode
|
@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.jarmode.tools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.JarOutputStream;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
abstract class AbstractTests {
|
||||
|
||||
@TempDir
|
||||
File tempDir;
|
||||
|
||||
Manifest createManifest(String... entries) {
|
||||
Manifest manifest = new Manifest();
|
||||
manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
|
||||
for (String entry : entries) {
|
||||
int colon = entry.indexOf(':');
|
||||
Assert.state(colon > -1, () -> "Colon not found in %s".formatted(entry));
|
||||
String key = entry.substring(0, colon).trim();
|
||||
String value = entry.substring(colon + 1).trim();
|
||||
manifest.getMainAttributes().putValue(key, value);
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
|
||||
File createArchive(String... entries) throws IOException {
|
||||
return createArchive(createManifest(), entries);
|
||||
}
|
||||
|
||||
File createArchive(Manifest manifest, String... entries) throws IOException {
|
||||
return createArchive(manifest, null, null, null, entries);
|
||||
}
|
||||
|
||||
File createArchive(Manifest manifest, Instant creationTime, Instant lastModifiedTime, Instant lastAccessTime,
|
||||
String... entries) throws IOException {
|
||||
Assert.state(entries.length % 2 == 0, "Entries must be key value pairs");
|
||||
File file = new File(this.tempDir, "test.jar");
|
||||
try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(file), manifest)) {
|
||||
for (int i = 0; i < entries.length; i += 2) {
|
||||
ZipEntry entry = new ZipEntry(entries[i]);
|
||||
if (creationTime != null) {
|
||||
entry.setCreationTime(FileTime.from(creationTime));
|
||||
}
|
||||
if (lastModifiedTime != null) {
|
||||
entry.setLastModifiedTime(FileTime.from(lastModifiedTime));
|
||||
}
|
||||
if (lastAccessTime != null) {
|
||||
entry.setLastAccessTime(FileTime.from(lastAccessTime));
|
||||
}
|
||||
jar.putNextEntry(entry);
|
||||
String resource = entries[i + 1];
|
||||
if (resource != null) {
|
||||
try (InputStream content = ListLayersCommandTests.class.getResourceAsStream(resource)) {
|
||||
assertThat(content).as("Resource " + resource).isNotNull();
|
||||
StreamUtils.copy(content, jar);
|
||||
}
|
||||
}
|
||||
jar.closeEntry();
|
||||
}
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
TestPrintStream runCommand(CommandFactory<?> commandFactory, File archive, String... arguments) {
|
||||
Context context = new Context(archive, this.tempDir);
|
||||
Command command = commandFactory.create(context);
|
||||
TestPrintStream out = new TestPrintStream(this);
|
||||
command.run(out, new ArrayDeque<>(Arrays.asList(arguments)));
|
||||
return out;
|
||||
}
|
||||
|
||||
Manifest getJarManifest(File jar) throws IOException {
|
||||
try (JarFile jarFile = new JarFile(jar)) {
|
||||
return jarFile.getManifest();
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, String> getJarManifestAttributes(File jar) throws IOException {
|
||||
assertThat(jar).exists();
|
||||
Manifest manifest = getJarManifest(jar);
|
||||
Map<String, String> result = new HashMap<>();
|
||||
manifest.getMainAttributes().forEach((key, value) -> result.put(key.toString(), value.toString()));
|
||||
return result;
|
||||
}
|
||||
|
||||
List<String> getJarEntryNames(File jar) throws IOException {
|
||||
assertThat(jar).exists();
|
||||
try (JarFile jarFile = new JarFile(jar)) {
|
||||
return jarFile.stream().map(ZipEntry::getName).toList();
|
||||
}
|
||||
}
|
||||
|
||||
List<String> listFilenames() throws IOException {
|
||||
return listFilenames(this.tempDir);
|
||||
}
|
||||
|
||||
List<String> listFilenames(File directory) throws IOException {
|
||||
try (Stream<Path> stream = Files.walk(directory.toPath())) {
|
||||
int substring = directory.getAbsolutePath().length() + 1;
|
||||
return stream.map((file) -> file.toAbsolutePath().toString())
|
||||
.map((file) -> (file.length() >= substring) ? file.substring(substring) : "")
|
||||
.filter(StringUtils::hasLength)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
interface CommandFactory<T extends Command> {
|
||||
|
||||
T create(Context context);
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@ -14,8 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.jarmode.layertools;
|
||||
package org.springframework.boot.jarmode.tools;
|
||||
|
||||
import java.io.PrintStream;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@ -24,9 +25,9 @@ import java.util.Map;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.jarmode.layertools.Command.Option;
|
||||
import org.springframework.boot.jarmode.layertools.Command.Options;
|
||||
import org.springframework.boot.jarmode.layertools.Command.Parameters;
|
||||
import org.springframework.boot.jarmode.tools.Command.Option;
|
||||
import org.springframework.boot.jarmode.tools.Command.Options;
|
||||
import org.springframework.boot.jarmode.tools.Command.Parameters;
|
||||
|
||||
import static org.assertj.core.api.Assertions.as;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@ -37,6 +38,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class CommandTests {
|
||||
|
||||
@ -44,6 +46,9 @@ class CommandTests {
|
||||
|
||||
private static final Option LOG_LEVEL_OPTION = Option.of("log-level", "Logging level (debug or info)", "string");
|
||||
|
||||
private static final Option LAYERS_OPTION = Option.of("layers", "Layers (leave empty for all)", "string list",
|
||||
true);
|
||||
|
||||
@Test
|
||||
void getNameReturnsName() {
|
||||
TestCommand command = new TestCommand("test");
|
||||
@ -146,8 +151,16 @@ class CommandTests {
|
||||
assertThat(option.getValueDescription()).isEqualTo("value description");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotParseFollowingOptionAsValue() {
|
||||
TestCommand command = new TestCommand("test", LAYERS_OPTION, LOG_LEVEL_OPTION);
|
||||
run(command, "--layers", "--log-level", "debug");
|
||||
assertThat(command.getRunOptions()).containsEntry(LAYERS_OPTION, null);
|
||||
assertThat(command.getRunOptions()).containsEntry(LOG_LEVEL_OPTION, "debug");
|
||||
}
|
||||
|
||||
private void run(TestCommand command, String... args) {
|
||||
command.run(new ArrayDeque<>(Arrays.asList(args)));
|
||||
command.run(System.out, new ArrayDeque<>(Arrays.asList(args)));
|
||||
}
|
||||
|
||||
static class TestCommand extends Command {
|
||||
@ -165,7 +178,7 @@ class CommandTests {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run(Map<Option, String> options, List<String> parameters) {
|
||||
protected void run(PrintStream out, Map<Option, String> options, List<String> parameters) {
|
||||
this.runOptions = options;
|
||||
this.runParameters = parameters;
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.jarmode.layertools;
|
||||
package org.springframework.boot.jarmode.tools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
@ -0,0 +1,303 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.jarmode.tools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.lang.Runtime.Version;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.BasicFileAttributeView;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.JRE;
|
||||
import org.junit.jupiter.api.condition.OS;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* Tests for {@link ExtractCommand}.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class ExtractCommandTests extends AbstractTests {
|
||||
|
||||
private static final Instant NOW = Instant.now();
|
||||
|
||||
private static final Instant CREATION_TIME = NOW.minus(3, ChronoUnit.DAYS);
|
||||
|
||||
private static final Instant LAST_MODIFIED_TIME = NOW.minus(2, ChronoUnit.DAYS);
|
||||
|
||||
private static final Instant LAST_ACCESS_TIME = NOW.minus(1, ChronoUnit.DAYS);
|
||||
|
||||
private File archive;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
Manifest manifest = createManifest("Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx",
|
||||
"Spring-Boot-Lib: BOOT-INF/lib/", "Spring-Boot-Classes: BOOT-INF/classes/",
|
||||
"Start-Class: org.example.Main", "Spring-Boot-Layers-Index: BOOT-INF/layers.idx",
|
||||
"Some-Attribute: Some-Value");
|
||||
this.archive = createArchive(manifest, CREATION_TIME, LAST_MODIFIED_TIME, LAST_ACCESS_TIME,
|
||||
"BOOT-INF/classpath.idx", "/jar-contents/classpath.idx", "BOOT-INF/layers.idx",
|
||||
"/jar-contents/layers.idx", "BOOT-INF/lib/dependency-1.jar", "/jar-contents/dependency-1",
|
||||
"BOOT-INF/lib/dependency-2.jar", "/jar-contents/dependency-2", "BOOT-INF/lib/dependency-3-SNAPSHOT.jar",
|
||||
"/jar-contents/dependency-3-SNAPSHOT", "org/springframework/boot/loader/launch/JarLauncher.class",
|
||||
"/jar-contents/JarLauncher", "BOOT-INF/classes/application.properties",
|
||||
"/jar-contents/application.properties");
|
||||
}
|
||||
|
||||
private File file(String name) {
|
||||
return new File(this.tempDir, name);
|
||||
}
|
||||
|
||||
private TestPrintStream run(File archive, String... args) {
|
||||
return runCommand(ExtractCommand::new, archive, args);
|
||||
}
|
||||
|
||||
private void timeAttributes(File file) {
|
||||
try {
|
||||
BasicFileAttributes basicAttributes = Files
|
||||
.getFileAttributeView(file.toPath(), BasicFileAttributeView.class)
|
||||
.readAttributes();
|
||||
assertThat(basicAttributes.lastModifiedTime().toInstant().truncatedTo(ChronoUnit.SECONDS))
|
||||
.isEqualTo(LAST_MODIFIED_TIME.truncatedTo(ChronoUnit.SECONDS));
|
||||
Instant expectedCreationTime = expectedCreationTime();
|
||||
if (expectedCreationTime != null) {
|
||||
assertThat(basicAttributes.creationTime().toInstant().truncatedTo(ChronoUnit.SECONDS))
|
||||
.isEqualTo(expectedCreationTime.truncatedTo(ChronoUnit.SECONDS));
|
||||
}
|
||||
assertThat(basicAttributes.lastAccessTime().toInstant().truncatedTo(ChronoUnit.SECONDS))
|
||||
.isEqualTo(LAST_ACCESS_TIME.truncatedTo(ChronoUnit.SECONDS));
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Instant expectedCreationTime() {
|
||||
// macOS uses last modified time until Java 20 where it uses creation time.
|
||||
// https://github.com/openjdk/jdk21u-dev/commit/6397d564a5dab07f81bf4c69b116ebfabb2446ba
|
||||
if (OS.MAC.isCurrentOs()) {
|
||||
return (EnumSet.range(JRE.JAVA_17, JRE.JAVA_19).contains(JRE.currentVersion())) ? LAST_MODIFIED_TIME
|
||||
: CREATION_TIME;
|
||||
}
|
||||
if (OS.LINUX.isCurrentOs()) {
|
||||
// Linux uses the modified time until Java 21.0.2 where a bug means that it
|
||||
// uses the birth time which it has not set, preventing us from verifying it.
|
||||
// https://github.com/openjdk/jdk21u-dev/commit/4cf572e3b99b675418e456e7815fb6fd79245e30
|
||||
return (Runtime.version().compareTo(Version.parse("21.0.2")) >= 0) ? null : LAST_MODIFIED_TIME;
|
||||
}
|
||||
return CREATION_TIME;
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Extract {
|
||||
|
||||
@Test
|
||||
void extractLibrariesAndCreatesRunner() throws IOException {
|
||||
run(ExtractCommandTests.this.archive);
|
||||
List<String> filenames = listFilenames();
|
||||
assertThat(filenames).contains("lib/dependency-1.jar")
|
||||
.contains("lib/dependency-2.jar")
|
||||
.contains("lib/dependency-3-SNAPSHOT.jar")
|
||||
.contains("runner.jar")
|
||||
.doesNotContain("org/springframework/boot/loader/launch/JarLauncher.class");
|
||||
}
|
||||
|
||||
@Test
|
||||
void extractLibrariesAndCreatesRunnerInDestination() throws IOException {
|
||||
run(ExtractCommandTests.this.archive, "--destination", file("out").getAbsolutePath());
|
||||
List<String> filenames = listFilenames();
|
||||
assertThat(filenames).contains("out/lib/dependency-1.jar")
|
||||
.contains("out/lib/dependency-2.jar")
|
||||
.contains("out/lib/dependency-3-SNAPSHOT.jar")
|
||||
.contains("out/runner.jar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void runnerNameAndLibrariesDirectoriesCanBeCustomized() throws IOException {
|
||||
run(ExtractCommandTests.this.archive, "--runner-filename", "runner-customized.jar", "--libraries",
|
||||
"dependencies");
|
||||
List<String> filenames = listFilenames();
|
||||
assertThat(filenames).contains("dependencies/dependency-1.jar")
|
||||
.contains("dependencies/dependency-2.jar")
|
||||
.contains("dependencies/dependency-3-SNAPSHOT.jar");
|
||||
File runner = file("runner-customized.jar");
|
||||
assertThat(runner).exists();
|
||||
Map<String, String> attributes = getJarManifestAttributes(runner);
|
||||
assertThat(attributes).containsEntry("Class-Path",
|
||||
"dependencies/dependency-1.jar dependencies/dependency-2.jar dependencies/dependency-3-SNAPSHOT.jar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void runnerContainsManifestEntries() throws IOException {
|
||||
run(ExtractCommandTests.this.archive);
|
||||
File runner = file("runner.jar");
|
||||
Map<String, String> attributes = getJarManifestAttributes(runner);
|
||||
assertThat(attributes).containsEntry("Main-Class", "org.example.Main")
|
||||
.containsEntry("Class-Path", "lib/dependency-1.jar lib/dependency-2.jar lib/dependency-3-SNAPSHOT.jar")
|
||||
.containsEntry("Some-Attribute", "Some-Value")
|
||||
.doesNotContainKeys("Start-Class", "Spring-Boot-Classes", "Spring-Boot-Lib",
|
||||
"Spring-Boot-Classpath-Index", "Spring-Boot-Layers-Index");
|
||||
}
|
||||
|
||||
@Test
|
||||
void runnerContainsApplicationClassesAndResources() throws IOException {
|
||||
run(ExtractCommandTests.this.archive);
|
||||
File runner = file("runner.jar");
|
||||
List<String> entryNames = getJarEntryNames(runner);
|
||||
assertThat(entryNames).contains("application.properties");
|
||||
}
|
||||
|
||||
@Test
|
||||
void appliesFileTimes() {
|
||||
run(ExtractCommandTests.this.archive);
|
||||
assertThat(file("lib/dependency-1.jar")).exists().satisfies(ExtractCommandTests.this::timeAttributes);
|
||||
assertThat(file("lib/dependency-2.jar")).exists().satisfies(ExtractCommandTests.this::timeAttributes);
|
||||
assertThat(file("lib/dependency-3-SNAPSHOT.jar")).exists()
|
||||
.satisfies(ExtractCommandTests.this::timeAttributes);
|
||||
}
|
||||
|
||||
@Test
|
||||
void runnerDoesntContainLibraries() throws IOException {
|
||||
run(ExtractCommandTests.this.archive);
|
||||
File runner = file("runner.jar");
|
||||
List<String> entryNames = getJarEntryNames(runner);
|
||||
assertThat(entryNames).doesNotContain("BOOT-INF/lib/dependency-1.jar", "BOOT-INF/lib/dependency-2.jar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void failsOnIncompatibleJar() throws IOException {
|
||||
File file = file("empty.jar");
|
||||
try (FileWriter writer = new FileWriter(file)) {
|
||||
writer.write("text");
|
||||
}
|
||||
assertThatIllegalStateException().isThrownBy(() -> run(file)).withMessageContaining("not compatible");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ExtractWithLayers {
|
||||
|
||||
@Test
|
||||
void extractLibrariesAndCreatesRunner() throws IOException {
|
||||
run(ExtractCommandTests.this.archive, "--layers");
|
||||
List<String> filenames = listFilenames();
|
||||
assertThat(filenames).contains("dependencies/lib/dependency-1.jar")
|
||||
.contains("dependencies/lib/dependency-2.jar")
|
||||
.contains("snapshot-dependencies/lib/dependency-3-SNAPSHOT.jar")
|
||||
.contains("application/runner.jar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void extractsOnlySelectedLayers() throws IOException {
|
||||
run(ExtractCommandTests.this.archive, "--layers", "dependencies");
|
||||
List<String> filenames = listFilenames();
|
||||
assertThat(filenames).contains("dependencies/lib/dependency-1.jar")
|
||||
.contains("dependencies/lib/dependency-2.jar")
|
||||
.doesNotContain("snapshot-dependencies/lib/dependency-3-SNAPSHOT.jar")
|
||||
.doesNotContain("application/runner.jar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void printErrorIfLayersAreNotEnabled() throws IOException {
|
||||
File archive = createArchive();
|
||||
TestPrintStream out = run(archive, "--layers");
|
||||
assertThat(out).hasSameContentAsResource("ExtractCommand-printErrorIfLayersAreNotEnabled.txt");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ExtractLauncher {
|
||||
|
||||
@Test
|
||||
void extract() throws IOException {
|
||||
run(ExtractCommandTests.this.archive, "--launcher");
|
||||
List<String> filenames = listFilenames();
|
||||
assertThat(filenames).contains("META-INF/MANIFEST.MF")
|
||||
.contains("BOOT-INF/classpath.idx")
|
||||
.contains("BOOT-INF/layers.idx")
|
||||
.contains("BOOT-INF/lib/dependency-1.jar")
|
||||
.contains("BOOT-INF/lib/dependency-2.jar")
|
||||
.contains("BOOT-INF/lib/dependency-3-SNAPSHOT.jar")
|
||||
.contains("BOOT-INF/classes/application.properties")
|
||||
.contains("org/springframework/boot/loader/launch/JarLauncher.class");
|
||||
}
|
||||
|
||||
@Test
|
||||
void runWithJarFileThatWouldWriteEntriesOutsideDestinationFails() throws Exception {
|
||||
File file = createArchive("e/../../e.jar", null);
|
||||
assertThatIllegalStateException().isThrownBy(() -> run(file, "--launcher"))
|
||||
.withMessageContaining("Entry 'e/../../e.jar' would be written");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ExtractLauncherWithLayers {
|
||||
|
||||
@Test
|
||||
void extract() throws IOException {
|
||||
run(ExtractCommandTests.this.archive, "--launcher", "--layers");
|
||||
List<String> filenames = listFilenames();
|
||||
assertThat(filenames).contains("application/META-INF/MANIFEST.MF")
|
||||
.contains("application/BOOT-INF/classpath.idx")
|
||||
.contains("application/BOOT-INF/layers.idx")
|
||||
.contains("dependencies/BOOT-INF/lib/dependency-1.jar")
|
||||
.contains("dependencies/BOOT-INF/lib/dependency-2.jar")
|
||||
.contains("snapshot-dependencies/BOOT-INF/lib/dependency-3-SNAPSHOT.jar")
|
||||
.contains("application/BOOT-INF/classes/application.properties")
|
||||
.contains("spring-boot-loader/org/springframework/boot/loader/launch/JarLauncher.class");
|
||||
}
|
||||
|
||||
@Test
|
||||
void printErrorIfLayersAreNotEnabled() throws IOException {
|
||||
File archive = createArchive();
|
||||
TestPrintStream out = run(archive, "--launcher", "--layers");
|
||||
assertThat(out).hasSameContentAsResource("ExtractCommand-printErrorIfLayersAreNotEnabled.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void extractsOnlySelectedLayers() throws IOException {
|
||||
run(ExtractCommandTests.this.archive, "--launcher", "--layers", "dependencies");
|
||||
List<String> filenames = listFilenames();
|
||||
assertThat(filenames).doesNotContain("application/META-INF/MANIFEST.MF")
|
||||
.doesNotContain("application/BOOT-INF/classpath.idx")
|
||||
.doesNotContain("application/BOOT-INF/layers.idx")
|
||||
.contains("dependencies/BOOT-INF/lib/dependency-1.jar")
|
||||
.contains("dependencies/BOOT-INF/lib/dependency-2.jar")
|
||||
.doesNotContain("snapshot-dependencies/BOOT-INF/lib/dependency-3-SNAPSHOT.jar")
|
||||
.doesNotContain("application/BOOT-INF/classes/application.properties")
|
||||
.doesNotContain("spring-boot-loader/org/springframework/boot/loader/launch/JarLauncher.class");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.jarmode.layertools;
|
||||
package org.springframework.boot.jarmode.tools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
@ -54,13 +54,13 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
|
||||
/**
|
||||
* Tests for {@link ExtractCommand}.
|
||||
* Tests for {@link ExtractLayersCommand}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ExtractCommandTests {
|
||||
class ExtractLayersCommandTests {
|
||||
|
||||
private static final Instant NOW = Instant.now();
|
||||
|
||||
@ -82,21 +82,21 @@ class ExtractCommandTests {
|
||||
|
||||
private final Layers layers = new TestLayers();
|
||||
|
||||
private ExtractCommand command;
|
||||
private ExtractLayersCommand command;
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws Exception {
|
||||
this.jarFile = createJarFile("test.jar");
|
||||
this.extract = new File(this.temp, "extract");
|
||||
this.extract.mkdir();
|
||||
this.command = new ExtractCommand(this.context, this.layers);
|
||||
this.command = new ExtractLayersCommand(this.context, this.layers);
|
||||
}
|
||||
|
||||
@Test
|
||||
void runExtractsLayers() {
|
||||
given(this.context.getArchiveFile()).willReturn(this.jarFile);
|
||||
given(this.context.getWorkingDir()).willReturn(this.extract);
|
||||
this.command.run(Collections.emptyMap(), Collections.emptyList());
|
||||
this.command.run(System.out, Collections.emptyMap(), Collections.emptyList());
|
||||
assertThat(this.extract.list()).containsOnly("a", "b", "c", "d");
|
||||
assertThat(new File(this.extract, "a/a/a.jar")).exists().satisfies(this::timeAttributes);
|
||||
assertThat(new File(this.extract, "b/b/b.jar")).exists().satisfies(this::timeAttributes);
|
||||
@ -145,7 +145,8 @@ class ExtractCommandTests {
|
||||
void runWhenHasDestinationOptionExtractsLayers() {
|
||||
given(this.context.getArchiveFile()).willReturn(this.jarFile);
|
||||
File out = new File(this.extract, "out");
|
||||
this.command.run(Collections.singletonMap(ExtractCommand.DESTINATION_OPTION, out.getAbsolutePath()),
|
||||
this.command.run(System.out,
|
||||
Collections.singletonMap(ExtractLayersCommand.DESTINATION_OPTION, out.getAbsolutePath()),
|
||||
Collections.emptyList());
|
||||
assertThat(this.extract.list()).containsOnly("out");
|
||||
assertThat(new File(this.extract, "out/a/a/a.jar")).exists().satisfies(this::timeAttributes);
|
||||
@ -157,7 +158,7 @@ class ExtractCommandTests {
|
||||
void runWhenHasLayerParamsExtractsLimitedLayers() {
|
||||
given(this.context.getArchiveFile()).willReturn(this.jarFile);
|
||||
given(this.context.getWorkingDir()).willReturn(this.extract);
|
||||
this.command.run(Collections.emptyMap(), Arrays.asList("a", "c"));
|
||||
this.command.run(System.out, Collections.emptyMap(), Arrays.asList("a", "c"));
|
||||
assertThat(this.extract.list()).containsOnly("a", "c");
|
||||
assertThat(new File(this.extract, "a/a/a.jar")).exists().satisfies(this::timeAttributes);
|
||||
assertThat(new File(this.extract, "c/c/c.jar")).exists().satisfies(this::timeAttributes);
|
||||
@ -171,10 +172,9 @@ class ExtractCommandTests {
|
||||
writer.write("text");
|
||||
}
|
||||
given(this.context.getArchiveFile()).willReturn(file);
|
||||
given(this.context.getWorkingDir()).willReturn(this.extract);
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> this.command.run(Collections.emptyMap(), Collections.emptyList()))
|
||||
.withMessageContaining("not compatible with layertools");
|
||||
.isThrownBy(() -> this.command.run(System.out, Collections.emptyMap(), Collections.emptyList()))
|
||||
.withMessageContaining("not compatible");
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -191,7 +191,7 @@ class ExtractCommandTests {
|
||||
given(this.context.getArchiveFile()).willReturn(this.jarFile);
|
||||
given(this.context.getWorkingDir()).willReturn(this.extract);
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> this.command.run(Collections.emptyMap(), Collections.emptyList()))
|
||||
.isThrownBy(() -> this.command.run(System.out, Collections.emptyMap(), Collections.emptyList()))
|
||||
.withMessageContaining("Entry 'e/../../e.jar' would be written");
|
||||
}
|
||||
|
||||
@ -247,16 +247,21 @@ class ExtractCommandTests {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLayer(ZipEntry entry) {
|
||||
if (entry.getName().startsWith("a")) {
|
||||
public String getLayer(String entryName) {
|
||||
if (entryName.startsWith("a")) {
|
||||
return "a";
|
||||
}
|
||||
if (entry.getName().startsWith("b")) {
|
||||
if (entryName.startsWith("b")) {
|
||||
return "b";
|
||||
}
|
||||
return "c";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getApplicationLayerName() {
|
||||
return "application";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.jarmode.tools;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
|
||||
/**
|
||||
* Tests for {@link HelpCommand}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class HelpCommandTests {
|
||||
|
||||
private HelpCommand command;
|
||||
|
||||
private TestPrintStream out;
|
||||
|
||||
@TempDir
|
||||
Path temp;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
Context context = Mockito.mock(Context.class);
|
||||
given(context.getArchiveFile()).willReturn(this.temp.resolve("test.jar").toFile());
|
||||
this.command = new HelpCommand(context, List.of(new TestCommand()), "tools");
|
||||
this.out = new TestPrintStream(this);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPrintAllCommands() {
|
||||
this.command.run(this.out, Collections.emptyList());
|
||||
assertThat(this.out).hasSameContentAsResource("help-output.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPrintCommandSpecificHelp() {
|
||||
this.command.run(this.out, List.of("test"));
|
||||
System.out.println(this.out);
|
||||
assertThat(this.out).hasSameContentAsResource("help-test-output.txt");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.jarmode.tools;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.UnaryOperator;
|
||||
import java.util.jar.JarOutputStream;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import org.springframework.boot.jarmode.tools.JarStructure.Entry;
|
||||
import org.springframework.boot.jarmode.tools.JarStructure.Entry.Type;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link IndexedJarStructure}.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class IndexedJarStructureTests {
|
||||
|
||||
@Test
|
||||
void shouldResolveLibraryEntry() throws IOException {
|
||||
IndexedJarStructure structure = createStructure();
|
||||
Entry entry = structure.resolve("BOOT-INF/lib/spring-webmvc-6.1.4.jar");
|
||||
assertThat(entry.location()).isEqualTo("spring-webmvc-6.1.4.jar");
|
||||
assertThat(entry.originalLocation()).isEqualTo("BOOT-INF/lib/spring-webmvc-6.1.4.jar");
|
||||
assertThat(entry.type()).isEqualTo(Type.LIBRARY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveApplicationEntry() throws IOException {
|
||||
IndexedJarStructure structure = createStructure();
|
||||
Entry entry = structure.resolve("BOOT-INF/classes/application.properties");
|
||||
assertThat(entry.location()).isEqualTo("application.properties");
|
||||
assertThat(entry.originalLocation()).isEqualTo("BOOT-INF/classes/application.properties");
|
||||
assertThat(entry.type()).isEqualTo(Type.APPLICATION_CLASS_OR_RESOURCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveLoaderEntry() throws IOException {
|
||||
IndexedJarStructure structure = createStructure();
|
||||
Entry entry = structure.resolve("org/springframework/boot/loader/launch/JarLauncher");
|
||||
assertThat(entry.location()).isEqualTo("org/springframework/boot/loader/launch/JarLauncher");
|
||||
assertThat(entry.originalLocation()).isEqualTo("org/springframework/boot/loader/launch/JarLauncher");
|
||||
assertThat(entry.type()).isEqualTo(Type.LOADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotResolveNonExistingLibs() throws IOException {
|
||||
IndexedJarStructure structure = createStructure();
|
||||
Entry entry = structure.resolve("BOOT-INF/lib/doesnt-exists.jar");
|
||||
assertThat(entry).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateLauncherManifest() throws IOException {
|
||||
IndexedJarStructure structure = createStructure();
|
||||
Manifest manifest = structure.createLauncherManifest(UnaryOperator.identity());
|
||||
Map<String, String> attributes = getAttributes(manifest);
|
||||
assertThat(attributes).containsEntry("Manifest-Version", "1.0")
|
||||
.containsEntry("Implementation-Title", "IndexedJarStructureTests")
|
||||
.containsEntry("Spring-Boot-Version", "3.3.0-SNAPSHOT")
|
||||
.containsEntry("Implementation-Version", "0.0.1-SNAPSHOT")
|
||||
.containsEntry("Build-Jdk-Spec", "17")
|
||||
.containsEntry("Class-Path",
|
||||
"spring-webmvc-6.1.4.jar spring-web-6.1.4.jar spring-boot-autoconfigure-3.3.0-SNAPSHOT.jar spring-boot-3.3.0-SNAPSHOT.jar jakarta.annotation-api-2.1.1.jar spring-context-6.1.4.jar spring-aop-6.1.4.jar spring-beans-6.1.4.jar spring-expression-6.1.4.jar spring-core-6.1.4.jar snakeyaml-2.2.jar jackson-datatype-jdk8-2.16.1.jar jackson-datatype-jsr310-2.16.1.jar jackson-module-parameter-names-2.16.1.jar jackson-databind-2.16.1.jar tomcat-embed-websocket-10.1.19.jar tomcat-embed-core-10.1.19.jar tomcat-embed-el-10.1.19.jar micrometer-observation-1.13.0-M1.jar logback-classic-1.4.14.jar log4j-to-slf4j-2.23.0.jar jul-to-slf4j-2.0.12.jar spring-jcl-6.1.4.jar jackson-annotations-2.16.1.jar jackson-core-2.16.1.jar micrometer-commons-1.13.0-M1.jar logback-core-1.4.14.jar slf4j-api-2.0.12.jar log4j-api-2.23.0.jar")
|
||||
.containsEntry("Main-Class", "org.springframework.boot.jarmode.tools.IndexedJarStructureTests")
|
||||
.doesNotContainKeys("Start-Class", "Spring-Boot-Classes", "Spring-Boot-Lib", "Spring-Boot-Classpath-Index",
|
||||
"Spring-Boot-Layers-Index");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldLoadFromFile(@TempDir File tempDir) throws IOException {
|
||||
File jarFile = new File(tempDir, "test.jar");
|
||||
try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(jarFile), createManifest())) {
|
||||
outputStream.putNextEntry(new ZipEntry("BOOT-INF/classpath.idx"));
|
||||
outputStream.write(createIndexFile().getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.closeEntry();
|
||||
}
|
||||
IndexedJarStructure structure = IndexedJarStructure.get(jarFile);
|
||||
assertThat(structure).isNotNull();
|
||||
assertThat(structure.resolve("BOOT-INF/lib/spring-webmvc-6.1.4.jar")).extracting(Entry::type)
|
||||
.isEqualTo(Type.LIBRARY);
|
||||
assertThat(structure.resolve("BOOT-INF/classes/application.properties")).extracting(Entry::type)
|
||||
.isEqualTo(Type.APPLICATION_CLASS_OR_RESOURCE);
|
||||
}
|
||||
|
||||
private Map<String, String> getAttributes(Manifest manifest) {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
manifest.getMainAttributes().forEach((key, value) -> result.put(key.toString(), value.toString()));
|
||||
return result;
|
||||
}
|
||||
|
||||
private IndexedJarStructure createStructure() throws IOException {
|
||||
return new IndexedJarStructure(createManifest(), createIndexFile());
|
||||
}
|
||||
|
||||
private String createIndexFile() {
|
||||
return """
|
||||
- "BOOT-INF/lib/spring-webmvc-6.1.4.jar"
|
||||
- "BOOT-INF/lib/spring-web-6.1.4.jar"
|
||||
- "BOOT-INF/lib/spring-boot-autoconfigure-3.3.0-SNAPSHOT.jar"
|
||||
- "BOOT-INF/lib/spring-boot-3.3.0-SNAPSHOT.jar"
|
||||
- "BOOT-INF/lib/jakarta.annotation-api-2.1.1.jar"
|
||||
- "BOOT-INF/lib/spring-context-6.1.4.jar"
|
||||
- "BOOT-INF/lib/spring-aop-6.1.4.jar"
|
||||
- "BOOT-INF/lib/spring-beans-6.1.4.jar"
|
||||
- "BOOT-INF/lib/spring-expression-6.1.4.jar"
|
||||
- "BOOT-INF/lib/spring-core-6.1.4.jar"
|
||||
- "BOOT-INF/lib/snakeyaml-2.2.jar"
|
||||
- "BOOT-INF/lib/jackson-datatype-jdk8-2.16.1.jar"
|
||||
- "BOOT-INF/lib/jackson-datatype-jsr310-2.16.1.jar"
|
||||
- "BOOT-INF/lib/jackson-module-parameter-names-2.16.1.jar"
|
||||
- "BOOT-INF/lib/jackson-databind-2.16.1.jar"
|
||||
- "BOOT-INF/lib/tomcat-embed-websocket-10.1.19.jar"
|
||||
- "BOOT-INF/lib/tomcat-embed-core-10.1.19.jar"
|
||||
- "BOOT-INF/lib/tomcat-embed-el-10.1.19.jar"
|
||||
- "BOOT-INF/lib/micrometer-observation-1.13.0-M1.jar"
|
||||
- "BOOT-INF/lib/logback-classic-1.4.14.jar"
|
||||
- "BOOT-INF/lib/log4j-to-slf4j-2.23.0.jar"
|
||||
- "BOOT-INF/lib/jul-to-slf4j-2.0.12.jar"
|
||||
- "BOOT-INF/lib/spring-jcl-6.1.4.jar"
|
||||
- "BOOT-INF/lib/jackson-annotations-2.16.1.jar"
|
||||
- "BOOT-INF/lib/jackson-core-2.16.1.jar"
|
||||
- "BOOT-INF/lib/micrometer-commons-1.13.0-M1.jar"
|
||||
- "BOOT-INF/lib/logback-core-1.4.14.jar"
|
||||
- "BOOT-INF/lib/slf4j-api-2.0.12.jar"
|
||||
- "BOOT-INF/lib/log4j-api-2.23.0.jar"
|
||||
""";
|
||||
}
|
||||
|
||||
private Manifest createManifest() throws IOException {
|
||||
return new Manifest(new ByteArrayInputStream("""
|
||||
Manifest-Version: 1.0
|
||||
Main-Class: org.springframework.boot.loader.launch.JarLauncher
|
||||
Start-Class: org.springframework.boot.jarmode.tools.IndexedJarStructureTests
|
||||
Spring-Boot-Version: 3.3.0-SNAPSHOT
|
||||
Spring-Boot-Classes: BOOT-INF/classes/
|
||||
Spring-Boot-Lib: BOOT-INF/lib/
|
||||
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
|
||||
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
|
||||
Build-Jdk-Spec: 17
|
||||
Implementation-Title: IndexedJarStructureTests
|
||||
Implementation-Version: 0.0.1-SNAPSHOT
|
||||
""".getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.jarmode.layertools;
|
||||
package org.springframework.boot.jarmode.tools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
@ -46,46 +46,46 @@ class IndexedLayersTests {
|
||||
|
||||
@Test
|
||||
void createWhenIndexFileIsEmptyThrowsException() {
|
||||
assertThatIllegalStateException().isThrownBy(() -> new IndexedLayers(" \n "))
|
||||
assertThatIllegalStateException().isThrownBy(() -> new IndexedLayers(" \n ", "BOOT-INF/classes"))
|
||||
.withMessage("Empty layer index file loaded");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenIndexFileIsMalformedThrowsException() {
|
||||
assertThatIllegalStateException().isThrownBy(() -> new IndexedLayers("test"))
|
||||
assertThatIllegalStateException().isThrownBy(() -> new IndexedLayers("test", "BOOT-INF/classes"))
|
||||
.withMessage("Layer index file is malformed");
|
||||
}
|
||||
|
||||
@Test
|
||||
void iteratorReturnsLayers() throws Exception {
|
||||
IndexedLayers layers = new IndexedLayers(getIndex());
|
||||
IndexedLayers layers = new IndexedLayers(getIndex(), "BOOT-INF/classes");
|
||||
assertThat(layers).containsExactly("test", "empty", "application");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLayerWhenMatchesNameReturnsLayer() throws Exception {
|
||||
IndexedLayers layers = new IndexedLayers(getIndex());
|
||||
IndexedLayers layers = new IndexedLayers(getIndex(), "BOOT-INF/classes");
|
||||
assertThat(layers.getLayer(mockEntry("BOOT-INF/lib/a.jar"))).isEqualTo("test");
|
||||
assertThat(layers.getLayer(mockEntry("BOOT-INF/classes/Demo.class"))).isEqualTo("application");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLayerWhenMatchesNameForMissingLayerThrowsException() throws Exception {
|
||||
IndexedLayers layers = new IndexedLayers(getIndex());
|
||||
IndexedLayers layers = new IndexedLayers(getIndex(), "BOOT-INF/classes");
|
||||
assertThatIllegalStateException().isThrownBy(() -> layers.getLayer(mockEntry("file.jar")))
|
||||
.withMessage("No layer defined in index for file " + "'file.jar'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLayerWhenMatchesDirectoryReturnsLayer() throws Exception {
|
||||
IndexedLayers layers = new IndexedLayers(getIndex());
|
||||
IndexedLayers layers = new IndexedLayers(getIndex(), "BOOT-INF/classes");
|
||||
assertThat(layers.getLayer(mockEntry("META-INF/MANIFEST.MF"))).isEqualTo("application");
|
||||
assertThat(layers.getLayer(mockEntry("META-INF/a/sub/directory/and/a/file"))).isEqualTo("application");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLayerWhenFileHasSpaceReturnsLayer() throws Exception {
|
||||
IndexedLayers layers = new IndexedLayers(getIndex());
|
||||
IndexedLayers layers = new IndexedLayers(getIndex(), "BOOT-INF/classes");
|
||||
assertThat(layers.getLayer(mockEntry("a b/c d"))).isEqualTo("application");
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.jarmode.layertools;
|
||||
package org.springframework.boot.jarmode.tools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
@ -62,43 +62,45 @@ class LayerToolsJarModeTests {
|
||||
this.out = new TestPrintStream(this);
|
||||
this.systemOut = System.out;
|
||||
System.setOut(this.out);
|
||||
LayerToolsJarMode.Runner.contextOverride = context;
|
||||
LayerToolsJarMode.contextOverride = context;
|
||||
System.setProperty("jarmode", "layertools");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void restore() {
|
||||
System.setOut(this.systemOut);
|
||||
LayerToolsJarMode.Runner.contextOverride = null;
|
||||
LayerToolsJarMode.contextOverride = null;
|
||||
System.clearProperty("jarmode");
|
||||
}
|
||||
|
||||
@Test
|
||||
void mainWithNoParametersShowsHelp() {
|
||||
new LayerToolsJarMode().run("layertools", NO_ARGS);
|
||||
assertThat(this.out).hasSameContentAsResource("help-output.txt");
|
||||
assertThat(this.out).hasSameContentAsResource("layertools-help-output.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void mainWithArgRunsCommand() {
|
||||
new LayerToolsJarMode().run("layertools", new String[] { "list" });
|
||||
assertThat(this.out).hasSameContentAsResource("list-output.txt");
|
||||
assertThat(this.out).hasSameContentAsResource("layertools-list-output.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void mainWithUnknownCommandShowsErrorAndHelp() {
|
||||
new LayerToolsJarMode().run("layertools", new String[] { "invalid" });
|
||||
assertThat(this.out).hasSameContentAsResource("error-command-unknown-output.txt");
|
||||
assertThat(this.out).hasSameContentAsResource("layertools-error-command-unknown-output.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void mainWithUnknownOptionShowsErrorAndCommandHelp() {
|
||||
new LayerToolsJarMode().run("layertools", new String[] { "extract", "--invalid" });
|
||||
assertThat(this.out).hasSameContentAsResource("error-option-unknown-output.txt");
|
||||
assertThat(this.out).hasSameContentAsResource("layertools-error-option-unknown-output.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void mainWithOptionMissingRequiredValueShowsErrorAndCommandHelp() {
|
||||
new LayerToolsJarMode().run("layertools", new String[] { "extract", "--destination" });
|
||||
assertThat(this.out).hasSameContentAsResource("error-option-missing-value-output.txt");
|
||||
assertThat(this.out).hasSameContentAsResource("layertools-error-option-missing-value-output.txt");
|
||||
}
|
||||
|
||||
private File createJarFile(String name) throws Exception {
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@ -14,12 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.jarmode.layertools;
|
||||
package org.springframework.boot.jarmode.tools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@ -35,7 +34,6 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
@ -55,16 +53,14 @@ class ListCommandTests {
|
||||
@Mock
|
||||
private Context context;
|
||||
|
||||
private File jarFile;
|
||||
|
||||
private ListCommand command;
|
||||
|
||||
private TestPrintStream out;
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws Exception {
|
||||
this.jarFile = createJarFile("test.jar");
|
||||
given(this.context.getArchiveFile()).willReturn(this.jarFile);
|
||||
File jarFile = createJarFile("test.jar");
|
||||
given(this.context.getArchiveFile()).willReturn(jarFile);
|
||||
this.command = new ListCommand(this.context);
|
||||
this.out = new TestPrintStream(this);
|
||||
}
|
||||
@ -73,7 +69,7 @@ class ListCommandTests {
|
||||
void listLayersShouldListLayers() {
|
||||
Layers layers = IndexedLayers.get(this.context);
|
||||
this.command.printLayers(layers, this.out);
|
||||
assertThat(this.out).hasSameContentAsResource("list-output.txt");
|
||||
assertThat(this.out).hasSameContentAsResource("list-output-without-deprecation.txt");
|
||||
}
|
||||
|
||||
private File createJarFile(String name) throws Exception {
|
||||
@ -117,9 +113,7 @@ class ListCommandTests {
|
||||
}
|
||||
|
||||
private String getFile(String fileName) throws Exception {
|
||||
ClassPathResource resource = new ClassPathResource(fileName, getClass());
|
||||
InputStreamReader reader = new InputStreamReader(resource.getInputStream());
|
||||
return FileCopyUtils.copyToString(reader);
|
||||
return new ClassPathResource(fileName, getClass()).getContentAsString(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.jarmode.tools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link ListLayersCommand}.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class ListLayersCommandTests extends AbstractTests {
|
||||
|
||||
@Test
|
||||
void shouldListLayers() throws IOException {
|
||||
Manifest manifest = createManifest("Spring-Boot-Layers-Index: META-INF/layers.idx");
|
||||
TestPrintStream out = run(createArchive(manifest, "META-INF/layers.idx", "/jar-contents/layers.idx"));
|
||||
assertThat(out).hasSameContentAsResource("list-layers-output.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPrintErrorWhenLayersAreNotEnabled() throws IOException {
|
||||
TestPrintStream out = run(createArchive());
|
||||
assertThat(out).hasSameContentAsResource("list-layers-output-layers-disabled.txt");
|
||||
}
|
||||
|
||||
private TestPrintStream run(File archive) {
|
||||
return runCommand(ListLayersCommand::new, archive);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.jarmode.tools;
|
||||
|
||||
import java.io.PrintStream;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class TestCommand extends Command {
|
||||
|
||||
TestCommand() {
|
||||
super("test", "Description of test",
|
||||
Options.of(Option.of("option1", "value1", "Description of option1"),
|
||||
Option.of("option2", "value2", "Description of option2")),
|
||||
Parameters.of("parameter1", "parameter2"));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run(PrintStream out, Map<Option, String> options, List<String> parameters) {
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.jarmode.layertools;
|
||||
package org.springframework.boot.jarmode.tools;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
@ -27,7 +27,7 @@ import org.assertj.core.api.AbstractAssert;
|
||||
import org.assertj.core.api.AssertProvider;
|
||||
import org.assertj.core.api.Assertions;
|
||||
|
||||
import org.springframework.boot.jarmode.layertools.TestPrintStream.PrintStreamAssert;
|
||||
import org.springframework.boot.jarmode.tools.TestPrintStream.PrintStreamAssert;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
|
||||
/**
|
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.jarmode.tools;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link ToolsJarMode}.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class ToolsJarModeTests extends AbstractTests {
|
||||
|
||||
private ToolsJarMode mode;
|
||||
|
||||
private TestPrintStream out;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
this.out = new TestPrintStream(this);
|
||||
Context context = new Context(createArchive(), this.tempDir);
|
||||
this.mode = new ToolsJarMode(context, this.out);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAcceptToolsMode() {
|
||||
assertThat(this.mode.accepts("tools")).isTrue();
|
||||
assertThat(this.mode.accepts("something-else")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void noParametersShowsHelp() {
|
||||
run();
|
||||
assertThat(this.out).hasSameContentAsResource("tools-help-output.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void helpForExtract() {
|
||||
run("help", "extract");
|
||||
assertThat(this.out).hasSameContentAsResource("tools-help-extract-output.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void helpForListLayers() {
|
||||
run("help", "list-layers");
|
||||
assertThat(this.out).hasSameContentAsResource("tools-help-list-layers-output.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void helpForHelp() {
|
||||
run("help", "help");
|
||||
assertThat(this.out).hasSameContentAsResource("tools-help-help-output.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void helpForUnknownCommand() {
|
||||
run("help", "unknown-command");
|
||||
assertThat(this.out).hasSameContentAsResource("tools-help-unknown-command-output.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownCommandShowsErrorAndHelp() {
|
||||
run("something-invalid");
|
||||
assertThat(this.out).hasSameContentAsResource("tools-error-command-unknown-output.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownOptionShowsErrorAndCommandHelp() {
|
||||
run("extract", "--something-invalid");
|
||||
assertThat(this.out).hasSameContentAsResource("tools-error-option-unknown-output.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void optionMissingRequiredValueShowsErrorAndCommandHelp() {
|
||||
run("extract", "--destination");
|
||||
assertThat(this.out).hasSameContentAsResource("tools-error-option-missing-value-output.txt");
|
||||
}
|
||||
|
||||
private void run(String... args) {
|
||||
this.mode.run("tools", args);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1 @@
|
||||
spring.application.name=test
|
@ -0,0 +1,3 @@
|
||||
- "BOOT-INF/lib/dependency-1.jar"
|
||||
- "BOOT-INF/lib/dependency-2.jar"
|
||||
- "BOOT-INF/lib/dependency-3-SNAPSHOT.jar"
|
@ -0,0 +1,12 @@
|
||||
- "dependencies":
|
||||
- "BOOT-INF/lib/dependency-1.jar"
|
||||
- "BOOT-INF/lib/dependency-2.jar"
|
||||
- "spring-boot-loader":
|
||||
- "org/"
|
||||
- "snapshot-dependencies":
|
||||
- "BOOT-INF/lib/dependency-3-SNAPSHOT.jar"
|
||||
- "application":
|
||||
- "BOOT-INF/classes/"
|
||||
- "BOOT-INF/classpath.idx"
|
||||
- "BOOT-INF/layers.idx"
|
||||
- "META-INF/"
|
@ -0,0 +1,2 @@
|
||||
Error: Layers are not enabled
|
||||
|
@ -0,0 +1,6 @@
|
||||
Usage:
|
||||
java -Djarmode=tools -jar test.jar
|
||||
|
||||
Available commands:
|
||||
test Description of test
|
||||
help Help about any command
|
@ -0,0 +1,8 @@
|
||||
Description of test
|
||||
|
||||
Usage:
|
||||
java -Djarmode=tools -jar test.jar test [options] parameter1 parameter2
|
||||
|
||||
Options:
|
||||
--option1 value1 Description of option1
|
||||
--option2 value2 Description of option2
|
@ -4,6 +4,7 @@ Usage:
|
||||
java -Djarmode=layertools -jar test.jar
|
||||
|
||||
Available commands:
|
||||
help Help about any command
|
||||
Deprecated commands:
|
||||
list List layers from the jar that can be extracted
|
||||
extract Extracts layers from the jar for image creation
|
||||
help Help about any command
|
@ -1,3 +1,5 @@
|
||||
Warning: This command is deprecated. Use '-Djarmode=tools extract --layers --launcher' instead.
|
||||
|
||||
Error: Option "--destination" for the extract command requires a value
|
||||
|
||||
Extracts layers from the jar for image creation
|
@ -1,3 +1,5 @@
|
||||
Warning: This command is deprecated. Use '-Djarmode=tools extract --layers --launcher' instead.
|
||||
|
||||
Error: Unknown option "--invalid" for the extract command
|
||||
|
||||
Extracts layers from the jar for image creation
|
@ -2,6 +2,7 @@ Usage:
|
||||
java -Djarmode=layertools -jar test.jar
|
||||
|
||||
Available commands:
|
||||
help Help about any command
|
||||
Deprecated commands:
|
||||
list List layers from the jar that can be extracted
|
||||
extract Extracts layers from the jar for image creation
|
||||
help Help about any command
|
@ -0,0 +1,5 @@
|
||||
Warning: This command is deprecated. Use '-Djarmode=tools list-layers' instead.
|
||||
|
||||
0001
|
||||
0002
|
||||
0003
|
@ -0,0 +1,2 @@
|
||||
Error: Layers are not enabled
|
||||
|
@ -0,0 +1,4 @@
|
||||
dependencies
|
||||
spring-boot-loader
|
||||
snapshot-dependencies
|
||||
application
|
@ -0,0 +1,9 @@
|
||||
Error: Unknown command "something-invalid"
|
||||
|
||||
Usage:
|
||||
java -Djarmode=tools -jar test.jar
|
||||
|
||||
Available commands:
|
||||
extract Extract the contents from the jar
|
||||
list-layers List layers from the jar that can be extracted
|
||||
help Help about any command
|
@ -0,0 +1,13 @@
|
||||
Error: Option "--destination" for the extract command requires a value
|
||||
|
||||
Extract the contents from the jar
|
||||
|
||||
Usage:
|
||||
java -Djarmode=tools -jar test.jar extract [options]
|
||||
|
||||
Options:
|
||||
--launcher Whether to extract the Spring Boot launcher
|
||||
--layers string list Layers to extract
|
||||
--destination string Directory to extract files to. Defaults to the current working directory
|
||||
--libraries string Name of the libraries directory. Only applicable when not using --launcher. Defaults to lib/
|
||||
--runner-filename string Name of the runner JAR file. Only applicable when not using --launcher. Defaults to runner.jar
|
@ -0,0 +1,13 @@
|
||||
Error: Unknown option "--something-invalid" for the extract command
|
||||
|
||||
Extract the contents from the jar
|
||||
|
||||
Usage:
|
||||
java -Djarmode=tools -jar test.jar extract [options]
|
||||
|
||||
Options:
|
||||
--launcher Whether to extract the Spring Boot launcher
|
||||
--layers string list Layers to extract
|
||||
--destination string Directory to extract files to. Defaults to the current working directory
|
||||
--libraries string Name of the libraries directory. Only applicable when not using --launcher. Defaults to lib/
|
||||
--runner-filename string Name of the runner JAR file. Only applicable when not using --launcher. Defaults to runner.jar
|
@ -0,0 +1,11 @@
|
||||
Extract the contents from the jar
|
||||
|
||||
Usage:
|
||||
java -Djarmode=tools -jar test.jar extract [options]
|
||||
|
||||
Options:
|
||||
--launcher Whether to extract the Spring Boot launcher
|
||||
--layers string list Layers to extract
|
||||
--destination string Directory to extract files to. Defaults to the current working directory
|
||||
--libraries string Name of the libraries directory. Only applicable when not using --launcher. Defaults to lib/
|
||||
--runner-filename string Name of the runner JAR file. Only applicable when not using --launcher. Defaults to runner.jar
|
@ -0,0 +1,4 @@
|
||||
Help about any command
|
||||
|
||||
Usage:
|
||||
java -Djarmode=tools -jar test.jar help [<command>]
|
@ -0,0 +1,4 @@
|
||||
List layers from the jar that can be extracted
|
||||
|
||||
Usage:
|
||||
java -Djarmode=tools -jar test.jar list-layers
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user