Skip to content

Commit 9f001ef

Browse files
Adjust fat jar central directory to account for launch script
An upgrade to Apache Commons Compress allows the build plugins to write the launch script to the fat jar as a proper preamble, making the file compatible with more jar and zip tooling. Fixes gh-22336
1 parent b5ef5a2 commit 9f001ef

File tree

11 files changed

+36
-46
lines changed

11 files changed

+36
-46
lines changed

spring-boot-project/spring-boot-parent/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ bom {
2727
]
2828
}
2929
}
30-
library("Commons Compress", "1.20") {
30+
library("Commons Compress", "1.21") {
3131
group("org.apache.commons") {
3232
modules = [
3333
"commons-compress"

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/ZipFileTarArchive.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,12 @@ public void writeTo(OutputStream outputStream) throws IOException {
7474
tar.finish();
7575
}
7676

77-
private void assertArchiveHasEntries(File jarFile) {
78-
try (ZipFile zipFile = new ZipFile(jarFile)) {
79-
Assert.state(zipFile.getEntries().hasMoreElements(), () -> "File '" + jarFile
80-
+ "' is not compatible with buildpacks; ensure jar file is valid and launch script is not enabled");
77+
private void assertArchiveHasEntries(File file) {
78+
try (ZipFile zipFile = new ZipFile(file)) {
79+
Assert.state(zipFile.getEntries().hasMoreElements(), () -> "Archive file '" + file + "' is not valid");
8180
}
8281
catch (IOException ex) {
83-
throw new IllegalStateException("File '" + jarFile + "' is not readable", ex);
82+
throw new IllegalStateException("File '" + file + "' is not readable", ex);
8483
}
8584
}
8685

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@ See the {buildpacks-reference}/reference/spec/platform-api/#users[CNB specificat
88

99
The task is automatically created when the `java` or `war` plugin is applied and is an instance of {boot-build-image-javadoc}[`BootBuildImage`].
1010

11-
NOTE: The `bootBuildImage` task can not be used with a <<packaging-executable.configuring.launch-script, fully executable Spring Boot archive>> that includes a launch script.
12-
Disable launch script configuration in the `bootJar` task when building a jar file that is intended to be used with `bootBuildImage`.
13-
1411

1512

1613
[[build-image.docker-daemon]]

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ On Unix-like platforms, this launch script allows the archive to be run directly
206206

207207
NOTE: Currently, some tools do not accept this format so you may not always be able to use this technique.
208208
For example, `jar -xf` may silently fail to extract a jar or war that has been made fully-executable.
209-
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.
209+
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.
210210

211211
To use this feature, the inclusion of the launch script must be enabled:
212212

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,8 @@ private void writeArchive(CopyActionProcessingStream copyActions) throws IOExcep
135135
}
136136

137137
private void writeArchive(CopyActionProcessingStream copyActions, OutputStream output) throws IOException {
138-
writeLaunchScriptIfNecessary(output);
139138
ZipArchiveOutputStream zipOutput = new ZipArchiveOutputStream(output);
139+
writeLaunchScriptIfNecessary(zipOutput);
140140
try {
141141
setEncodingIfNecessary(zipOutput);
142142
Processor processor = new Processor(zipOutput);
@@ -148,15 +148,14 @@ private void writeArchive(CopyActionProcessingStream copyActions, OutputStream o
148148
}
149149
}
150150

151-
private void writeLaunchScriptIfNecessary(OutputStream outputStream) {
151+
private void writeLaunchScriptIfNecessary(ZipArchiveOutputStream outputStream) {
152152
if (this.launchScript == null) {
153153
return;
154154
}
155155
try {
156156
File file = this.launchScript.getScript();
157157
Map<String, String> properties = this.launchScript.getProperties();
158-
outputStream.write(new DefaultLaunchScript(file, properties).toByteArray());
159-
outputStream.flush();
158+
outputStream.writePreamble(new DefaultLaunchScript(file, properties).toByteArray());
160159
this.output.setExecutable(true);
161160
}
162161
catch (IOException ex) {

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -279,11 +279,14 @@ void launchScriptCanBePrepended() throws IOException {
279279
properties.put("initInfoProvides", this.task.getArchiveBaseName().get());
280280
properties.put("initInfoShortDescription", this.project.getDescription());
281281
properties.put("initInfoDescription", this.project.getDescription());
282-
assertThat(Files.readAllBytes(this.task.getArchiveFile().get().getAsFile().toPath()))
282+
File archiveFile = this.task.getArchiveFile().get().getAsFile();
283+
assertThat(Files.readAllBytes(archiveFile.toPath()))
283284
.startsWith(new DefaultLaunchScript(null, properties).toByteArray());
285+
try (ZipFile zipFile = new ZipFile(archiveFile)) {
286+
assertThat(zipFile.getEntries().hasMoreElements()).isTrue();
287+
}
284288
try {
285-
Set<PosixFilePermission> permissions = Files
286-
.getPosixFilePermissions(this.task.getArchiveFile().get().getAsFile().toPath());
289+
Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(archiveFile.toPath());
287290
assertThat(permissions).contains(PosixFilePermission.OWNER_EXECUTE);
288291
}
289292
catch (UnsupportedOperationException ex) {

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -235,12 +235,16 @@ void buildsImageWithBinding() throws IOException {
235235
}
236236

237237
@TestTemplate
238-
void failsWithLaunchScript() throws IOException {
238+
void buildsImageWithLaunchScript() throws IOException {
239239
writeMainClass();
240240
writeLongNameResource();
241-
BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage");
242-
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.FAILED);
243-
assertThat(result.getOutput()).contains("not compatible with buildpacks");
241+
BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT");
242+
String projectName = this.gradleBuild.getProjectDir().getName();
243+
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
244+
assertThat(result.getOutput()).contains("docker.io/library/" + projectName);
245+
assertThat(result.getOutput()).contains("---> Test Info buildpack building");
246+
assertThat(result.getOutput()).contains("---> Test Info buildpack done");
247+
removeImage(projectName);
244248
}
245249

246250
@TestTemplate

spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarWriter.java

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,12 +20,7 @@
2020
import java.io.FileNotFoundException;
2121
import java.io.FileOutputStream;
2222
import java.io.IOException;
23-
import java.nio.file.Files;
24-
import java.nio.file.Path;
2523
import java.nio.file.attribute.FileTime;
26-
import java.nio.file.attribute.PosixFilePermission;
27-
import java.util.HashSet;
28-
import java.util.Set;
2924
import java.util.zip.ZipEntry;
3025
import java.util.zip.ZipException;
3126

@@ -39,6 +34,7 @@
3934
* @author Phillip Webb
4035
* @author Andy Wilkinson
4136
* @author Madhura Bhave
37+
* @author Scott Frederick
4238
* @since 1.0.0
4339
*/
4440
public class JarWriter extends AbstractJarWriter implements AutoCloseable {
@@ -80,28 +76,15 @@ public JarWriter(File file, LaunchScript launchScript) throws FileNotFoundExcept
8076
*/
8177
public JarWriter(File file, LaunchScript launchScript, FileTime lastModifiedTime)
8278
throws FileNotFoundException, IOException {
83-
FileOutputStream fileOutputStream = new FileOutputStream(file);
79+
this.jarOutputStream = new JarArchiveOutputStream(new FileOutputStream(file));
8480
if (launchScript != null) {
85-
fileOutputStream.write(launchScript.toByteArray());
86-
setExecutableFilePermission(file);
81+
this.jarOutputStream.writePreamble(launchScript.toByteArray());
82+
file.setExecutable(true);
8783
}
88-
this.jarOutputStream = new JarArchiveOutputStream(fileOutputStream);
8984
this.jarOutputStream.setEncoding("UTF-8");
9085
this.lastModifiedTime = lastModifiedTime;
9186
}
9287

93-
private void setExecutableFilePermission(File file) {
94-
try {
95-
Path path = file.toPath();
96-
Set<PosixFilePermission> permissions = new HashSet<>(Files.getPosixFilePermissions(path));
97-
permissions.add(PosixFilePermission.OWNER_EXECUTE);
98-
Files.setPosixFilePermissions(path, permissions);
99-
}
100-
catch (Throwable ex) {
101-
// Ignore and continue creating the jar
102-
}
103-
}
104-
10588
@Override
10689
protected void writeToArchive(ZipEntry entry, EntryWriter entryWriter) throws IOException {
10790
JarArchiveEntry jarEntry = asJarArchiveEntry(entry);

spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -49,6 +49,7 @@
4949
* @author Phillip Webb
5050
* @author Andy Wilkinson
5151
* @author Madhura Bhave
52+
* @author Scott Frederick
5253
*/
5354
class RepackagerTests extends AbstractPackagerTests<Repackager> {
5455

@@ -159,6 +160,9 @@ void addLauncherScript() throws Exception {
159160
assertThat(new String(bytes)).startsWith("ABC");
160161
assertThat(hasLauncherClasses(source)).isFalse();
161162
assertThat(hasLauncherClasses(this.destination)).isTrue();
163+
try (ZipFile zipFile = new ZipFile(this.destination)) {
164+
assertThat(zipFile.getEntries().hasMoreElements()).isTrue();
165+
}
162166
try {
163167
assertThat(Files.getPosixFilePermissions(this.destination.toPath()))
164168
.contains(PosixFilePermission.OWNER_EXECUTE);

spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/intTest/java/org/springframework/boot/launchscript/AbstractLaunchScriptIntegrationTests.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -106,7 +106,8 @@ private LaunchScriptTestContainer(String os, String version, String scriptsDir,
106106
withCopyFileToContainer(
107107
MountableFile.forHostPath("src/intTest/resources/scripts/" + scriptsDir + testScript),
108108
"/" + testScript);
109-
withCommand("/bin/bash", "-c", "chmod +x " + testScript + " && ./" + testScript);
109+
withCommand("/bin/bash", "-c",
110+
"chown root:root *.sh && chown root:root *.jar && chmod +x " + testScript + " && ./" + testScript);
110111
withStartupCheckStrategy(new OneShotStartupCheckStrategy().withTimeout(Duration.ofMinutes(5)));
111112
}
112113

0 commit comments

Comments
 (0)