diff --git a/spring-boot-project/spring-boot-parent/build.gradle b/spring-boot-project/spring-boot-parent/build.gradle index 25d3f8b38a3..a7ddd8c77a3 100644 --- a/spring-boot-project/spring-boot-parent/build.gradle +++ b/spring-boot-project/spring-boot-parent/build.gradle @@ -27,7 +27,7 @@ bom { ] } } - library("Commons Compress", "1.20") { + library("Commons Compress", "1.21") { group("org.apache.commons") { modules = [ "commons-compress" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/ZipFileTarArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/ZipFileTarArchive.java index 0bab02e1f8c..dcbb3229b92 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/ZipFileTarArchive.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/ZipFileTarArchive.java @@ -74,13 +74,12 @@ public class ZipFileTarArchive implements TarArchive { tar.finish(); } - private void assertArchiveHasEntries(File jarFile) { - try (ZipFile zipFile = new ZipFile(jarFile)) { - Assert.state(zipFile.getEntries().hasMoreElements(), () -> "File '" + jarFile - + "' is not compatible with buildpacks; ensure jar file is valid and launch script is not enabled"); + private void assertArchiveHasEntries(File file) { + try (ZipFile zipFile = new ZipFile(file)) { + Assert.state(zipFile.getEntries().hasMoreElements(), () -> "Archive file '" + file + "' is not valid"); } catch (IOException ex) { - throw new IllegalStateException("File '" + jarFile + "' is not readable", ex); + throw new IllegalStateException("File '" + file + "' is not readable", ex); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc index ce2ca57a4b0..65f0f4cc6db 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -8,9 +8,6 @@ See the {buildpacks-reference}/reference/spec/platform-api/#users[CNB specificat The task is automatically created when the `java` or `war` plugin is applied and is an instance of {boot-build-image-javadoc}[`BootBuildImage`]. -NOTE: The `bootBuildImage` task can not be used with a <> that includes a launch script. -Disable launch script configuration in the `bootJar` task when building a jar file that is intended to be used with `bootBuildImage`. - [[build-image.docker-daemon]] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging.adoc index 4f4cc4856b6..45771cd51d1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging.adoc @@ -206,7 +206,7 @@ On Unix-like platforms, this launch script allows the archive to be run directly NOTE: Currently, some tools do not accept this format so you may not always be able to use this technique. For example, `jar -xf` may silently fail to extract a jar or war that has been made fully-executable. -It is recommended that you only enable this option if you intend to execute it directly, rather than running it with `java -jar`, deploying it to a servlet container, or including it in an OCI image. +It is recommended that you only enable this option if you intend to execute it directly, rather than running it with `java -jar` or deploying it to a servlet container. To use this feature, the inclusion of the launch script must be enabled: 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 4453843c64d..39e512ec6dc 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 @@ -135,8 +135,8 @@ class BootZipCopyAction implements CopyAction { } private void writeArchive(CopyActionProcessingStream copyActions, OutputStream output) throws IOException { - writeLaunchScriptIfNecessary(output); ZipArchiveOutputStream zipOutput = new ZipArchiveOutputStream(output); + writeLaunchScriptIfNecessary(zipOutput); try { setEncodingIfNecessary(zipOutput); Processor processor = new Processor(zipOutput); @@ -148,15 +148,14 @@ class BootZipCopyAction implements CopyAction { } } - private void writeLaunchScriptIfNecessary(OutputStream outputStream) { + private void writeLaunchScriptIfNecessary(ZipArchiveOutputStream outputStream) { if (this.launchScript == null) { return; } try { File file = this.launchScript.getScript(); Map properties = this.launchScript.getProperties(); - outputStream.write(new DefaultLaunchScript(file, properties).toByteArray()); - outputStream.flush(); + outputStream.writePreamble(new DefaultLaunchScript(file, properties).toByteArray()); this.output.setExecutable(true); } catch (IOException ex) { 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 96a14088a19..fb23adb6bde 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 @@ -279,11 +279,14 @@ abstract class AbstractBootArchiveTests { properties.put("initInfoProvides", this.task.getArchiveBaseName().get()); properties.put("initInfoShortDescription", this.project.getDescription()); properties.put("initInfoDescription", this.project.getDescription()); - assertThat(Files.readAllBytes(this.task.getArchiveFile().get().getAsFile().toPath())) + File archiveFile = this.task.getArchiveFile().get().getAsFile(); + assertThat(Files.readAllBytes(archiveFile.toPath())) .startsWith(new DefaultLaunchScript(null, properties).toByteArray()); + try (ZipFile zipFile = new ZipFile(archiveFile)) { + assertThat(zipFile.getEntries().hasMoreElements()).isTrue(); + } try { - Set permissions = Files - .getPosixFilePermissions(this.task.getArchiveFile().get().getAsFile().toPath()); + Set permissions = Files.getPosixFilePermissions(archiveFile.toPath()); assertThat(permissions).contains(PosixFilePermission.OWNER_EXECUTE); } catch (UnsupportedOperationException ex) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java index 90dc7d3c617..a8517e11ef6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java @@ -235,12 +235,16 @@ class BootBuildImageIntegrationTests { } @TestTemplate - void failsWithLaunchScript() throws IOException { + void buildsImageWithLaunchScript() throws IOException { writeMainClass(); writeLongNameResource(); - BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage"); - assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.FAILED); - assertThat(result.getOutput()).contains("not compatible with buildpacks"); + BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT"); + String projectName = this.gradleBuild.getProjectDir().getName(); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("docker.io/library/" + projectName); + assertThat(result.getOutput()).contains("---> Test Info buildpack building"); + assertThat(result.getOutput()).contains("---> Test Info buildpack done"); + removeImage(projectName); } @TestTemplate diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWithLaunchScript.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithLaunchScript.gradle similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWithLaunchScript.gradle rename to spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithLaunchScript.gradle diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarWriter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarWriter.java index 56bf80f95b2..c45ee567aca 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarWriter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -20,12 +20,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.nio.file.attribute.FileTime; -import java.nio.file.attribute.PosixFilePermission; -import java.util.HashSet; -import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipException; @@ -39,6 +34,7 @@ import org.apache.commons.compress.archivers.jar.JarArchiveOutputStream; * @author Phillip Webb * @author Andy Wilkinson * @author Madhura Bhave + * @author Scott Frederick * @since 1.0.0 */ public class JarWriter extends AbstractJarWriter implements AutoCloseable { @@ -80,28 +76,15 @@ public class JarWriter extends AbstractJarWriter implements AutoCloseable { */ public JarWriter(File file, LaunchScript launchScript, FileTime lastModifiedTime) throws FileNotFoundException, IOException { - FileOutputStream fileOutputStream = new FileOutputStream(file); + this.jarOutputStream = new JarArchiveOutputStream(new FileOutputStream(file)); if (launchScript != null) { - fileOutputStream.write(launchScript.toByteArray()); - setExecutableFilePermission(file); + this.jarOutputStream.writePreamble(launchScript.toByteArray()); + file.setExecutable(true); } - this.jarOutputStream = new JarArchiveOutputStream(fileOutputStream); this.jarOutputStream.setEncoding("UTF-8"); this.lastModifiedTime = lastModifiedTime; } - private void setExecutableFilePermission(File file) { - try { - Path path = file.toPath(); - Set permissions = new HashSet<>(Files.getPosixFilePermissions(path)); - permissions.add(PosixFilePermission.OWNER_EXECUTE); - Files.setPosixFilePermissions(path, permissions); - } - catch (Throwable ex) { - // Ignore and continue creating the jar - } - } - @Override protected void writeToArchive(ZipEntry entry, EntryWriter entryWriter) throws IOException { JarArchiveEntry jarEntry = asJarArchiveEntry(entry); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java index 1367231dcb4..89e7f3a513c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -49,6 +49,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException * @author Phillip Webb * @author Andy Wilkinson * @author Madhura Bhave + * @author Scott Frederick */ class RepackagerTests extends AbstractPackagerTests { @@ -159,6 +160,9 @@ class RepackagerTests extends AbstractPackagerTests { assertThat(new String(bytes)).startsWith("ABC"); assertThat(hasLauncherClasses(source)).isFalse(); assertThat(hasLauncherClasses(this.destination)).isTrue(); + try (ZipFile zipFile = new ZipFile(this.destination)) { + assertThat(zipFile.getEntries().hasMoreElements()).isTrue(); + } try { assertThat(Files.getPosixFilePermissions(this.destination.toPath())) .contains(PosixFilePermission.OWNER_EXECUTE); diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/intTest/java/org/springframework/boot/launchscript/AbstractLaunchScriptIntegrationTests.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/intTest/java/org/springframework/boot/launchscript/AbstractLaunchScriptIntegrationTests.java index 64fce246ad4..eee11367474 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/intTest/java/org/springframework/boot/launchscript/AbstractLaunchScriptIntegrationTests.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/intTest/java/org/springframework/boot/launchscript/AbstractLaunchScriptIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -106,7 +106,8 @@ abstract class AbstractLaunchScriptIntegrationTests { withCopyFileToContainer( MountableFile.forHostPath("src/intTest/resources/scripts/" + scriptsDir + testScript), "/" + testScript); - withCommand("/bin/bash", "-c", "chmod +x " + testScript + " && ./" + testScript); + withCommand("/bin/bash", "-c", + "chown root:root *.sh && chown root:root *.jar && chmod +x " + testScript + " && ./" + testScript); withStartupCheckStrategy(new OneShotStartupCheckStrategy().withTimeout(Duration.ofMinutes(5))); }