From 793aca60d2d6ad4ff1618bc19672038cd4755120 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Fri, 9 Feb 2024 15:04:46 +0100 Subject: [PATCH] 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 --- settings.gradle | 2 +- .../spring-boot-dependencies/build.gradle | 2 +- .../gradle/tasks/bundling/BootArchive.java | 12 +- .../tasks/bundling/BootArchiveSupport.java | 6 +- .../boot/gradle/tasks/bundling/BootJar.java | 21 +- .../boot/gradle/tasks/bundling/BootWar.java | 21 +- .../tasks/bundling/BootZipCopyAction.java | 10 +- .../gradle/tasks/bundling/LayeredSpec.java | 4 +- .../AbstractBootArchiveIntegrationTests.java | 23 +- .../bundling/AbstractBootArchiveTests.java | 32 +- .../bundling/BootJarIntegrationTests.java | 6 +- ...ootJarIntegrationTests-customLayers.gradle | 8 +- ...endenciesAreNotIncludedInTheArchive.gradle | 4 +- ...AreNotIncludedInTheArchiveByDefault.gradle | 4 +- ...pendenciesCanBeIncludedInTheArchive.gradle | 4 +- ...tJarIntegrationTests-implicitLayers.gradle | 8 +- ...tionTests-jarTypeFilteringIsApplied.gradle | 4 +- ...tionTests-layersWithCustomSourceSet.gradle | 8 +- ...rationTests-multiModuleCustomLayers.gradle | 8 +- ...tionTests-multiModuleImplicitLayers.gradle | 8 +- ...enBuiltWithToolsAndThenWithoutTools.gradle | 9 + ...AreNotIncludedInTheArchiveByDefault.gradle | 4 +- ...pendenciesCanBeIncludedInTheArchive.gradle | 4 +- ...ootWarIntegrationTests-customLayers.gradle | 8 +- ...AreNotIncludedInTheArchiveByDefault.gradle | 4 +- ...pendenciesCanBeIncludedInTheArchive.gradle | 4 +- ...tWarIntegrationTests-implicitLayers.gradle | 8 +- ...tionTests-jarTypeFilteringIsApplied.gradle | 4 +- ...tionTests-layersWithCustomSourceSet.gradle | 8 +- ...rationTests-multiModuleCustomLayers.gradle | 8 +- ...tionTests-multiModuleImplicitLayers.gradle | 8 +- ...enBuiltWithToolsAndThenWithoutTools.gradle | 10 + ...AreNotIncludedInTheArchiveByDefault.gradle | 4 +- ...pendenciesCanBeIncludedInTheArchive.gradle | 4 +- .../jarmode/layertools/ExtractCommand.java | 119 ----- .../jarmode/layertools/LayerToolsJarMode.java | 116 ----- .../main/resources/META-INF/spring.factories | 3 - .../jarmode/layertools/HelpCommandTests.java | 103 ----- .../layertools/help-extract-output.txt | 7 - .../build.gradle | 2 +- .../boot/jarmode/tools}/Command.java | 93 +++- .../boot/jarmode/tools}/Context.java | 4 +- .../boot/jarmode/tools/ExtractCommand.java | 436 ++++++++++++++++++ .../jarmode/tools/ExtractLayersCommand.java | 69 +++ .../boot/jarmode/tools}/HelpCommand.java | 64 ++- .../jarmode/tools/IndexedJarStructure.java | 159 +++++++ .../boot/jarmode/tools}/IndexedLayers.java | 25 +- .../boot/jarmode/tools/JarStructure.java | 78 ++++ .../boot/jarmode/tools/LayerToolsJarMode.java | 54 +++ .../boot/jarmode/tools}/Layers.java | 33 +- .../boot/jarmode/tools}/ListCommand.java | 27 +- .../boot/jarmode/tools/ListLayersCommand.java | 59 +++ .../jarmode/tools}/MissingValueException.java | 4 +- .../boot/jarmode/tools/Runner.java | 94 ++++ .../boot/jarmode/tools/ToolsJarMode.java | 64 +++ .../tools}/UnknownOptionException.java | 4 +- .../boot/jarmode/tools}/package-info.java | 4 +- .../main/resources/META-INF/spring.factories | 4 + .../boot/jarmode/tools/AbstractTests.java | 154 +++++++ .../boot/jarmode/tools}/CommandTests.java | 27 +- .../boot/jarmode/tools}/ContextTests.java | 4 +- .../jarmode/tools/ExtractCommandTests.java | 303 ++++++++++++ .../tools/ExtractLayersCommandTests.java} | 35 +- .../boot/jarmode/tools/HelpCommandTests.java | 66 +++ .../tools/IndexedJarStructureTests.java | 173 +++++++ .../jarmode/tools}/IndexedLayersTests.java | 18 +- .../tools}/LayerToolsJarModeTests.java | 20 +- .../boot/jarmode/tools}/ListCommandTests.java | 18 +- .../jarmode/tools/ListLayersCommandTests.java | 51 ++ .../boot/jarmode/tools/TestCommand.java | 40 ++ .../boot/jarmode/tools}/TestPrintStream.java | 6 +- .../boot/jarmode/tools/ToolsJarModeTests.java | 102 ++++ .../test/resources/jar-contents/JarLauncher | 0 .../jar-contents/application.properties | 1 + .../test/resources/jar-contents/classpath.idx | 3 + .../test/resources/jar-contents/dependency-1 | 0 .../test/resources/jar-contents/dependency-2 | 0 .../jar-contents/dependency-3-SNAPSHOT | 0 .../test/resources/jar-contents/layers.idx | 12 + ...ommand-printErrorIfLayersAreNotEnabled.txt | 2 + .../boot/jarmode/tools/help-output.txt | 6 + .../boot/jarmode/tools/help-test-output.txt | 8 + ...yertools-error-command-unknown-output.txt} | 3 +- ...ols-error-option-missing-value-output.txt} | 2 + ...ayertools-error-option-unknown-output.txt} | 2 + .../jarmode/tools/layertools-help-output.txt} | 3 +- .../jarmode/tools/layertools-list-output.txt | 5 + .../list-layers-output-layers-disabled.txt | 2 + .../boot/jarmode/tools/list-layers-output.txt | 4 + .../list-output-without-deprecation.txt} | 0 .../boot/jarmode/tools}/test-layers.idx | 0 .../boot/jarmode/tools}/test-manifest.MF | 0 .../boot/jarmode/tools}/test-war-layers.idx | 0 .../boot/jarmode/tools}/test-war-manifest.MF | 0 .../tools-error-command-unknown-output.txt | 9 + ...ools-error-option-missing-value-output.txt | 13 + .../tools-error-option-unknown-output.txt | 13 + .../tools/tools-help-extract-output.txt | 11 + .../jarmode/tools/tools-help-help-output.txt | 4 + .../tools/tools-help-list-layers-output.txt | 4 + .../boot/jarmode/tools/tools-help-output.txt | 7 + .../tools-help-unknown-command-output.txt | 9 + .../spring-boot-loader-tools/build.gradle | 8 +- .../boot/loader/tools/JarModeLibrary.java | 4 +- .../boot/loader/tools/Packager.java | 4 +- .../loader/tools/AbstractPackagerTests.java | 4 +- .../boot/maven/JarIntegrationTests.java | 26 +- .../boot/maven/WarIntegrationTests.java | 25 +- .../projects/jar-no-tools/jar-release/pom.xml | 11 + .../jar-no-tools/jar-snapshot/pom.xml | 11 + .../intTest/projects/jar-no-tools/jar/pom.xml | 44 ++ .../main/java/org/test/SampleApplication.java | 24 + .../src/intTest/projects/jar-no-tools/pom.xml | 19 + .../projects/war-no-tools/jar-release/pom.xml | 11 + .../war-no-tools/jar-snapshot/pom.xml | 11 + .../src/intTest/projects/war-no-tools/pom.xml | 19 + .../intTest/projects/war-no-tools/war/pom.xml | 67 +++ .../main/java/org/test/SampleApplication.java | 24 + .../war/src/main/webapp/index.html | 1 + .../boot/maven/AbstractPackagerMojo.java | 27 +- .../springframework/boot/maven/Layers.java | 5 +- 121 files changed, 2780 insertions(+), 612 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-notUpToDateWhenBuiltWithToolsAndThenWithoutTools.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenBuiltWithToolsAndThenWithoutTools.gradle delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/ExtractCommand.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/LayerToolsJarMode.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/resources/META-INF/spring.factories delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/HelpCommandTests.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/help-extract-output.txt rename spring-boot-project/spring-boot-tools/{spring-boot-jarmode-layertools => spring-boot-jarmode-tools}/build.gradle (93%) rename spring-boot-project/spring-boot-tools/{spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools => spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools}/Command.java (76%) rename spring-boot-project/spring-boot-tools/{spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools => spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools}/Context.java (97%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractLayersCommand.java rename spring-boot-project/spring-boot-tools/{spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools => spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools}/HelpCommand.java (58%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedJarStructure.java rename spring-boot-project/spring-boot-tools/{spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools => spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools}/IndexedLayers.java (83%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/JarStructure.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/LayerToolsJarMode.java rename spring-boot-project/spring-boot-tools/{spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools => spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools}/Layers.java (63%) rename spring-boot-project/spring-boot-tools/{spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools => spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools}/ListCommand.java (60%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ListLayersCommand.java rename spring-boot-project/spring-boot-tools/{spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools => spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools}/MissingValueException.java (89%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Runner.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ToolsJarMode.java rename spring-boot-project/spring-boot-tools/{spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools => spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools}/UnknownOptionException.java (89%) rename spring-boot-project/spring-boot-tools/{spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools => spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools}/package-info.java (87%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/resources/META-INF/spring.factories create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/AbstractTests.java rename spring-boot-project/spring-boot-tools/{spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools => spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools}/CommandTests.java (85%) rename spring-boot-project/spring-boot-tools/{spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools => spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools}/ContextTests.java (96%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java rename spring-boot-project/spring-boot-tools/{spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ExtractCommandTests.java => spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractLayersCommandTests.java} (89%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/HelpCommandTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedJarStructureTests.java rename spring-boot-project/spring-boot-tools/{spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools => spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools}/IndexedLayersTests.java (88%) rename spring-boot-project/spring-boot-tools/{spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools => spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools}/LayerToolsJarModeTests.java (83%) rename spring-boot-project/spring-boot-tools/{spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools => spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools}/ListCommandTests.java (85%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ListLayersCommandTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/TestCommand.java rename spring-boot-project/spring-boot-tools/{spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools => spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools}/TestPrintStream.java (91%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ToolsJarModeTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/JarLauncher create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/application.properties create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/classpath.idx create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/dependency-1 create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/dependency-2 create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/dependency-3-SNAPSHOT create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/layers.idx create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/ExtractCommand-printErrorIfLayersAreNotEnabled.txt create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/help-output.txt create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/help-test-output.txt rename spring-boot-project/spring-boot-tools/{spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-command-unknown-output.txt => spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-command-unknown-output.txt} (92%) rename spring-boot-project/spring-boot-tools/{spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-option-missing-value-output.txt => spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-missing-value-output.txt} (73%) rename spring-boot-project/spring-boot-tools/{spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-option-unknown-output.txt => spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-unknown-output.txt} (72%) rename spring-boot-project/spring-boot-tools/{spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/help-output.txt => spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-help-output.txt} (91%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-list-output.txt create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-layers-output-layers-disabled.txt create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-layers-output.txt rename spring-boot-project/spring-boot-tools/{spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/list-output.txt => spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-output-without-deprecation.txt} (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools => spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools}/test-layers.idx (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools => spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools}/test-manifest.MF (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools => spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools}/test-war-layers.idx (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools => spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools}/test-war-manifest.MF (100%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-error-command-unknown-output.txt create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-error-option-missing-value-output.txt create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-error-option-unknown-output.txt create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-extract-output.txt create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-help-output.txt create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-list-layers-output.txt create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-output.txt create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-unknown-command-output.txt create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar-release/pom.xml create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar-snapshot/pom.xml create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar/pom.xml create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar/src/main/java/org/test/SampleApplication.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/pom.xml create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/jar-release/pom.xml create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/jar-snapshot/pom.xml create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/pom.xml create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/war/pom.xml create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/war/src/main/java/org/test/SampleApplication.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/war/src/main/webapp/index.html diff --git a/settings.gradle b/settings.gradle index 8312af44273..4c07940fbdd 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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" diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 04f7a42dc9e..867434fe3dc 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -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", diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchive.java index 2da97a470c8..ec7d7099bcf 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchive.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchive.java @@ -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 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 getIncludeTools(); + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java index 330bc1aef1c..23cf1141756 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java @@ -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 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; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java index 7ed3f998c54..e7b542bebe3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java @@ -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 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java index d19f152f84b..302e9ceb1be 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java @@ -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 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java index 780944145dc..60bcebc0492 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java @@ -96,7 +96,7 @@ class BootZipCopyAction implements CopyAction { private final boolean includeDefaultLoader; - private final String layerToolsLocation; + private final String jarmodeToolsLocation; private final Spec 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 requiresUnpack, + boolean includeDefaultLoader, String jarmodeToolsLocation, Spec requiresUnpack, Spec exclusions, LaunchScriptConfiguration launchScript, Spec librarySpec, Function 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); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayeredSpec.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayeredSpec.java index b2f6842f21f..98898783a26 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayeredSpec.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayeredSpec.java @@ -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 getIncludeLayerTools(); /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java index 4019ffbf6af..f7b07b990c5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java @@ -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> 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> 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> 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> 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(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java index c5c78eb5a21..ebda757e698 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java @@ -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 the type of the concrete BootArchive implementation * @author Andy Wilkinson * @author Scott Frederick + * @author Moritz Halbritter */ abstract class AbstractBootArchiveTests { @@ -496,7 +497,7 @@ abstract class AbstractBootArchiveTests { 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 { List 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 expected = new ArrayList<>(); expected.add("- \"dependencies\":"); expected.add(" - \"" + this.libPath + "first-library.jar\""); @@ -584,7 +585,7 @@ abstract class AbstractBootArchiveTests { List 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 expected = new ArrayList<>(); expected.add("- \"my-deps\":"); expected.add(" - \"" + layerToolsJar + "\""); @@ -614,15 +615,32 @@ abstract class AbstractBootArchiveTests { @Test void whenArchiveIsLayeredThenLayerToolsAreAddedToTheJar() throws IOException { List 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 entryNames = getEntryNames(this.task.getArchiveFile().get().getAsFile()); + assertThat(entryNames).isNotEmpty().contains(this.libPath + JarModeLibrary.TOOLS.getName()); + } + + @Test + @SuppressWarnings("removal") void whenArchiveIsLayeredAndIncludeLayerToolsIsFalseThenLayerToolsAreNotAddedToTheJar() throws IOException { List 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 entryNames = getEntryNames(this.task.getArchiveFile().get().getAsFile()); + assertThat(entryNames).isNotEmpty().doesNotContain(this.libPath + JarModeLibrary.TOOLS.getName()); } protected File jarFile(String name) throws IOException { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java index d83e54ed165..cf14bba28c3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java @@ -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. "); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-customLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-customLayers.gradle index 3ba806e07e0..3103d3a44fd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-customLayers.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-customLayers.gradle @@ -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" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchive.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchive.gradle index cdbb87315a6..2e42641b008 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchive.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchive.gradle @@ -17,7 +17,5 @@ dependencies { } bootJar { - layered { - enabled = false - } + includeTools = false } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle index 941f20aa4c9..803b09d444e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle @@ -18,7 +18,5 @@ dependencies { } bootJar { - layered { - enabled = false - } + includeTools = false } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle index d035cf456ef..a412d8d01b8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle @@ -21,7 +21,5 @@ bootJar { } bootJar { - layered { - enabled = false - } + includeTools = false } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-implicitLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-implicitLayers.gradle index cc3aa6f0e8b..131fb7d18ce 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-implicitLayers.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-implicitLayers.gradle @@ -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" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-jarTypeFilteringIsApplied.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-jarTypeFilteringIsApplied.gradle index 970d90d116f..29e8f89004c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-jarTypeFilteringIsApplied.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-jarTypeFilteringIsApplied.gradle @@ -19,7 +19,5 @@ dependencies { } bootJar { - layered { - enabled = false - } + includeTools = false } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-layersWithCustomSourceSet.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-layersWithCustomSourceSet.gradle index 1f18bb3ebc2..f79bdd4414a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-layersWithCustomSourceSet.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-layersWithCustomSourceSet.gradle @@ -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" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleCustomLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleCustomLayers.gradle index f8127b2f3db..eae79719225 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleCustomLayers.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleCustomLayers.gradle @@ -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" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleImplicitLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleImplicitLayers.gradle index ba34cf4d1d6..16986fb0668 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleImplicitLayers.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleImplicitLayers.gradle @@ -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" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-notUpToDateWhenBuiltWithToolsAndThenWithoutTools.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-notUpToDateWhenBuiltWithToolsAndThenWithoutTools.gradle new file mode 100644 index 00000000000..583ab4fa370 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-notUpToDateWhenBuiltWithToolsAndThenWithoutTools.gradle @@ -0,0 +1,9 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClass = 'com.example.Application' + {includeTools} +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle index 7f4ca313065..27347023a82 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle @@ -18,7 +18,5 @@ dependencies { } bootJar { - layered { - enabled = false - } + includeTools = false } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle index 45041d1c190..d0dfd4e27f8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle @@ -21,7 +21,5 @@ bootJar { } bootJar { - layered { - enabled = false - } + includeTools = false } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-customLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-customLayers.gradle index 0c4bcdcaf06..a643858e224 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-customLayers.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-customLayers.gradle @@ -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" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle index 85aea3ecce2..b04983661b9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle @@ -18,7 +18,5 @@ dependencies { } bootWar { - layered { - enabled = false - } + includeTools = false } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle index 184c97603e2..3cbd4aa11f8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle @@ -21,7 +21,5 @@ bootWar { } bootWar { - layered { - enabled = false - } + includeTools = false } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-implicitLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-implicitLayers.gradle index 6fd9018c455..07cda46020a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-implicitLayers.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-implicitLayers.gradle @@ -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" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-jarTypeFilteringIsApplied.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-jarTypeFilteringIsApplied.gradle index 60e32af928b..a8c43a25016 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-jarTypeFilteringIsApplied.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-jarTypeFilteringIsApplied.gradle @@ -19,7 +19,5 @@ dependencies { } bootWar { - layered { - enabled = false - } + includeTools = false } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-layersWithCustomSourceSet.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-layersWithCustomSourceSet.gradle index 6892f814bbb..20daaf0f3aa 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-layersWithCustomSourceSet.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-layersWithCustomSourceSet.gradle @@ -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" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleCustomLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleCustomLayers.gradle index da574d0d153..c798b6bf190 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleCustomLayers.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleCustomLayers.gradle @@ -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" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleImplicitLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleImplicitLayers.gradle index cba40d5c3d3..8f467a84b68 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleImplicitLayers.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleImplicitLayers.gradle @@ -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" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenBuiltWithToolsAndThenWithoutTools.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenBuiltWithToolsAndThenWithoutTools.gradle new file mode 100644 index 00000000000..851db4027a0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenBuiltWithToolsAndThenWithoutTools.gradle @@ -0,0 +1,10 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' + id 'war' +} + +bootWar { + mainClass = 'com.example.Application' + {includeTools} +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle index f2d285e4081..00efac247c1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle @@ -18,7 +18,5 @@ dependencies { } bootWar { - layered { - enabled = false - } + includeTools = false } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle index de8e9d65217..5688972529c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle @@ -21,7 +21,5 @@ bootWar { } bootWar { - layered { - enabled = false - } + includeTools = false } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/ExtractCommand.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/ExtractCommand.java deleted file mode 100644 index c9f661facb1..00000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/ExtractCommand.java +++ /dev/null @@ -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("[...]")); - this.context = context; - this.layers = layers; - } - - @Override - protected void run(Map options, List 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); - } - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/LayerToolsJarMode.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/LayerToolsJarMode.java deleted file mode 100644 index 7ac5c2d8ae9..00000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/LayerToolsJarMode.java +++ /dev/null @@ -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 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 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 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 dequeOf(String... args) { - return new ArrayDeque<>(Arrays.asList(args)); - } - - static List getCommands(Context context) { - List commands = new ArrayList<>(); - commands.add(new ListCommand(context)); - commands.add(new ExtractCommand(context)); - return Collections.unmodifiableList(commands); - } - - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 850c9c453d3..00000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,3 +0,0 @@ -# Jar Modes -org.springframework.boot.loader.jarmode.JarMode=\ -org.springframework.boot.jarmode.layertools.LayerToolsJarMode \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/HelpCommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/HelpCommandTests.java deleted file mode 100644 index 9acb9c9c6ca..00000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/HelpCommandTests.java +++ /dev/null @@ -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); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/help-extract-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/help-extract-output.txt deleted file mode 100644 index 0d2f9e143cf..00000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/help-extract-output.txt +++ /dev/null @@ -1,7 +0,0 @@ -Extracts layers from the jar for image creation - -Usage: - java -Djarmode=layertools -jar test.jar extract [options] [...] - -Options: - --destination string The destination to extract files to diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/build.gradle similarity index 93% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/build.gradle rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/build.gradle index 96d50392499..7a0a9e911b2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/build.gradle @@ -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")) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/Command.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Command.java similarity index 76% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/Command.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Command.java index c6669b0d541..b0aee1d8ee8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/Command.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Command.java @@ -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 args) { + final void run(PrintStream out, Deque args) { List parameters = new ArrayList<>(); Map 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 options, List parameters); + abstract void run(PrintStream out, Map options, List 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 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 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,19 @@ 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); + } + + /** + * 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 + * @return a new {@link Option} instance + */ + static Option of(String name, String valueDescription, String description) { + return new Option(name, valueDescription, description, false); } /** @@ -332,10 +378,11 @@ abstract class Command { * @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 */ - 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, boolean optionalValue) { + return new Option(name, valueDescription, description, optionalValue); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/Context.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Context.java similarity index 97% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/Context.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Context.java index 4be9b946f4c..32f7976cddf 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/Context.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Context.java @@ -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; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java new file mode 100644 index 00000000000..dace9d19437 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java @@ -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 options, List 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 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 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 options) { + String runnerFilename = getRunnerFilename(options); + if (!options.containsKey(LAYERS_OPTION)) { + return new NoLayersFileResolver(destination, runnerFilename); + } + Layers layers = getLayers(); + Set layersToExtract = StringUtils.commaDelimitedListToSet(options.get(LAYERS_OPTION)); + return new LayersFileResolver(destination, layers, layersToExtract, runnerFilename); + } + + private File getWorkingDirectory(Map 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 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 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 layersToExtract; + + private final File directory; + + private final String runnerFilename; + + LayersFileResolver(File directory, Layers layers, Set 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); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractLayersCommand.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractLayersCommand.java new file mode 100644 index 00000000000..b2770b9fe41 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractLayersCommand.java @@ -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("[...]")); + 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 options, List parameters) { + Map 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()); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/HelpCommand.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/HelpCommand.java similarity index 58% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/HelpCommand.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/HelpCommand.java index 4f25c80dc25..17188c941ce 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/HelpCommand.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/HelpCommand.java @@ -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 commands; + private final String jarMode; + HelpCommand(Context context, List commands) { - super("help", "Help about any command", Options.none(), Parameters.of("[ commands, String jarMode) { + super("help", "Help about any command", Options.none(), Parameters.of("[]")); this.context = context; this.commands = commands; + this.jarMode = (jarMode != null) ? jarMode : "tools"; } @Override - protected void run(Map options, List parameters) { - run(System.out, parameters); + void run(PrintStream out, Map options, List parameters) { + run(out, parameters); } void run(PrintStream out, List 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; + } + 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; } - printUsageAndCommands(out); + 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 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 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(); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedJarStructure.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedJarStructure.java new file mode 100644 index 00000000000..8c026bbf409 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedJarStructure.java @@ -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 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 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 readIndexFile(String indexFile) { + String[] lines = Arrays.stream(indexFile.split("\n")) + .map((line) -> line.replace("\r", "")) + .filter(StringUtils::hasText) + .toArray(String[]::new); + List 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 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); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/IndexedLayers.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedLayers.java similarity index 83% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/IndexedLayers.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedLayers.java index 33e2ae66e0a..0469aeed836 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/IndexedLayers.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedLayers.java @@ -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> 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 { @@ -66,16 +71,17 @@ class IndexedLayers implements Layers { } @Override - public Iterator iterator() { - return this.layers.keySet().iterator(); + public String getApplicationLayerName() { + return getLayer(this.classesLocation); } @Override - public String getLayer(ZipEntry entry) { - return getLayer(entry.getName()); + public Iterator iterator() { + return this.layers.keySet().iterator(); } - private String getLayer(String name) { + @Override + public String getLayer(String name) { for (Map.Entry> 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; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/JarStructure.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/JarStructure.java new file mode 100644 index 00000000000..372437eacd6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/JarStructure.java @@ -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 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 + + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/LayerToolsJarMode.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/LayerToolsJarMode.java new file mode 100644 index 00000000000..851afd18019 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/LayerToolsJarMode.java @@ -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 getCommands(Context context) { + return List.of(new ListCommand(context), new ExtractLayersCommand(context)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/Layers.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Layers.java similarity index 63% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/Layers.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Layers.java index 181efb3cd84..05eb07ebdc9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/Layers.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Layers.java @@ -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 { * @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"); + } + + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/ListCommand.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ListCommand.java similarity index 60% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/ListCommand.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ListCommand.java index b08a7924944..fd5af54828b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/ListCommand.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ListCommand.java @@ -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 + boolean isDeprecated() { + return true; + } + + @Override + String getDeprecationMessage() { + return "Use '-Djarmode=tools list-layers' instead."; } @Override - protected void run(Map options, List parameters) { - printLayers(Layers.get(this.context), System.out); + void run(PrintStream out, Map options, List parameters) { + this.delegate.run(out, options, parameters); } void printLayers(Layers layers, PrintStream out) { - layers.forEach(out::println); + this.delegate.printLayers(out, layers); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ListLayersCommand.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ListLayersCommand.java new file mode 100644 index 00000000000..a2b122ec21e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ListLayersCommand.java @@ -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 options, List 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(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/MissingValueException.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/MissingValueException.java similarity index 89% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/MissingValueException.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/MissingValueException.java index f5fa8570e94..c4c08474899 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/MissingValueException.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/MissingValueException.java @@ -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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Runner.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Runner.java new file mode 100644 index 00000000000..71c0617cb75 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Runner.java @@ -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 commands = new ArrayList<>(); + + private final HelpCommand help; + + Runner(PrintStream out, Context context, List 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 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 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 dequeOf(String... args) { + return new ArrayDeque<>(Arrays.asList(args)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ToolsJarMode.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ToolsJarMode.java new file mode 100644 index 00000000000..3e70734d3f2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ToolsJarMode.java @@ -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 getCommands(Context context) { + return List.of(new ExtractCommand(context), new ListLayersCommand(context)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/UnknownOptionException.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/UnknownOptionException.java similarity index 89% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/UnknownOptionException.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/UnknownOptionException.java index 65d55413071..ba368ddef5a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/UnknownOptionException.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/UnknownOptionException.java @@ -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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/package-info.java similarity index 87% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/package-info.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/package-info.java index dff2cc43578..8686163fe26 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/package-info.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/package-info.java @@ -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; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000000..bc571075c5f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +# Jar Modes +org.springframework.boot.loader.jarmode.JarMode=\ +org.springframework.boot.jarmode.tools.LayerToolsJarMode,\ +org.springframework.boot.jarmode.tools.ToolsJarMode diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/AbstractTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/AbstractTests.java new file mode 100644 index 00000000000..1ffa0bfe603 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/AbstractTests.java @@ -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 getJarManifestAttributes(File jar) throws IOException { + assertThat(jar).exists(); + Manifest manifest = getJarManifest(jar); + Map result = new HashMap<>(); + manifest.getMainAttributes().forEach((key, value) -> result.put(key.toString(), value.toString())); + return result; + } + + List getJarEntryNames(File jar) throws IOException { + assertThat(jar).exists(); + try (JarFile jarFile = new JarFile(jar)) { + return jarFile.stream().map(ZipEntry::getName).toList(); + } + } + + List listFilenames() throws IOException { + return listFilenames(this.tempDir); + } + + List listFilenames(File directory) throws IOException { + try (Stream 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 create(Context context); + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/CommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/CommandTests.java similarity index 85% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/CommandTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/CommandTests.java index 4293af4fa2a..c16653dc2f0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/CommandTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/CommandTests.java @@ -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 options, List parameters) { + protected void run(PrintStream out, Map options, List parameters) { this.runOptions = options; this.runParameters = parameters; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ContextTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ContextTests.java similarity index 96% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ContextTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ContextTests.java index 44c6c5b0a0d..0d4c9c53e8c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ContextTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ContextTests.java @@ -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; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java new file mode 100644 index 00000000000..e6a50f36333 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java @@ -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 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 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 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 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 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 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 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 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 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 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 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 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"); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ExtractCommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractLayersCommandTests.java similarity index 89% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ExtractCommandTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractLayersCommandTests.java index 319e59c69af..bf77b76a054 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ExtractCommandTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractLayersCommandTests.java @@ -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"; + } + } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/HelpCommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/HelpCommandTests.java new file mode 100644 index 00000000000..80291888a1c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/HelpCommandTests.java @@ -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"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedJarStructureTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedJarStructureTests.java new file mode 100644 index 00000000000..d3f6387f8fb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedJarStructureTests.java @@ -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 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 getAttributes(Manifest manifest) { + Map 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))); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/IndexedLayersTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedLayersTests.java similarity index 88% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/IndexedLayersTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedLayersTests.java index 9b34b4cd304..9c543ca6b25 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/IndexedLayersTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedLayersTests.java @@ -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"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/LayerToolsJarModeTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/LayerToolsJarModeTests.java similarity index 83% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/LayerToolsJarModeTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/LayerToolsJarModeTests.java index a8c57607516..7abc558b25d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/LayerToolsJarModeTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/LayerToolsJarModeTests.java @@ -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 { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ListCommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ListCommandTests.java similarity index 85% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ListCommandTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ListCommandTests.java index 91856efecc2..c8b1dc53b24 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ListCommandTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ListCommandTests.java @@ -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); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ListLayersCommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ListLayersCommandTests.java new file mode 100644 index 00000000000..4b338a6b3e3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ListLayersCommandTests.java @@ -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); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/TestCommand.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/TestCommand.java new file mode 100644 index 00000000000..c8d5aba2c97 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/TestCommand.java @@ -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 options, List parameters) { + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/TestPrintStream.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/TestPrintStream.java similarity index 91% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/TestPrintStream.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/TestPrintStream.java index f3d1beb43f8..ce4bd67b8a4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/TestPrintStream.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/TestPrintStream.java @@ -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; /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ToolsJarModeTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ToolsJarModeTests.java new file mode 100644 index 00000000000..135a305dc44 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ToolsJarModeTests.java @@ -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); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/JarLauncher b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/JarLauncher new file mode 100644 index 00000000000..e69de29bb2d diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/application.properties b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/application.properties new file mode 100644 index 00000000000..15edec8e3d7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/application.properties @@ -0,0 +1 @@ +spring.application.name=test diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/classpath.idx b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/classpath.idx new file mode 100644 index 00000000000..dac08add8aa --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/classpath.idx @@ -0,0 +1,3 @@ +- "BOOT-INF/lib/dependency-1.jar" +- "BOOT-INF/lib/dependency-2.jar" +- "BOOT-INF/lib/dependency-3-SNAPSHOT.jar" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/dependency-1 b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/dependency-1 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/dependency-2 b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/dependency-2 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/dependency-3-SNAPSHOT b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/dependency-3-SNAPSHOT new file mode 100644 index 00000000000..e69de29bb2d diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/layers.idx b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/layers.idx new file mode 100644 index 00000000000..09c3d320f1f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/layers.idx @@ -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/" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/ExtractCommand-printErrorIfLayersAreNotEnabled.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/ExtractCommand-printErrorIfLayersAreNotEnabled.txt new file mode 100644 index 00000000000..994354551a6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/ExtractCommand-printErrorIfLayersAreNotEnabled.txt @@ -0,0 +1,2 @@ +Error: Layers are not enabled + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/help-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/help-output.txt new file mode 100644 index 00000000000..69c78f5a375 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/help-output.txt @@ -0,0 +1,6 @@ +Usage: + java -Djarmode=tools -jar test.jar + +Available commands: + test Description of test + help Help about any command diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/help-test-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/help-test-output.txt new file mode 100644 index 00000000000..eb7a19d113f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/help-test-output.txt @@ -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 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-command-unknown-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-command-unknown-output.txt similarity index 92% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-command-unknown-output.txt rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-command-unknown-output.txt index 20a2ca2098d..425ac1025e3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-command-unknown-output.txt +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-command-unknown-output.txt @@ -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 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-option-missing-value-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-missing-value-output.txt similarity index 73% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-option-missing-value-output.txt rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-missing-value-output.txt index 6a5034cedd7..18e87069ce0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-option-missing-value-output.txt +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-missing-value-output.txt @@ -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 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-option-unknown-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-unknown-output.txt similarity index 72% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-option-unknown-output.txt rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-unknown-output.txt index a207b3b20a2..3b832dd642e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-option-unknown-output.txt +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-unknown-output.txt @@ -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 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/help-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-help-output.txt similarity index 91% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/help-output.txt rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-help-output.txt index 47d5f4b3ba9..daea473a9b6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/help-output.txt +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-help-output.txt @@ -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 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-list-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-list-output.txt new file mode 100644 index 00000000000..03f7f88556f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-list-output.txt @@ -0,0 +1,5 @@ +Warning: This command is deprecated. Use '-Djarmode=tools list-layers' instead. + +0001 +0002 +0003 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-layers-output-layers-disabled.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-layers-output-layers-disabled.txt new file mode 100644 index 00000000000..994354551a6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-layers-output-layers-disabled.txt @@ -0,0 +1,2 @@ +Error: Layers are not enabled + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-layers-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-layers-output.txt new file mode 100644 index 00000000000..8a1e6d4dcc6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-layers-output.txt @@ -0,0 +1,4 @@ +dependencies +spring-boot-loader +snapshot-dependencies +application diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/list-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-output-without-deprecation.txt similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/list-output.txt rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-output-without-deprecation.txt diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/test-layers.idx b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/test-layers.idx similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/test-layers.idx rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/test-layers.idx diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/test-manifest.MF b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/test-manifest.MF similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/test-manifest.MF rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/test-manifest.MF diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/test-war-layers.idx b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/test-war-layers.idx similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/test-war-layers.idx rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/test-war-layers.idx diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/test-war-manifest.MF b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/test-war-manifest.MF similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/test-war-manifest.MF rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/test-war-manifest.MF diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-error-command-unknown-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-error-command-unknown-output.txt new file mode 100644 index 00000000000..069368999c6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-error-command-unknown-output.txt @@ -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 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-error-option-missing-value-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-error-option-missing-value-output.txt new file mode 100644 index 00000000000..19dd4f72ada --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-error-option-missing-value-output.txt @@ -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 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-error-option-unknown-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-error-option-unknown-output.txt new file mode 100644 index 00000000000..9ea8f884ae4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-error-option-unknown-output.txt @@ -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 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-extract-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-extract-output.txt new file mode 100644 index 00000000000..514fe294155 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-extract-output.txt @@ -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 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-help-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-help-output.txt new file mode 100644 index 00000000000..8b5187e7679 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-help-output.txt @@ -0,0 +1,4 @@ +Help about any command + +Usage: + java -Djarmode=tools -jar test.jar help [] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-list-layers-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-list-layers-output.txt new file mode 100644 index 00000000000..d97934afca7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-list-layers-output.txt @@ -0,0 +1,4 @@ +List layers from the jar that can be extracted + +Usage: + java -Djarmode=tools -jar test.jar list-layers diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-output.txt new file mode 100644 index 00000000000..8d29d30374b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-output.txt @@ -0,0 +1,7 @@ +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 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-unknown-command-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-unknown-command-output.txt new file mode 100644 index 00000000000..f31247aea6c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-unknown-command-output.txt @@ -0,0 +1,9 @@ +Error: Unknown command "unknown-command" + +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 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle index f7968f659d5..c2e3330517d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle @@ -43,7 +43,7 @@ dependencies { loader(project(":spring-boot-project:spring-boot-tools:spring-boot-loader")) loaderClassic(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-classic")) - jarmode(project(":spring-boot-project:spring-boot-tools:spring-boot-jarmode-layertools")) + jarmode(project(":spring-boot-project:spring-boot-tools:spring-boot-jarmode-tools")) testImplementation("org.assertj:assertj-core") testImplementation("org.junit.jupiter:junit-jupiter") @@ -81,18 +81,18 @@ task reproducibleLoaderClassicJar(type: Jar) { destinationDirectory = file("${generatedResources}/META-INF/loader") } -task layerToolsJar(type: Sync) { +task toolsJar(type: Sync) { dependsOn configurations.jarmode from { file(configurations.jarmode.incoming.files.singleFile) } - rename({ "spring-boot-jarmode-layertools.jar" }) + rename({ "spring-boot-jarmode-tools.jar" }) into(file("${generatedResources}/META-INF/jarmode")) } sourceSets { main { - output.dir(generatedResources, builtBy: [layerToolsJar, reproducibleLoaderJar, reproducibleLoaderClassicJar]) + output.dir(generatedResources, builtBy: [toolsJar, reproducibleLoaderJar, reproducibleLoaderClassicJar]) } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarModeLibrary.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarModeLibrary.java index 49508ab5187..ef6199f6713 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarModeLibrary.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarModeLibrary.java @@ -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. @@ -35,7 +35,7 @@ public class JarModeLibrary extends Library { /** * {@link JarModeLibrary} for layer tools. */ - public static final JarModeLibrary LAYER_TOOLS = new JarModeLibrary("spring-boot-jarmode-layertools"); + public static final JarModeLibrary TOOLS = new JarModeLibrary("spring-boot-jarmode-tools"); JarModeLibrary(String artifactId) { this(createCoordinates(artifactId)); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java index 8a66fa186aa..0fa33c64de6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java @@ -511,8 +511,8 @@ public abstract class Packager { addLibrary(library); } }); - if (isLayered() && Packager.this.includeRelevantJarModeJars) { - addLibrary(JarModeLibrary.LAYER_TOOLS); + if (Packager.this.includeRelevantJarModeJars) { + addLibrary(JarModeLibrary.TOOLS); } this.unpackHandler = new PackagedLibrariesUnpackHandler(); this.libraryLookup = this::lookup; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java index 7dce78edafc..7d1ff71ce25 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java @@ -226,6 +226,7 @@ abstract class AbstractPackagerTests

{ this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); File file = this.testJarFile.getFile(); P packager = createPackager(file); + packager.setIncludeRelevantJarModeJars(false); execute(packager, (callback) -> { callback.library(newLibrary(libJarFile1, LibraryScope.COMPILE, false)); callback.library(newLibrary(libJarFile2, LibraryScope.COMPILE, false)); @@ -299,7 +300,7 @@ abstract class AbstractPackagerTests

{ assertThat(hasPackagedEntry("BOOT-INF/classpath.idx")).isTrue(); String classpathIndex = getPackagedEntryContent("BOOT-INF/classpath.idx"); assertThat(Arrays.asList(classpathIndex.split("\\n"))) - .containsExactly("- \"BOOT-INF/lib/spring-boot-jarmode-layertools.jar\""); + .containsExactly("- \"BOOT-INF/lib/spring-boot-jarmode-tools.jar\""); assertThat(hasPackagedEntry("BOOT-INF/layers.idx")).isTrue(); String layersIndex = getPackagedEntryContent("BOOT-INF/layers.idx"); List expectedLayers = new ArrayList<>(); @@ -606,6 +607,7 @@ abstract class AbstractPackagerTests

{ this.testJarFile.addClass("WEB-INF/classes/com/example/Application.class", ClassWithMainMethod.class); this.testJarFile.addFile("WEB-INF/lib/" + webLibrary.getName(), webLibrary); P packager = createPackager(this.testJarFile.getFile("war")); + packager.setIncludeRelevantJarModeJars(false); packager.setLayout(new Layouts.War()); execute(packager, (callback) -> { callback.library(newLibrary(webLibrary, LibraryScope.COMPILE, false, false)); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java index d96b959fe64..d15caedf313 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java @@ -40,6 +40,7 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Andy Wilkinson * @author Madhura Bhave * @author Scott Frederick + * @author Moritz Halbritter */ @ExtendWith(MavenBuildExtension.class) class JarIntegrationTests extends AbstractArchiveIntegrationTests { @@ -337,8 +338,7 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests { assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/classes/") .hasEntryWithNameStartingWith("BOOT-INF/lib/jar-release") .hasEntryWithNameStartingWith("BOOT-INF/lib/jar-snapshot") - .hasEntryWithNameStartingWith( - "BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getCoordinates().getArtifactId()); + .hasEntryWithNameStartingWith("BOOT-INF/lib/" + JarModeLibrary.TOOLS.getCoordinates().getArtifactId()); try (JarFile jarFile = new JarFile(repackaged)) { Map> layerIndex = readLayerIndex(jarFile); assertThat(layerIndex.keySet()).containsExactly("dependencies", "spring-boot-loader", @@ -361,8 +361,8 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests { assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/classes/") .hasEntryWithNameStartingWith("BOOT-INF/lib/jar-release") .hasEntryWithNameStartingWith("BOOT-INF/lib/jar-snapshot") - .doesNotHaveEntryWithName("BOOT-INF/layers.idx") - .doesNotHaveEntryWithNameStartingWith("BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName()); + .hasEntryWithNameStartingWith("BOOT-INF/lib/" + JarModeLibrary.TOOLS.getCoordinates().getArtifactId()) + .doesNotHaveEntryWithName("BOOT-INF/layers.idx"); }); } @@ -374,7 +374,20 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests { .hasEntryWithNameStartingWith("BOOT-INF/lib/jar-release") .hasEntryWithNameStartingWith("BOOT-INF/lib/jar-snapshot") .hasEntryWithNameStartingWith("BOOT-INF/layers.idx") - .doesNotHaveEntryWithNameStartingWith("BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName()); + .doesNotHaveEntryWithNameStartingWith( + "BOOT-INF/lib/" + JarModeLibrary.TOOLS.getCoordinates().getArtifactId()); + }); + } + + @TestTemplate + void whenJarIsRepackagedWithToolsExclude(MavenBuild mavenBuild) { + mavenBuild.project("jar-no-tools").execute((project) -> { + File repackaged = new File(project, "jar/target/jar-no-tools-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/classes/") + .hasEntryWithNameStartingWith("BOOT-INF/lib/jar-release") + .hasEntryWithNameStartingWith("BOOT-INF/lib/jar-snapshot") + .doesNotHaveEntryWithNameStartingWith( + "BOOT-INF/lib/" + JarModeLibrary.TOOLS.getCoordinates().getArtifactId()); }); } @@ -451,7 +464,8 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests { File repackaged = new File(project, "target/jar-output-timestamp-0.0.1.BUILD-SNAPSHOT.jar"); List sortedLibs = Arrays.asList("BOOT-INF/lib/jakarta.servlet-api", "BOOT-INF/lib/micrometer-commons", "BOOT-INF/lib/micrometer-observation", "BOOT-INF/lib/spring-aop", - "BOOT-INF/lib/spring-beans", "BOOT-INF/lib/spring-boot-jarmode-layertools", + "BOOT-INF/lib/spring-beans", + "BOOT-INF/lib/" + JarModeLibrary.TOOLS.getCoordinates().getArtifactId(), "BOOT-INF/lib/spring-context", "BOOT-INF/lib/spring-core", "BOOT-INF/lib/spring-expression", "BOOT-INF/lib/spring-jcl"); assertThat(jar(repackaged)).entryNamesInPath("BOOT-INF/lib/") diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/WarIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/WarIntegrationTests.java index 56a266b5bfd..d9038197cbf 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/WarIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/WarIntegrationTests.java @@ -40,6 +40,7 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Andy Wilkinson * @author Scott Frederick + * @author Moritz Halbritter */ @ExtendWith(MavenBuildExtension.class) class WarIntegrationTests extends AbstractArchiveIntegrationTests { @@ -127,7 +128,7 @@ class WarIntegrationTests extends AbstractArchiveIntegrationTests { "WEB-INF/lib/spring-expression", "WEB-INF/lib/spring-jcl", // these libraries are contributed by Spring Boot repackaging, and // sorted separately - "WEB-INF/lib/spring-boot-jarmode-layertools"); + "WEB-INF/lib/" + JarModeLibrary.TOOLS.getCoordinates().getArtifactId()); assertThat(jar(repackaged)).entryNamesInPath("WEB-INF/lib/") .zipSatisfy(sortedLibs, (String jarLib, String expectedLib) -> assertThat(jarLib).startsWith(expectedLib)); @@ -150,8 +151,7 @@ class WarIntegrationTests extends AbstractArchiveIntegrationTests { assertThat(jar(repackaged)).hasEntryWithNameStartingWith("WEB-INF/classes/") .hasEntryWithNameStartingWith("WEB-INF/lib/jar-release") .hasEntryWithNameStartingWith("WEB-INF/lib/jar-snapshot") - .hasEntryWithNameStartingWith( - "WEB-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getCoordinates().getArtifactId()); + .hasEntryWithNameStartingWith("WEB-INF/lib/" + JarModeLibrary.TOOLS.getCoordinates().getArtifactId()); try (JarFile jarFile = new JarFile(repackaged)) { Map> layerIndex = readLayerIndex(jarFile); assertThat(layerIndex.keySet()).containsExactly("dependencies", "spring-boot-loader", @@ -179,8 +179,8 @@ class WarIntegrationTests extends AbstractArchiveIntegrationTests { assertThat(jar(repackaged)).hasEntryWithNameStartingWith("WEB-INF/classes/") .hasEntryWithNameStartingWith("WEB-INF/lib/jar-release") .hasEntryWithNameStartingWith("WEB-INF/lib/jar-snapshot") - .doesNotHaveEntryWithName("WEB-INF/layers.idx") - .doesNotHaveEntryWithNameStartingWith("WEB-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName()); + .hasEntryWithNameStartingWith("WEB-INF/lib/" + JarModeLibrary.TOOLS.getCoordinates().getArtifactId()) + .doesNotHaveEntryWithName("WEB-INF/layers.idx"); }); } @@ -192,7 +192,20 @@ class WarIntegrationTests extends AbstractArchiveIntegrationTests { .hasEntryWithNameStartingWith("WEB-INF/lib/jar-release") .hasEntryWithNameStartingWith("WEB-INF/lib/jar-snapshot") .hasEntryWithNameStartingWith("WEB-INF/layers.idx") - .doesNotHaveEntryWithNameStartingWith("WEB-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName()); + .doesNotHaveEntryWithNameStartingWith( + "WEB-INF/lib/" + JarModeLibrary.TOOLS.getCoordinates().getArtifactId()); + }); + } + + @TestTemplate + void whenWarIsRepackagedWithToolsExclude(MavenBuild mavenBuild) { + mavenBuild.project("war-no-tools").execute((project) -> { + File repackaged = new File(project, "war/target/war-no-tools-0.0.1.BUILD-SNAPSHOT.war"); + assertThat(jar(repackaged)).hasEntryWithNameStartingWith("WEB-INF/classes/") + .hasEntryWithNameStartingWith("WEB-INF/lib/jar-release") + .hasEntryWithNameStartingWith("WEB-INF/lib/jar-snapshot") + .doesNotHaveEntryWithNameStartingWith( + "WEB-INF/lib/" + JarModeLibrary.TOOLS.getCoordinates().getArtifactId()); }); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar-release/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar-release/pom.xml new file mode 100644 index 00000000000..a06fe545f18 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar-release/pom.xml @@ -0,0 +1,11 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-release + 0.0.1.RELEASE + jar + jar + Release Jar dependency + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar-snapshot/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar-snapshot/pom.xml new file mode 100644 index 00000000000..ab31e719baf --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar-snapshot/pom.xml @@ -0,0 +1,11 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-snapshot + 0.0.1.BUILD-SNAPSHOT + jar + jar + Snapshot Jar dependency + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar/pom.xml new file mode 100644 index 00000000000..beb9e4c68e1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-no-tools + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + false + + + + + + + + + org.springframework.boot.maven.it + jar-snapshot + 0.0.1.BUILD-SNAPSHOT + + + org.springframework.boot.maven.it + jar-release + 0.0.1.RELEASE + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar/src/main/java/org/test/SampleApplication.java new file mode 100644 index 00000000000..1277cdbc5a6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * 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.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/pom.xml new file mode 100644 index 00000000000..fdd98953811 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + org.springframework.boot.maven.it + aggregator + 0.0.1.BUILD-SNAPSHOT + pom + + UTF-8 + @java.version@ + @java.version@ + + + jar-snapshot + jar-release + jar + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/jar-release/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/jar-release/pom.xml new file mode 100644 index 00000000000..a06fe545f18 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/jar-release/pom.xml @@ -0,0 +1,11 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-release + 0.0.1.RELEASE + jar + jar + Release Jar dependency + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/jar-snapshot/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/jar-snapshot/pom.xml new file mode 100644 index 00000000000..ab31e719baf --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/jar-snapshot/pom.xml @@ -0,0 +1,11 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-snapshot + 0.0.1.BUILD-SNAPSHOT + jar + jar + Snapshot Jar dependency + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/pom.xml new file mode 100644 index 00000000000..60503bdf68f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + org.springframework.boot.maven.it + aggregator + 0.0.1.BUILD-SNAPSHOT + pom + + UTF-8 + @java.version@ + @java.version@ + + + jar-snapshot + jar-release + war + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/war/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/war/pom.xml new file mode 100644 index 00000000000..fc478ed1111 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/war/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + org.springframework.boot.maven.it + aggregator + 0.0.1.BUILD-SNAPSHOT + + war-no-tools + war + war + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + false + + + + + + org.apache.maven.plugins + maven-war-plugin + @maven-war-plugin.version@ + + + + Foo + + + + + + + + + org.springframework + spring-context + @spring-framework.version@ + + + jakarta.servlet + jakarta.servlet-api + @jakarta-servlet.version@ + provided + + + org.springframework.boot.maven.it + jar-snapshot + 0.0.1.BUILD-SNAPSHOT + + + org.springframework.boot.maven.it + jar-release + 0.0.1.RELEASE + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/war/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/war/src/main/java/org/test/SampleApplication.java new file mode 100644 index 00000000000..1277cdbc5a6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/war/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * 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.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/war/src/main/webapp/index.html b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/war/src/main/webapp/index.html new file mode 100644 index 00000000000..18ecdcb795c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/war/src/main/webapp/index.html @@ -0,0 +1 @@ + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java index 28d55d213a1..3fbfe48e710 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java @@ -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. @@ -56,6 +56,7 @@ import org.springframework.boot.loader.tools.layer.CustomLayers; * * @author Phillip Webb * @author Scott Frederick + * @author Moritz Halbritter * @since 2.3.0 */ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo { @@ -112,13 +113,20 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo @Parameter(defaultValue = "false") public boolean includeSystemScope; + /** + * Include JAR tools. + * @since 3.3.0 + */ + @Parameter(defaultValue = "true") + public boolean includeTools = true; + /** * Layer configuration with options to disable layer creation, exclude layer tools * jar, and provide a custom layers configuration file. * @since 2.3.0 */ @Parameter - private Layers layers; + private Layers layers = new Layers(); /** * Return the type of archive that should be packaged by this MOJO. @@ -164,17 +172,22 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo getLog().info("Layout: " + layout); packager.setLayout(layout.layout()); } - if (this.layers == null) { - packager.setLayers(IMPLICIT_LAYERS); - } - else if (this.layers.isEnabled()) { + if (this.layers.isEnabled()) { packager.setLayers((this.layers.getConfiguration() != null) ? getCustomLayers(this.layers.getConfiguration()) : IMPLICIT_LAYERS); - packager.setIncludeRelevantJarModeJars(this.layers.isIncludeLayerTools()); } + packager.setIncludeRelevantJarModeJars(getIncludeRelevantJarModeJars()); return packager; } + @SuppressWarnings("removal") + private boolean getIncludeRelevantJarModeJars() { + if (!this.includeTools) { + return false; + } + return this.layers.isIncludeLayerTools(); + } + private CustomLayers getCustomLayers(File configuration) { try { Document document = getDocumentIfAvailable(configuration); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layers.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layers.java index ce981b4248f..cb4d6a235d8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layers.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layers.java @@ -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. @@ -28,6 +28,7 @@ public class Layers { private boolean enabled = true; + @Deprecated(since = "3.3.0", forRemoval = true) private boolean includeLayerTools = true; private File configuration; @@ -43,7 +44,9 @@ public class Layers { /** * Whether to include the layer tools jar. * @return true if layer tools should be included + * @deprecated since 3.3.0 for removal in 3.5.0 in favor of {@code includeTools}. */ + @Deprecated(since = "3.3.0", forRemoval = true) public boolean isIncludeLayerTools() { return this.includeLayerTools; }