Remove support for embedded launch scripts

Closes gh-47666
This commit is contained in:
Andy Wilkinson
2025-10-20 20:03:14 +01:00
parent de39cc6659
commit 81aa674adb
99 changed files with 18 additions and 3511 deletions
@@ -1,32 +0,0 @@
/*
* Copyright 2012-present 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.
*/
plugins {
id 'java'
id 'org.springframework.boot' version '{version-spring-boot}'
}
tasks.named("bootJar") {
mainClass = 'com.example.ExampleApplication'
}
// tag::custom-launch-script[]
tasks.named("bootJar") {
launchScript {
script = file('src/custom.script')
}
}
// end::custom-launch-script[]
@@ -1,18 +0,0 @@
import org.springframework.boot.gradle.tasks.bundling.BootJar
plugins {
java
id("org.springframework.boot") version "{version-spring-boot}"
}
tasks.named<BootJar>("bootJar") {
mainClass.set("com.example.ExampleApplication")
}
// tag::custom-launch-script[]
tasks.named<BootJar>("bootJar") {
launchScript {
script = file("src/custom.script")
}
}
// end::custom-launch-script[]
@@ -1,30 +0,0 @@
/*
* Copyright 2012-present 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.
*/
plugins {
id 'java'
id 'org.springframework.boot' version '{version-spring-boot}'
}
tasks.named("bootJar") {
mainClass = 'com.example.ExampleApplication'
}
// tag::include-launch-script[]
tasks.named("bootJar") {
launchScript()
}
// end::include-launch-script[]
@@ -1,16 +0,0 @@
import org.springframework.boot.gradle.tasks.bundling.BootJar
plugins {
java
id("org.springframework.boot") version "{version-spring-boot}"
}
tasks.named<BootJar>("bootJar") {
mainClass.set("com.example.ExampleApplication")
}
// tag::include-launch-script[]
tasks.named<BootJar>("bootJar") {
launchScript()
}
// end::include-launch-script[]
@@ -1,32 +0,0 @@
/*
* Copyright 2012-present 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.
*/
plugins {
id 'java'
id 'org.springframework.boot' version '{version-spring-boot}'
}
tasks.named("bootJar") {
mainClass = 'com.example.ExampleApplication'
}
// tag::launch-script-properties[]
tasks.named("bootJar") {
launchScript {
properties 'logFilename': 'example-app.log'
}
}
// end::launch-script-properties[]
@@ -1,18 +0,0 @@
import org.springframework.boot.gradle.tasks.bundling.BootJar
plugins {
java
id("org.springframework.boot") version "{version-spring-boot}"
}
tasks.named<BootJar>("bootJar") {
mainClass.set("com.example.ExampleApplication")
}
// tag::launch-script-properties[]
tasks.named<BootJar>("bootJar") {
launchScript {
properties(mapOf("logFilename" to "example-app.log"))
}
}
// end::launch-script-properties[]
@@ -246,75 +246,6 @@ The closure is passed a `FileTreeElement` and should return a `boolean` indicati
[[packaging-executable.configuring.launch-script]]
=== Making an Archive Fully Executable
Spring Boot provides support for fully executable archives.
An archive is made fully executable by prepending a shell script that knows how to launch the application.
On Unix-like platforms, this launch script allows the archive to be run directly like any other executable or to be installed as a service.
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` or deploying it to a servlet container.
To use this feature, the inclusion of the launch script must be enabled:
[tabs]
======
Groovy::
+
[source,groovy,indent=0,subs="verbatim,attributes"]
----
include::example$packaging/boot-jar-include-launch-script.gradle[tags=include-launch-script]
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,attributes"]
----
include::example$packaging/boot-jar-include-launch-script.gradle.kts[tags=include-launch-script]
----
======
This will add Spring Boot's default launch script to the archive.
The default launch script includes several properties with sensible default values.
The values can be customized using the `properties` property:
[tabs]
======
Groovy::
+
[source,groovy,indent=0,subs="verbatim,attributes"]
----
include::example$packaging/boot-jar-launch-script-properties.gradle[tags=launch-script-properties]
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,attributes"]
----
include::example$packaging/boot-jar-launch-script-properties.gradle.kts[tags=launch-script-properties]
----
======
If the default launch script does not meet your needs, the `script` property can be used to provide a custom launch script:
[tabs]
======
Groovy::
+
[source,groovy,indent=0,subs="verbatim,attributes"]
----
include::example$packaging/boot-jar-custom-launch-script.gradle[tags=custom-launch-script]
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,attributes"]
----
include::example$packaging/boot-jar-custom-launch-script.gradle.kts[tags=custom-launch-script]
----
======
[[packaging-executable.configuring.properties-launcher]]
=== Using the PropertiesLauncher
@@ -18,7 +18,6 @@ package org.springframework.boot.gradle.tasks.bundling;
import java.util.Set;
import org.gradle.api.Action;
import org.gradle.api.JavaVersion;
import org.gradle.api.Project;
import org.gradle.api.Task;
@@ -30,7 +29,6 @@ import org.gradle.api.provider.Provider;
import org.gradle.api.specs.Spec;
import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Nested;
import org.gradle.api.tasks.Optional;
import org.jspecify.annotations.Nullable;
@@ -65,28 +63,6 @@ public interface BootArchive extends Task {
*/
void requiresUnpack(Spec<FileTreeElement> spec);
/**
* Returns the {@link LaunchScriptConfiguration} that will control the script that is
* prepended to the archive.
* @return the launch script configuration, or {@code null} if the launch script has
* not been configured.
*/
@Nested
@Optional
@Nullable LaunchScriptConfiguration getLaunchScript();
/**
* Configures the archive to have a prepended launch script.
*/
void launchScript();
/**
* Configures the archive to have a prepended launch script, customizing its
* configuration using the given {@code action}.
* @param action the action to apply
*/
void launchScript(Action<LaunchScriptConfiguration> action);
/**
* Returns the classpath that will be included in the archive.
* @return the classpath
@@ -75,8 +75,6 @@ class BootArchiveSupport {
private final Function<FileCopyDetails, ZipCompression> compressionResolver;
private @Nullable LaunchScriptConfiguration launchScript;
BootArchiveSupport(String loaderMainClass, Spec<FileCopyDetails> librarySpec,
Function<FileCopyDetails, ZipCompression> compressionResolver) {
this.loaderMainClass = loaderMainClass;
@@ -129,13 +127,12 @@ class BootArchiveSupport {
boolean includeDefaultLoader = isUsingDefaultLoader(jar);
Spec<FileTreeElement> requiresUnpack = this.requiresUnpack.getAsSpec();
Spec<FileTreeElement> exclusions = this.exclusions.getAsExcludeSpec();
LaunchScriptConfiguration launchScript = this.launchScript;
Spec<FileCopyDetails> librarySpec = this.librarySpec;
Function<FileCopyDetails, ZipCompression> compressionResolver = this.compressionResolver;
String encoding = jar.getMetadataCharset();
CopyAction action = new BootZipCopyAction(output, manifest, preserveFileTimestamps, dirPermissions,
filePermissions, includeDefaultLoader, jarmodeToolsLocation, requiresUnpack, exclusions, launchScript,
librarySpec, compressionResolver, encoding, resolvedDependencies, supportsSignatureFile, layerResolver);
filePermissions, includeDefaultLoader, jarmodeToolsLocation, requiresUnpack, exclusions, librarySpec,
compressionResolver, encoding, resolvedDependencies, supportsSignatureFile, layerResolver);
return action;
}
@@ -175,14 +172,6 @@ class BootArchiveSupport {
return DEFAULT_LAUNCHER_CLASSES.contains(jar.getManifest().getAttributes().get("Main-Class"));
}
@Nullable LaunchScriptConfiguration getLaunchScript() {
return this.launchScript;
}
void setLaunchScript(LaunchScriptConfiguration launchScript) {
this.launchScript = launchScript;
}
void requiresUnpack(String... patterns) {
this.requiresUnpack.include(patterns);
}
@@ -166,21 +166,6 @@ public abstract class BootJar extends Jar implements BootArchive {
this.support.requiresUnpack(spec);
}
@Override
public @Nullable LaunchScriptConfiguration getLaunchScript() {
return this.support.getLaunchScript();
}
@Override
public void launchScript() {
enableLaunchScriptIfNecessary();
}
@Override
public void launchScript(Action<LaunchScriptConfiguration> action) {
action.execute(enableLaunchScriptIfNecessary());
}
/**
* Returns the spec that describes the layers in a layered jar.
* @return the spec for the layers
@@ -273,15 +258,6 @@ public abstract class BootJar extends Jar implements BootArchive {
return path.startsWith(LIB_DIRECTORY);
}
private LaunchScriptConfiguration enableLaunchScriptIfNecessary() {
LaunchScriptConfiguration launchScript = this.support.getLaunchScript();
if (launchScript == null) {
launchScript = new LaunchScriptConfiguration(this);
this.support.setLaunchScript(launchScript);
}
return launchScript;
}
/**
* Syntactic sugar that makes {@link CopySpec#into} calls a little easier to read.
* @param <T> the result type
@@ -140,21 +140,6 @@ public abstract class BootWar extends War implements BootArchive {
this.support.requiresUnpack(spec);
}
@Override
public @Nullable LaunchScriptConfiguration getLaunchScript() {
return this.support.getLaunchScript();
}
@Override
public void launchScript() {
enableLaunchScriptIfNecessary();
}
@Override
public void launchScript(Action<LaunchScriptConfiguration> action) {
action.execute(enableLaunchScriptIfNecessary());
}
/**
* Returns the provided classpath, the contents of which will be included in the
* {@code WEB-INF/lib-provided} directory of the war.
@@ -241,15 +226,6 @@ public abstract class BootWar extends War implements BootArchive {
return path.startsWith(LIB_DIRECTORY) || path.startsWith(LIB_PROVIDED_DIRECTORY);
}
private LaunchScriptConfiguration enableLaunchScriptIfNecessary() {
LaunchScriptConfiguration launchScript = this.support.getLaunchScript();
if (launchScript == null) {
launchScript = new LaunchScriptConfiguration(this);
this.support.setLaunchScript(launchScript);
}
return launchScript;
}
/**
* Syntactic sugar that makes {@link CopySpec#into} calls a little easier to read.
* @param <T> the result type
@@ -54,7 +54,6 @@ import org.gradle.util.GradleVersion;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.gradle.tasks.bundling.ResolvedDependencies.DependencyDescriptor;
import org.springframework.boot.loader.tools.DefaultLaunchScript;
import org.springframework.boot.loader.tools.FileUtils;
import org.springframework.boot.loader.tools.JarModeLibrary;
import org.springframework.boot.loader.tools.Layer;
@@ -102,8 +101,6 @@ class BootZipCopyAction implements CopyAction {
private final Spec<FileTreeElement> exclusions;
private final @Nullable LaunchScriptConfiguration launchScript;
private final Spec<FileCopyDetails> librarySpec;
private final Function<FileCopyDetails, ZipCompression> compressionResolver;
@@ -118,8 +115,7 @@ class BootZipCopyAction implements CopyAction {
BootZipCopyAction(File output, Manifest manifest, boolean preserveFileTimestamps, @Nullable Integer dirMode,
@Nullable Integer fileMode, boolean includeDefaultLoader, @Nullable String jarmodeToolsLocation,
Spec<FileTreeElement> requiresUnpack, Spec<FileTreeElement> exclusions,
@Nullable LaunchScriptConfiguration launchScript, Spec<FileCopyDetails> librarySpec,
Spec<FileTreeElement> requiresUnpack, Spec<FileTreeElement> exclusions, Spec<FileCopyDetails> librarySpec,
Function<FileCopyDetails, ZipCompression> compressionResolver, @Nullable String encoding,
ResolvedDependencies resolvedDependencies, boolean supportsSignatureFile,
@Nullable LayerResolver layerResolver) {
@@ -132,7 +128,6 @@ class BootZipCopyAction implements CopyAction {
this.jarmodeToolsLocation = jarmodeToolsLocation;
this.requiresUnpack = requiresUnpack;
this.exclusions = exclusions;
this.launchScript = launchScript;
this.librarySpec = librarySpec;
this.compressionResolver = compressionResolver;
this.encoding = encoding;
@@ -164,7 +159,6 @@ class BootZipCopyAction implements CopyAction {
private void writeArchive(CopyActionProcessingStream copyActions, OutputStream output) throws IOException {
ZipArchiveOutputStream zipOutput = new ZipArchiveOutputStream(output);
writeLaunchScriptIfNecessary(zipOutput);
try {
setEncodingIfNecessary(zipOutput);
Processor processor = new Processor(zipOutput);
@@ -176,21 +170,6 @@ class BootZipCopyAction implements CopyAction {
}
}
private void writeLaunchScriptIfNecessary(ZipArchiveOutputStream outputStream) {
if (this.launchScript == null) {
return;
}
try {
File file = this.launchScript.getScript();
Map<String, String> properties = this.launchScript.getProperties();
outputStream.writePreamble(new DefaultLaunchScript(file, properties).toByteArray());
this.output.setExecutable(true);
}
catch (IOException ex) {
throw new GradleException("Failed to write launch script to " + this.output, ex);
}
}
private void setEncodingIfNecessary(ZipArchiveOutputStream zipOutputStream) {
if (this.encoding != null) {
zipOutputStream.setEncoding(this.encoding);
@@ -1,128 +0,0 @@
/*
* Copyright 2012-present 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.gradle.tasks.bundling;
import java.io.File;
import java.io.Serializable;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Pattern;
import org.gradle.api.Project;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.PathSensitive;
import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.bundling.AbstractArchiveTask;
import org.jspecify.annotations.Nullable;
import org.springframework.lang.Contract;
import org.springframework.util.StringUtils;
/**
* Encapsulates the configuration of the launch script for an executable jar or war.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
@SuppressWarnings("serial")
public class LaunchScriptConfiguration implements Serializable {
private static final Pattern WHITE_SPACE_PATTERN = Pattern.compile("\\s+");
private static final Pattern LINE_FEED_PATTERN = Pattern.compile("\n");
// We don't care about the order, but Gradle's configuration cache currently does.
// https://github.com/gradle/gradle/pull/17863
private final Map<String, String> properties = new TreeMap<>();
private @Nullable File script;
public LaunchScriptConfiguration() {
}
LaunchScriptConfiguration(AbstractArchiveTask archiveTask) {
Project project = archiveTask.getProject();
String baseName = archiveTask.getArchiveBaseName().get();
putIfMissing(this.properties, "initInfoProvides", baseName);
putIfMissing(this.properties, "initInfoShortDescription", removeLineBreaks(project.getDescription()), baseName);
putIfMissing(this.properties, "initInfoDescription", augmentLineBreaks(project.getDescription()), baseName);
}
/**
* Returns the properties that are applied to the launch script when it's being
* including in the executable archive.
* @return the properties
*/
@Input
public Map<String, String> getProperties() {
return this.properties;
}
/**
* Sets the properties that are applied to the launch script when it's being including
* in the executable archive.
* @param properties the properties
*/
public void properties(Map<String, String> properties) {
this.properties.putAll(properties);
}
/**
* Returns the script {@link File} that will be included in the executable archive.
* When {@code null}, the default launch script will be used.
* @return the script file
*/
@Optional
@InputFile
@PathSensitive(PathSensitivity.RELATIVE)
public @Nullable File getScript() {
return this.script;
}
/**
* Sets the script {@link File} that will be included in the executable archive. When
* {@code null}, the default launch script will be used.
* @param script the script file
*/
public void setScript(@Nullable File script) {
this.script = script;
}
@Contract("!null -> !null")
private @Nullable String removeLineBreaks(@Nullable String string) {
return (string != null) ? WHITE_SPACE_PATTERN.matcher(string).replaceAll(" ") : null;
}
@Contract("!null -> !null")
private @Nullable String augmentLineBreaks(@Nullable String string) {
return (string != null) ? LINE_FEED_PATTERN.matcher(string).replaceAll("\n# ") : null;
}
private void putIfMissing(Map<String, String> properties, String key, @Nullable String... valueCandidates) {
if (!properties.containsKey(key)) {
for (String candidate : valueCandidates) {
if (StringUtils.hasLength(candidate)) {
properties.put(key, candidate);
return;
}
}
}
}
}
@@ -18,8 +18,6 @@ package org.springframework.boot.gradle.docs;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Collections;
import java.util.jar.JarEntry;
@@ -34,7 +32,6 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.gradle.junit.GradleMultiDslExtension;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import org.springframework.util.FileCopyUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -128,36 +125,6 @@ class PackagingDocumentationTests {
}
}
@TestTemplate
void bootJarIncludeLaunchScript() throws IOException {
this.gradleBuild.script(Examples.DIR + "packaging/boot-jar-include-launch-script").build("bootJar");
File file = new File(this.gradleBuild.getProjectDir(),
"build/libs/" + this.gradleBuild.getProjectDir().getName() + ".jar");
assertThat(file).isFile();
assertThat(FileCopyUtils.copyToString(new FileReader(file))).startsWith("#!/bin/bash");
}
@TestTemplate
void bootJarLaunchScriptProperties() throws IOException {
this.gradleBuild.script(Examples.DIR + "packaging/boot-jar-launch-script-properties").build("bootJar");
File file = new File(this.gradleBuild.getProjectDir(),
"build/libs/" + this.gradleBuild.getProjectDir().getName() + ".jar");
assertThat(file).isFile();
assertThat(FileCopyUtils.copyToString(new FileReader(file))).contains("example-app.log");
}
@TestTemplate
void bootJarCustomLaunchScript() throws IOException {
File customScriptFile = new File(this.gradleBuild.getProjectDir(), "src/custom.script");
customScriptFile.getParentFile().mkdirs();
FileCopyUtils.copy("custom", new FileWriter(customScriptFile));
this.gradleBuild.script(Examples.DIR + "packaging/boot-jar-custom-launch-script").build("bootJar");
File file = new File(this.gradleBuild.getProjectDir(),
"build/libs/" + this.gradleBuild.getProjectDir().getName() + ".jar");
assertThat(file).isFile();
assertThat(FileCopyUtils.copyToString(new FileReader(file))).startsWith("custom");
}
@TestTemplate
void bootWarPropertiesLauncher() throws IOException {
this.gradleBuild.script(Examples.DIR + "packaging/boot-war-properties-launcher").build("bootWar");
@@ -123,56 +123,6 @@ abstract class AbstractBootArchiveIntegrationTests {
assertThat(task.getOutcome()).isEqualTo(TaskOutcome.UP_TO_DATE);
}
@TestTemplate
void upToDateWhenBuiltTwiceWithLaunchScriptIncluded() {
BuildTask task = this.gradleBuild.build("-PincludeLaunchScript=true", this.taskName).task(":" + this.taskName);
assertThat(task).isNotNull();
assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
task = this.gradleBuild.build("-PincludeLaunchScript=true", this.taskName).task(":" + this.taskName);
assertThat(task).isNotNull();
assertThat(task.getOutcome()).isEqualTo(TaskOutcome.UP_TO_DATE);
}
@TestTemplate
void notUpToDateWhenLaunchScriptWasNotIncludedAndThenIsIncluded() {
BuildTask task = this.gradleBuild.scriptProperty("launchScript", "")
.build(this.taskName)
.task(":" + this.taskName);
assertThat(task).isNotNull();
assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
task = this.gradleBuild.scriptProperty("launchScript", "launchScript()")
.build(this.taskName)
.task(":" + this.taskName);
assertThat(task).isNotNull();
assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
}
@TestTemplate
void notUpToDateWhenLaunchScriptWasIncludedAndThenIsNotIncluded() {
BuildTask task = this.gradleBuild.scriptProperty("launchScript", "launchScript()")
.build(this.taskName)
.task(":" + this.taskName);
assertThat(task).isNotNull();
assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
task = this.gradleBuild.scriptProperty("launchScript", "").build(this.taskName).task(":" + this.taskName);
assertThat(task).isNotNull();
assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
}
@TestTemplate
void notUpToDateWhenLaunchScriptPropertyChanges() {
BuildTask task = this.gradleBuild.scriptProperty("launchScriptProperty", "alpha")
.build(this.taskName)
.task(":" + this.taskName);
assertThat(task).isNotNull();
assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
task = this.gradleBuild.scriptProperty("launchScriptProperty", "bravo")
.build(this.taskName)
.task(":" + this.taskName);
assertThat(task).isNotNull();
assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
}
@TestTemplate
void applicationPluginMainClassNameIsUsed() throws IOException {
BuildTask task = this.gradleBuild.build(this.taskName).task(":" + this.taskName);
@@ -23,18 +23,12 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.PosixFilePermission;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
@@ -66,7 +60,6 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.gradle.junit.GradleProjectBuilder;
import org.springframework.boot.loader.tools.DefaultLaunchScript;
import org.springframework.boot.loader.tools.JarModeLibrary;
import org.springframework.util.FileCopyUtils;
@@ -310,57 +303,6 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
}
}
@Test
void launchScriptCanBePrepended() throws IOException {
this.task.getMainClass().set("com.example.Main");
this.task.launchScript();
executeTask();
Map<String, String> properties = new HashMap<>();
properties.put("initInfoProvides", this.task.getArchiveBaseName().get());
properties.put("initInfoShortDescription", this.project.getDescription());
properties.put("initInfoDescription", this.project.getDescription());
File archiveFile = this.task.getArchiveFile().get().getAsFile();
assertThat(Files.readAllBytes(archiveFile.toPath()))
.startsWith(new DefaultLaunchScript(null, properties).toByteArray());
try (ZipFile zipFile = ZipFile.builder().setFile(archiveFile).get()) {
assertThat(zipFile.getEntries().hasMoreElements()).isTrue();
}
try {
Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(archiveFile.toPath());
assertThat(permissions).contains(PosixFilePermission.OWNER_EXECUTE);
}
catch (UnsupportedOperationException ex) {
// Windows, presumably. Continue
}
}
@Test
void customLaunchScriptCanBePrepended() throws IOException {
this.task.getMainClass().set("com.example.Main");
File customScript = new File(this.temp, "custom.script");
Files.writeString(customScript.toPath(), "custom script", StandardOpenOption.CREATE);
this.task.launchScript((configuration) -> configuration.setScript(customScript));
executeTask();
Path path = this.task.getArchiveFile().get().getAsFile().toPath();
assertThat(Files.readString(path, StandardCharsets.ISO_8859_1)).startsWith("custom script");
}
@Test
void launchScriptInitInfoPropertiesCanBeCustomized() throws IOException {
this.task.getMainClass().set("com.example.Main");
this.task.launchScript((configuration) -> {
configuration.getProperties().put("initInfoProvides", "provides");
configuration.getProperties().put("initInfoShortDescription", "short description");
configuration.getProperties().put("initInfoDescription", "description");
});
executeTask();
Path path = this.task.getArchiveFile().get().getAsFile().toPath();
String content = Files.readString(path, StandardCharsets.ISO_8859_1);
assertThat(content).containsSequence("Provides: provides");
assertThat(content).containsSequence("Short-Description: short description");
assertThat(content).containsSequence("Description: description");
}
@Test
void customMainClassInTheManifestIsHonored() throws IOException {
this.task.getMainClass().set("com.example.Main");
@@ -1,112 +0,0 @@
/*
* Copyright 2012-present 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.gradle.tasks.bundling;
import org.gradle.api.Project;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.bundling.AbstractArchiveTask;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link LaunchScriptConfiguration}.
*
* @author Andy Wilkinson
*/
class LaunchScriptConfigurationTests {
private final AbstractArchiveTask task = mock(AbstractArchiveTask.class);
private final Project project = mock(Project.class);
@BeforeEach
void setUp() {
given(this.task.getProject()).willReturn(this.project);
}
@Test
void initInfoProvidesUsesArchiveBaseNameByDefault() {
Property<String> baseName = stringProperty("base-name");
given(this.task.getArchiveBaseName()).willReturn(baseName);
assertThat(new LaunchScriptConfiguration(this.task).getProperties()).containsEntry("initInfoProvides",
"base-name");
}
@Test
void initInfoShortDescriptionUsesDescriptionByDefault() {
given(this.project.getDescription()).willReturn("Project description");
Property<String> baseName = stringProperty("base-name");
given(this.task.getArchiveBaseName()).willReturn(baseName);
assertThat(new LaunchScriptConfiguration(this.task).getProperties()).containsEntry("initInfoShortDescription",
"Project description");
}
@Test
void initInfoShortDescriptionUsesArchiveBaseNameWhenDescriptionIsNull() {
Property<String> baseName = stringProperty("base-name");
given(this.task.getArchiveBaseName()).willReturn(baseName);
assertThat(new LaunchScriptConfiguration(this.task).getProperties()).containsEntry("initInfoShortDescription",
"base-name");
}
@Test
void initInfoShortDescriptionUsesSingleLineVersionOfMultiLineProjectDescription() {
given(this.project.getDescription()).willReturn("Project\ndescription");
Property<String> baseName = stringProperty("base-name");
given(this.task.getArchiveBaseName()).willReturn(baseName);
assertThat(new LaunchScriptConfiguration(this.task).getProperties()).containsEntry("initInfoShortDescription",
"Project description");
}
@Test
void initInfoDescriptionUsesArchiveBaseNameWhenDescriptionIsNull() {
Property<String> baseName = stringProperty("base-name");
given(this.task.getArchiveBaseName()).willReturn(baseName);
assertThat(new LaunchScriptConfiguration(this.task).getProperties()).containsEntry("initInfoDescription",
"base-name");
}
@Test
void initInfoDescriptionUsesProjectDescriptionByDefault() {
given(this.project.getDescription()).willReturn("Project description");
Property<String> baseName = stringProperty("base-name");
given(this.task.getArchiveBaseName()).willReturn(baseName);
assertThat(new LaunchScriptConfiguration(this.task).getProperties()).containsEntry("initInfoDescription",
"Project description");
}
@Test
void initInfoDescriptionUsesCorrectlyFormattedMultiLineProjectDescription() {
given(this.project.getDescription()).willReturn("The\nproject\ndescription");
Property<String> baseName = stringProperty("base-name");
given(this.task.getArchiveBaseName()).willReturn(baseName);
assertThat(new LaunchScriptConfiguration(this.task).getProperties()).containsEntry("initInfoDescription",
"The\n# project\n# description");
}
@SuppressWarnings("unchecked")
private Property<String> stringProperty(String value) {
Property<String> property = mock(Property.class);
given(property.get()).willReturn(value);
return property;
}
}
@@ -41,7 +41,7 @@ import tools.jackson.databind.JacksonModule;
import org.springframework.asm.ClassVisitor;
import org.springframework.boot.buildpack.platform.build.BuildRequest;
import org.springframework.boot.loader.tools.LaunchScript;
import org.springframework.boot.loader.tools.Layers;
import org.springframework.boot.testsupport.BuildOutput;
import org.springframework.boot.testsupport.gradle.testkit.Dsl;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
@@ -80,7 +80,7 @@ public class PluginClasspathGradleBuild extends GradleBuild {
classpath.add(new File("bin/main"));
classpath.add(new File("build/classes/java/main"));
classpath.add(new File("build/resources/main"));
classpath.add(new File(pathOfJarContaining(LaunchScript.class)));
classpath.add(new File(pathOfJarContaining(Layers.class)));
classpath.add(new File(pathOfJarContaining(ClassVisitor.class)));
classpath.add(new File(pathOfJarContaining(DependencyManagementPlugin.class)));
if (this.kotlin) {
@@ -1,27 +0,0 @@
/*
* Copyright 2012-present 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.
*/
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
}
bootJar {
mainClass = 'com.example.Application'
launchScript {
properties 'prop' : '{launchScriptProperty}'
}
}
@@ -1,25 +0,0 @@
/*
* Copyright 2012-present 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.
*/
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
}
bootJar {
mainClass = 'com.example.Application'
{launchScript}
}
@@ -1,25 +0,0 @@
/*
* Copyright 2012-present 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.
*/
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
}
bootJar {
mainClass = 'com.example.Application'
{launchScript}
}
@@ -1,25 +0,0 @@
/*
* Copyright 2012-present 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.
*/
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
}
bootJar {
mainClass = 'com.example.Application'
launchScript()
}
@@ -1,27 +0,0 @@
/*
* Copyright 2012-present 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.
*/
plugins {
id 'war'
id 'org.springframework.boot' version '{version}'
}
bootWar {
mainClass = 'com.example.Application'
launchScript {
properties 'prop' : '{launchScriptProperty}'
}
}
@@ -1,25 +0,0 @@
/*
* Copyright 2012-present 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.
*/
plugins {
id 'war'
id 'org.springframework.boot' version '{version}'
}
bootWar {
mainClass = 'com.example.Application'
{launchScript}
}
@@ -1,25 +0,0 @@
/*
* Copyright 2012-present 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.
*/
plugins {
id 'war'
id 'org.springframework.boot' version '{version}'
}
bootWar {
mainClass = 'com.example.Application'
{launchScript}
}
@@ -1,25 +0,0 @@
/*
* Copyright 2012-present 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.
*/
plugins {
id 'war'
id 'org.springframework.boot' version '{version}'
}
bootWar {
mainClass = 'com.example.Application'
launchScript()
}
@@ -160,15 +160,6 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests {
});
}
@TestTemplate
void whenACustomLaunchScriptIsConfiguredItAppearsInTheRepackagedJar(MavenBuild mavenBuild) {
mavenBuild.project("jar-custom-launcher").goals("install").execute((project) -> {
File repackaged = new File(project, "target/jar-0.0.1.BUILD-SNAPSHOT.jar");
assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/classes/");
assertThat(launchScript(repackaged)).contains("Hello world");
});
}
@TestTemplate
void whenAnEntryIsExcludedItDoesNotAppearInTheRepackagedJar(MavenBuild mavenBuild) {
mavenBuild.project("jar-exclude-entry").goals("install").execute((project) -> {
@@ -267,17 +258,6 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests {
});
}
@TestTemplate
void whenAJarIsExecutableItBeginsWithTheDefaultLaunchScript(MavenBuild mavenBuild) {
mavenBuild.project("jar-executable").execute((project) -> {
File repackaged = new File(project, "target/jar-executable-0.0.1.BUILD-SNAPSHOT.jar");
assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/classes/");
assertThat(launchScript(repackaged)).contains("Spring Boot Startup Script")
.contains("MyFullyExecutableJarName")
.contains("MyFullyExecutableJarDesc");
});
}
@TestTemplate
void whenAJarIsBuiltWithLibrariesWithConflictingNamesTheyAreMadeUniqueUsingTheirGroupIds(MavenBuild mavenBuild) {
mavenBuild.project("jar-lib-name-conflict").execute((project) -> {
@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>jar</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>@java.version@</maven.compiler.source>
<maven.compiler.target>@java.version@</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>@project.groupId@</groupId>
<artifactId>@project.artifactId@</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<embeddedLaunchScript>${basedir}/src/launcher/custom.script</embeddedLaunchScript>
<embeddedLaunchScriptProperties>
<name>world</name>
</embeddedLaunchScriptProperties>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -1,3 +0,0 @@
#!/bin/sh
echo "Hello {{name}}"
@@ -1,24 +0,0 @@
/*
* Copyright 2012-present 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) {
}
}
@@ -1,46 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>jar-executable</artifactId>
<name>MyFullyExecutableJarName</name>
<description>MyFullyExecutableJarDesc</description>
<version>0.0.1.BUILD-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>@java.version@</maven.compiler.source>
<maven.compiler.target>@java.version@</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>@project.groupId@</groupId>
<artifactId>@project.artifactId@</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<executable>true</executable>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>@maven-jar-plugin.version@</version>
<configuration>
<archive>
<manifest>
<mainClass>some.random.Main</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -1,24 +0,0 @@
/*
* Copyright 2012-present 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) {
}
}
@@ -20,7 +20,6 @@ import java.io.File;
import java.io.IOException;
import java.nio.file.attribute.FileTime;
import java.util.List;
import java.util.Properties;
import java.util.regex.Pattern;
import javax.inject.Inject;
@@ -36,13 +35,10 @@ import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProjectHelper;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.loader.tools.DefaultLaunchScript;
import org.springframework.boot.loader.tools.LaunchScript;
import org.springframework.boot.loader.tools.LayoutFactory;
import org.springframework.boot.loader.tools.Libraries;
import org.springframework.boot.loader.tools.Repackager;
import org.springframework.lang.Contract;
import org.springframework.util.StringUtils;
/**
* Repackage existing JAR and WAR archives so that they can be executed from the command
@@ -122,35 +118,6 @@ public class RepackageMojo extends AbstractPackagerMojo {
@Parameter
private @Nullable List<Dependency> requiresUnpack;
/**
* Make a fully executable jar for *nix machines by prepending a launch script to the
* jar.
* <p>
* Currently, some tools do not accept this format so you may not always be able to
* use this technique. For example, {@code 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
* {@code java -jar} or deploying it to a servlet container.
* @since 1.3.0
*/
@Parameter(defaultValue = "false")
private boolean executable;
/**
* The embedded launch script to prepend to the front of the jar if it is fully
* executable. If not specified the 'Spring Boot' default script will be used.
* @since 1.3.0
*/
@Parameter
private @Nullable File embeddedLaunchScript;
/**
* Properties that should be expanded in the embedded launch script.
* @since 1.3.0
*/
@Parameter
private @Nullable Properties embeddedLaunchScriptProperties;
/**
* Timestamp for reproducible output archive entries, either formatted as ISO 8601
* (<code>yyyy-MM-dd'T'HH:mm:ssXXX</code>) or an {@code int} representing seconds
@@ -227,8 +194,7 @@ public class RepackageMojo extends AbstractPackagerMojo {
Repackager repackager = getRepackager(source.getFile());
Libraries libraries = getLibraries(this.requiresUnpack);
try {
LaunchScript launchScript = getLaunchScript();
repackager.repackage(target, libraries, launchScript, parseOutputTimestamp());
repackager.repackage(target, libraries, parseOutputTimestamp());
}
catch (IOException ex) {
throw new MojoExecutionException(ex.getMessage(), ex);
@@ -249,41 +215,11 @@ public class RepackageMojo extends AbstractPackagerMojo {
return getConfiguredPackager(() -> new Repackager(source));
}
private @Nullable LaunchScript getLaunchScript() throws IOException {
if (this.executable || this.embeddedLaunchScript != null) {
return new DefaultLaunchScript(this.embeddedLaunchScript, buildLaunchScriptProperties());
}
return null;
}
private Properties buildLaunchScriptProperties() {
Properties properties = new Properties();
if (this.embeddedLaunchScriptProperties != null) {
properties.putAll(this.embeddedLaunchScriptProperties);
}
putIfMissing(properties, "initInfoProvides", this.project.getArtifactId());
putIfMissing(properties, "initInfoShortDescription", this.project.getName(), this.project.getArtifactId());
putIfMissing(properties, "initInfoDescription", removeLineBreaks(this.project.getDescription()),
this.project.getName(), this.project.getArtifactId());
return properties;
}
@Contract("!null -> !null")
private @Nullable String removeLineBreaks(@Nullable String description) {
return (description != null) ? WHITE_SPACE_PATTERN.matcher(description).replaceAll(" ") : null;
}
private void putIfMissing(Properties properties, String key, String... valueCandidates) {
if (!properties.containsKey(key)) {
for (String candidate : valueCandidates) {
if (StringUtils.hasLength(candidate)) {
properties.put(key, candidate);
return;
}
}
}
}
private void updateArtifact(Artifact source, File target, File original) {
if (this.attach) {
attachArtifact(source, target, original);
@@ -335,7 +335,6 @@
* xref:gradle-plugin:packaging-oci-image.adoc#build-image[gradle-plugin#build-image]
* xref:gradle-plugin:packaging.adoc#packaging-executable.and-plain-archives[gradle-plugin#packaging-executable.and-plain-archives]
* xref:gradle-plugin:packaging.adoc#packaging-executable.configuring.including-development-only-dependencies[gradle-plugin#packaging-executable.configuring.including-development-only-dependencies]
* xref:gradle-plugin:packaging.adoc#packaging-executable.configuring.launch-script[gradle-plugin#packaging-executable.configuring.launch-script]
* xref:gradle-plugin:packaging.adoc#packaging-executable.configuring.layered-archives.configuration[gradle-plugin#packaging-executable.configuring.layered-archives.configuration]
* xref:gradle-plugin:packaging.adoc#packaging-executable.configuring.layered-archives[gradle-plugin#packaging-executable.configuring.layered-archives]
* xref:gradle-plugin:packaging.adoc#packaging-executable.configuring.main-class[gradle-plugin#packaging-executable.configuring.main-class]
@@ -502,23 +501,6 @@
* xref:how-to:deployment/index.adoc#howto.deployment[#deployment]
* xref:how-to:deployment/index.adoc[#deployment]
* xref:how-to:deployment/index.adoc[deployment]
* xref:how-to:deployment/installing.adoc#howto.deployment.installing.init-d.script-customization.when-running.conf-file[#deployment.installing.init-d.script-customization.when-running.conf-file]
* xref:how-to:deployment/installing.adoc#howto.deployment.installing.init-d.script-customization.when-running.conf-file[#deployment.installing.nix-services.script-customization.when-running.conf-file]
* xref:how-to:deployment/installing.adoc#howto.deployment.installing.init-d.script-customization.when-running[#deployment-script-customization-when-it-runs]
* xref:how-to:deployment/installing.adoc#howto.deployment.installing.init-d.script-customization.when-running[#deployment.installing.init-d.script-customization.when-running]
* xref:how-to:deployment/installing.adoc#howto.deployment.installing.init-d.script-customization.when-running[#deployment.installing.nix-services.script-customization.when-running]
* xref:how-to:deployment/installing.adoc#howto.deployment.installing.init-d.script-customization.when-written[#deployment-script-customization-when-it-written]
* xref:how-to:deployment/installing.adoc#howto.deployment.installing.init-d.script-customization.when-written[#deployment.installing.init-d.script-customization.when-written]
* xref:how-to:deployment/installing.adoc#howto.deployment.installing.init-d.script-customization.when-written[#deployment.installing.nix-services.script-customization.when-written]
* xref:how-to:deployment/installing.adoc#howto.deployment.installing.init-d.script-customization[#deployment-script-customization]
* xref:how-to:deployment/installing.adoc#howto.deployment.installing.init-d.script-customization[#deployment.installing.init-d.script-customization]
* xref:how-to:deployment/installing.adoc#howto.deployment.installing.init-d.script-customization[#deployment.installing.nix-services.script-customization]
* xref:how-to:deployment/installing.adoc#howto.deployment.installing.init-d.securing[#deployment-initd-service-securing]
* xref:how-to:deployment/installing.adoc#howto.deployment.installing.init-d.securing[#deployment.installing.init-d.securing]
* xref:how-to:deployment/installing.adoc#howto.deployment.installing.init-d.securing[#deployment.installing.nix-services.init-d.securing]
* xref:how-to:deployment/installing.adoc#howto.deployment.installing.init-d[#deployment-initd-service]
* xref:how-to:deployment/installing.adoc#howto.deployment.installing.init-d[#deployment.installing.init-d]
* xref:how-to:deployment/installing.adoc#howto.deployment.installing.init-d[#deployment.installing.nix-services.init-d]
* xref:how-to:deployment/installing.adoc#howto.deployment.installing.system-d[#deployment-systemd-service]
* xref:how-to:deployment/installing.adoc#howto.deployment.installing.system-d[#deployment.installing.nix-services.system-d]
* xref:how-to:deployment/installing.adoc#howto.deployment.installing.system-d[#deployment.installing.system-d]
@@ -879,13 +861,10 @@
* xref:maven-plugin:packaging.adoc#packaging.repackage-goal.optional-parameters[maven-plugin#packaging.repackage-goal.optional-parameters]
* xref:maven-plugin:packaging.adoc#packaging.repackage-goal.parameter-details.attach[maven-plugin#packaging.repackage-goal.parameter-details.attach]
* xref:maven-plugin:packaging.adoc#packaging.repackage-goal.parameter-details.classifier[maven-plugin#packaging.repackage-goal.parameter-details.classifier]
* xref:maven-plugin:packaging.adoc#packaging.repackage-goal.parameter-details.embedded-launch-script-properties[maven-plugin#packaging.repackage-goal.parameter-details.embedded-launch-script-properties]
* xref:maven-plugin:packaging.adoc#packaging.repackage-goal.parameter-details.embedded-launch-script[maven-plugin#packaging.repackage-goal.parameter-details.embedded-launch-script]
* xref:maven-plugin:packaging.adoc#packaging.repackage-goal.parameter-details.exclude-devtools[maven-plugin#packaging.repackage-goal.parameter-details.exclude-devtools]
* xref:maven-plugin:packaging.adoc#packaging.repackage-goal.parameter-details.exclude-docker-compose[maven-plugin#packaging.repackage-goal.parameter-details.exclude-docker-compose]
* xref:maven-plugin:packaging.adoc#packaging.repackage-goal.parameter-details.exclude-group-ids[maven-plugin#packaging.repackage-goal.parameter-details.exclude-group-ids]
* xref:maven-plugin:packaging.adoc#packaging.repackage-goal.parameter-details.excludes[maven-plugin#packaging.repackage-goal.parameter-details.excludes]
* xref:maven-plugin:packaging.adoc#packaging.repackage-goal.parameter-details.executable[maven-plugin#packaging.repackage-goal.parameter-details.executable]
* xref:maven-plugin:packaging.adoc#packaging.repackage-goal.parameter-details.include-system-scope[maven-plugin#packaging.repackage-goal.parameter-details.include-system-scope]
* xref:maven-plugin:packaging.adoc#packaging.repackage-goal.parameter-details.include-tools[maven-plugin#packaging.repackage-goal.parameter-details.include-tools]
* xref:maven-plugin:packaging.adoc#packaging.repackage-goal.parameter-details.includes[maven-plugin#packaging.repackage-goal.parameter-details.includes]
@@ -2,7 +2,7 @@
= Deploying Spring Boot Applications
Spring Boot's flexible packaging options provide a great deal of choice when it comes to deploying your application.
You can deploy Spring Boot applications to a variety of cloud platforms, to virtual/real machines, or make them fully executable for Unix systems.
You can deploy Spring Boot applications to a variety of cloud platforms and to virtual or real machines.
This section covers some of the more common deployment scenarios.
@@ -1,7 +1,7 @@
[[howto.deployment.installing]]
= Installing Spring Boot Applications
In addition to running Spring Boot applications by using `java -jar` directly, it is also possible to run them as `systemd`, `init.d` or Windows services.
In addition to running Spring Boot applications by using `java -jar` directly, it is also possible to run them as services.
@@ -51,335 +51,6 @@ Run `man systemctl` for more details.
[[howto.deployment.installing.init-d]]
== Installation as an init.d Service (System V)
To use your application as `init.d` service, configure its build to produce a xref:deployment/installing.adoc[fully executable jar].
CAUTION: Fully executable jars work by embedding an extra script at the front of the file.
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 make your jar or war fully executable only if you intend to execute it directly, rather than running it with `java -jar` or deploying it to a servlet container.
CAUTION: A zip64-format jar file cannot be made fully executable.
Attempting to do so will result in a jar file that is reported as corrupt when executed directly or with `java -jar`.
A standard-format jar file that contains one or more zip64-format nested jars can be fully executable.
To create a '`fully executable`' jar with Maven, use the following plugin configuration:
[source,xml]
----
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
</configuration>
</plugin>
----
The following example shows the equivalent Gradle configuration:
[source,gradle]
----
tasks.named('bootJar') {
launchScript()
}
----
It can then be symlinked to `init.d` to support the standard `start`, `stop`, `restart`, and `status` commands.
The default launch script that is added to a fully executable jar supports most Linux distributions and is tested on CentOS and Ubuntu.
Other platforms, such as OS X and FreeBSD, require the use of a custom script.
The default scripts supports the following features:
* Starts the services as the user that owns the jar file
* Tracks the application's PID by using `/var/run/<appname>/<appname>.pid`
* Writes console logs to `/var/log/<appname>.log`
Assuming that you have a Spring Boot application installed in `/var/myapp`, to install a Spring Boot application as an `init.d` service, create a symlink, as follows:
[source,shell]
----
$ sudo ln -s /var/myapp/myapp.jar /etc/init.d/myapp
----
Once installed, you can start and stop the service in the usual way.
For example, on a Debian-based system, you could start it with the following command:
[source,shell]
----
$ service myapp start
----
TIP: If your application fails to start, check the log file written to `/var/log/<appname>.log` for errors.
You can also flag the application to start automatically by using your standard operating system tools.
For example, on Debian, you could use the following command:
[source,shell]
----
$ update-rc.d myapp defaults <priority>
----
[[howto.deployment.installing.init-d.securing]]
=== Securing an init.d Service
NOTE: The following is a set of guidelines on how to secure a Spring Boot application that runs as an init.d service.
It is not intended to be an exhaustive list of everything that should be done to harden an application and the environment in which it runs.
When executed as root, as is the case when root is being used to start an init.d service, the default executable script runs the application as the user specified in the `RUN_AS_USER` environment variable.
When the environment variable is not set, the user who owns the jar file is used instead.
You should never run a Spring Boot application as `root`, so `RUN_AS_USER` should never be root and your application's jar file should never be owned by root.
Instead, create a specific user to run your application and set the `RUN_AS_USER` environment variable or use `chown` to make it the owner of the jar file, as shown in the following example:
[source,shell]
----
$ chown bootapp:bootapp your-app.jar
----
In this case, the default executable script runs the application as the `bootapp` user.
TIP: To reduce the chances of the application's user account being compromised, you should consider preventing it from using a login shell.
For example, you can set the account's shell to `/usr/sbin/nologin`.
You should also take steps to prevent the modification of your application's jar file.
Firstly, configure its permissions so that it cannot be written and can only be read or executed by its owner, as shown in the following example:
[source,shell]
----
$ chmod 500 your-app.jar
----
Second, you should also take steps to limit the damage if your application or the account that is running it is compromised.
If an attacker does gain access, they could make the jar file writable and change its contents.
One way to protect against this is to make it immutable by using `chattr`, as shown in the following example:
[source,shell]
----
$ sudo chattr +i your-app.jar
----
This will prevent any user, including root, from modifying the jar.
If root is used to control the application's service and you xref:deployment/installing.adoc#howto.deployment.installing.init-d.script-customization.when-running.conf-file[use a `.conf` file] to customize its startup, the `.conf` file is read and evaluated by the root user.
It should be secured accordingly.
Use `chmod` so that the file can only be read by the owner and use `chown` to make root the owner, as shown in the following example:
[source,shell]
----
$ chmod 400 your-app.conf
$ sudo chown root:root your-app.conf
----
[[howto.deployment.installing.init-d.script-customization]]
=== Customizing the Startup Script
The default embedded startup script written by the Maven or Gradle plugin can be customized in a number of ways.
For most people, using the default script along with a few customizations is usually enough.
If you find you cannot customize something that you need to, use the `embeddedLaunchScript` option to write your own file entirely.
[[howto.deployment.installing.init-d.script-customization.when-written]]
==== Customizing the Start Script When It Is Written
It often makes sense to customize elements of the start script as it is written into the jar file.
For example, init.d scripts can provide a "`description`".
Since you know the description up front (and it need not change), you may as well provide it when the jar is generated.
To customize written elements, use the `embeddedLaunchScriptProperties` option of the Spring Boot Maven plugin or the xref:gradle-plugin:packaging.adoc#packaging-executable.configuring.launch-script[`properties` property of the Spring Boot Gradle plugin's `launchScript`].
The following property substitutions are supported with the default script:
[cols="1,3,3,3"]
|===
| Name | Description | Gradle default | Maven default
| `mode`
| The script mode.
| `auto`
| `auto`
| `initInfoProvides`
| The `Provides` section of "`INIT INFO`"
| `${task.baseName}`
| `${project.artifactId}`
| `initInfoRequiredStart`
| `Required-Start` section of "`INIT INFO`".
| `$remote_fs $syslog $network`
| `$remote_fs $syslog $network`
| `initInfoRequiredStop`
| `Required-Stop` section of "`INIT INFO`".
| `$remote_fs $syslog $network`
| `$remote_fs $syslog $network`
| `initInfoDefaultStart`
| `Default-Start` section of "`INIT INFO`".
| `2 3 4 5`
| `2 3 4 5`
| `initInfoDefaultStop`
| `Default-Stop` section of "`INIT INFO`".
| `0 1 6`
| `0 1 6`
| `initInfoShortDescription`
| `Short-Description` section of "`INIT INFO`".
| Single-line version of `${project.description}` (falling back to `${task.baseName}`)
| `${project.name}`
| `initInfoDescription`
| `Description` section of "`INIT INFO`".
| `${project.description}` (falling back to `${task.baseName}`)
| `${project.description}` (falling back to `${project.name}`)
| `initInfoChkconfig`
| `chkconfig` section of "`INIT INFO`"
| `2345 99 01`
| `2345 99 01`
| `confFolder`
| The default value for `CONF_FOLDER`
| Folder containing the jar
| Folder containing the jar
| `inlinedConfScript`
| Reference to a file script that should be inlined in the default launch script.
This can be used to set environmental variables such as `JAVA_OPTS` before any external config files are loaded
|
|
| `logFolder`
| Default value for `LOG_FOLDER`.
Only valid for an `init.d` service
|
|
| `logFilename`
| Default value for `LOG_FILENAME`.
Only valid for an `init.d` service
|
|
| `pidFolder`
| Default value for `PID_FOLDER`.
Only valid for an `init.d` service
|
|
| `pidFilename`
| Default value for the name of the PID file in `PID_FOLDER`.
Only valid for an `init.d` service
|
|
| `useStartStopDaemon`
| Whether the `start-stop-daemon` command, when it is available, should be used to control the process
| `true`
| `true`
| `stopWaitTime`
| Default value for `STOP_WAIT_TIME` in seconds.
Only valid for an `init.d` service
| 60
| 60
|===
[[howto.deployment.installing.init-d.script-customization.when-running]]
==== Customizing a Script When It Runs
For items of the script that need to be customized _after_ the jar has been written, you can use environment variables or a xref:deployment/installing.adoc#howto.deployment.installing.init-d.script-customization.when-running.conf-file[config file].
The following environment properties are supported with the default script:
[cols="1,6"]
|===
| Variable | Description
| `MODE`
| The "`mode`" of operation.
The default depends on the way the jar was built but is usually `auto` (meaning it tries to guess if it is an init script by checking if it is a symlink in a directory called `init.d`).
You can explicitly set it to `service` so that the `stop\|start\|status\|restart` commands work or to `run` if you want to run the script in the foreground.
| `RUN_AS_USER`
| The user that will be used to run the application.
When not set, the user that owns the jar file will be used.
| `USE_START_STOP_DAEMON`
| Whether the `start-stop-daemon` command, when it is available, should be used to control the process.
Defaults to `true`.
| `PID_FOLDER`
| The root name of the pid folder (`/var/run` by default).
| `LOG_FOLDER`
| The name of the folder in which to put log files (`/var/log` by default).
| `CONF_FOLDER`
| The name of the folder from which to read .conf files (same folder as jar-file by default).
| `LOG_FILENAME`
| The name of the log file in the `LOG_FOLDER` (`<appname>.log` by default).
| `APP_NAME`
| The name of the app.
If the jar is run from a symlink, the script guesses the app name.
If it is not a symlink or you want to explicitly set the app name, this can be useful.
| `RUN_ARGS`
| The arguments to pass to the program (the Spring Boot app).
| `JAVA_HOME`
| The location of the `java` executable is discovered by using the `PATH` by default, but you can set it explicitly if there is an executable file at `$JAVA_HOME/bin/java`.
| `JAVA_OPTS`
| Options that are passed to the JVM when it is launched.
| `JARFILE`
| The explicit location of the jar file, in case the script is being used to launch a jar that it is not actually embedded.
| `DEBUG`
| If not empty, sets the `-x` flag on the shell process, allowing you to see the logic in the script.
| `STOP_WAIT_TIME`
| The time in seconds to wait when stopping the application before forcing a shutdown (`60` by default).
|===
NOTE: The `PID_FOLDER`, `LOG_FOLDER`, and `LOG_FILENAME` variables are only valid for an `init.d` service.
For `systemd`, the equivalent customizations are made by using the '`service`' script.
See the https://www.freedesktop.org/software/systemd/man/systemd.service.html[service unit configuration man page] for more details.
[[howto.deployment.installing.init-d.script-customization.when-running.conf-file]]
===== Using a Conf File
With the exception of `JARFILE` and `APP_NAME`, the settings listed in the preceding section can be configured by using a `.conf` file.
The file is expected to be next to the jar file and have the same name but suffixed with `.conf` rather than `.jar`.
For example, a jar named `/var/myapp/myapp.jar` uses the configuration file named `/var/myapp/myapp.conf`, as shown in the following example:
.myapp.conf
[source,properties]
----
JAVA_OPTS=-Xmx1024M
LOG_FOLDER=/custom/log/folder
----
TIP: If you do not like having the config file next to the jar file, you can set a `CONF_FOLDER` environment variable to customize the location of the config file.
To learn about securing this file appropriately, see xref:deployment/installing.adoc#howto.deployment.installing.init-d.securing[the guidelines for securing an init.d service].
[[howto.deployment.installing.windows-services]]
== Microsoft Windows Services
@@ -5,9 +5,6 @@ While it is possible to convert a Spring Boot uber jar into a Docker image with
When you create a jar containing the layers index file, the `spring-boot-jarmode-tools` jar will be added as a dependency to your jar.
With this jar on the classpath, you can launch your application in a special mode which allows the bootstrap code to run something entirely different from your application, for example, something that extracts the layers.
CAUTION: The `tools` mode can not be used with a xref:how-to:deployment/installing.adoc[fully executable Spring Boot archive] that includes a launch script.
Disable launch script configuration when building a jar file that is intended to be used with the `extract` tools mode command.
Heres how you can launch your jar with a `tools` jar mode:
[source,shell]
@@ -1,61 +0,0 @@
= Spring Boot Launch Script Tests
This module contains integration tests for the default launch script that is used
to make a jar file fully executable on Linux. The tests use Docker to verify the
functionality in a variety of Linux distributions.
== Setting up Docker
The setup that's required varies depending on your operating system.
=== Docker on OS X
Install Docker for Mac. See the https://docs.docker.com/docker-for-mac/install/[macOS
installation instructions] for details.
=== Docker on Linux
Install Docker as appropriate for your Linux distribution. See the
https://docs.docker.com/engine/installation/[Linux installation instructions] for more
information.
Next, add your user to the `docker` group. For example:
----
$ sudo usermod -a -G docker awilkinson
----
You may need to log out and back in again for this change to take effect and for your
user to be able to connect to the daemon.
== Running the tests
You're now ready to run the tests. Assuming that you're in the same directory as this
README, the tests can be launched as follows:
----
$ gradle intTest
----
The first time the tests are run, Docker will create the container images that are used to
run the tests. This can take several minutes, particularly if you have a slow network
connection. Subsequent runs will be faster as the images are cached locally. You can run
`docker images` to see a list of the cached images. Images created by these tests will be
tagged with `spring-boot-it` prefix to easily distinguish them.
== Cleaning up
If you want to reclaim the disk space used by the cached images (at the expense of having
to wait for them to be downloaded and rebuilt the next time you run the tests), you can
use `docker images` to list the images and `docker rmi <image>` to delete them (look for
`spring-boot-it` tag). See `docker rmi --help` for further details.
@@ -1,81 +0,0 @@
/*
* Copyright 2012-present 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.
*/
plugins {
id "java"
id "org.springframework.boot.docker-test"
id "de.undercouch.download"
}
description = "Spring Boot Launch Script Integration Tests"
def jdkVersion = "17.0.11+10"
def jdkArch = "aarch64".equalsIgnoreCase(System.getProperty("os.arch")) ? "aarch64" : "amd64"
configurations {
app
}
dependencies {
app project(path: ":platform:spring-boot-dependencies", configuration: "mavenRepository")
app project(path: ":build-plugin:spring-boot-gradle-plugin", configuration: "mavenRepository")
dockerTestImplementation(project(":test-support:spring-boot-docker-test-support"))
dockerTestImplementation(project(":starter:spring-boot-starter-test"))
dockerTestImplementation("org.testcontainers:testcontainers")
}
tasks.register("syncMavenRepository", Sync) {
from configurations.app
into layout.buildDirectory.dir("docker-test-maven-repository")
}
tasks.register("syncAppSource", org.springframework.boot.build.SyncAppSource) {
sourceDirectory = file("spring-boot-launch-script-tests-app")
destinationDirectory = file(layout.buildDirectory.dir("spring-boot-launch-script-tests-app"))
}
tasks.register("buildApp", GradleBuild) {
dependsOn syncAppSource, syncMavenRepository
dir = layout.buildDirectory.dir("spring-boot-launch-script-tests-app")
startParameter.buildCacheEnabled = false
tasks = ["build"]
}
tasks.register("downloadJdk", Download) {
def destFolder = new File(project.gradle.gradleUserHomeDir, "caches/springboot/downloads/jdk/bellsoft")
destFolder.mkdirs()
src "https://download.bell-sw.com/java/${jdkVersion}/bellsoft-jdk${jdkVersion}-linux-${jdkArch}.tar.gz"
dest destFolder
tempAndMove true
overwrite false
retries 3
}
tasks.register("syncJdkDownloads", Sync) {
dependsOn downloadJdk
from "${project.gradle.gradleUserHomeDir}/caches/springboot/downloads/jdk/bellsoft/"
include "bellsoft-jdk${jdkVersion}-linux-${jdkArch}.tar.gz"
into layout.buildDirectory.dir("downloads/jdk/bellsoft/")
}
tasks.named("processDockerTestResources").configure {
dependsOn syncJdkDownloads
}
tasks.named("dockerTest").configure {
dependsOn buildApp
}
@@ -1,40 +0,0 @@
/*
* Copyright 2012-present 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.
*/
plugins {
id "java"
id "org.springframework.boot"
}
java {
sourceCompatibility = '17'
targetCompatibility = '17'
}
repositories {
maven { url = layout.projectDirectory.dir("../docker-test-maven-repository") }
mavenCentral()
spring.mavenRepositories()
}
dependencies {
implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))
implementation("org.apache.tomcat.embed:tomcat-embed-core")
}
bootJar {
launchScript()
}
@@ -1,31 +0,0 @@
/*
* Copyright 2012-present 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.
*/
pluginManagement {
evaluate(new File("${gradle.parent.rootProject.rootDir}/buildSrc/SpringRepositorySupport.groovy")).apply(this)
repositories {
maven { url = layout.settingsDirectory.dir("../docker-test-maven-repository") }
mavenCentral()
spring.mavenRepositories()
}
resolutionStrategy {
eachPlugin {
if (requested.id.id == "org.springframework.boot") {
useModule "org.springframework.boot:spring-boot-gradle-plugin:${requested.version}"
}
}
}
}
@@ -1,88 +0,0 @@
/*
* Copyright 2012-present 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.launchscript;
import java.io.IOException;
import java.net.URL;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.startup.Tomcat;
public class LaunchScriptTestApplication {
public static void main(String[] args) throws LifecycleException {
System.out.println("Starting " + LaunchScriptTestApplication.class.getSimpleName() + " (" + findSource() + ")");
Tomcat tomcat = new Tomcat();
tomcat.getConnector().setPort(getPort(args));
Context context = tomcat.addContext(getContextPath(args), null);
tomcat.addServlet(context.getPath(), "test", new HttpServlet() {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.getWriter().println("Launched");
}
});
context.addServletMappingDecoded("/", "test");
tomcat.start();
}
private static URL findSource() {
try {
ProtectionDomain domain = LaunchScriptTestApplication.class.getProtectionDomain();
CodeSource codeSource = (domain != null) ? domain.getCodeSource() : null;
return (codeSource != null) ? codeSource.getLocation() : null;
}
catch (Exception ex) {
}
return null;
}
private static int getPort(String[] args) {
String port = getProperty(args, "server.port");
return (port != null) ? Integer.parseInt(port) : 8080;
}
private static String getContextPath(String[] args) {
String contextPath = getProperty(args, "server.servlet.context-path");
return (contextPath != null) ? contextPath : "";
}
private static String getProperty(String[] args, String property) {
String value = System.getProperty(property);
if (value != null) {
return value;
}
String prefix = "--" + property + "=";
for (String arg : args) {
if (arg.startsWith(prefix)) {
return arg.substring(prefix.length());
}
}
return null;
}
}
@@ -1,137 +0,0 @@
/*
* Copyright 2012-present 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.launchscript;
import java.io.File;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.function.Predicate;
import org.assertj.core.api.Condition;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.containers.output.ToStringConsumer;
import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy;
import org.testcontainers.images.builder.ImageFromDockerfile;
import org.testcontainers.utility.MountableFile;
import org.springframework.boot.ansi.AnsiColor;
import org.springframework.util.Assert;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
/**
* Abstract base class for testing the launch script.
*
* @author Andy Wilkinson
* @author Ali Shahbour
* @author Alexey Vinogradov
* @author Moritz Halbritter
*/
abstract class AbstractLaunchScriptIntegrationTests {
protected static final char ESC = 27;
private final String scriptsDir;
protected AbstractLaunchScriptIntegrationTests(String scriptsDir) {
this.scriptsDir = scriptsDir;
}
static List<Object[]> filterParameters(Predicate<File> osFilter) {
List<Object[]> parameters = new ArrayList<>();
for (File os : new File("src/dockerTest/resources/conf").listFiles()) {
if (osFilter.test(os)) {
for (File version : os.listFiles()) {
parameters.add(new Object[] { os.getName(), version.getName() });
}
}
}
return parameters;
}
protected Condition<String> coloredString(AnsiColor color, String string) {
String colorString = ESC + "[0;" + color + "m" + string + ESC + "[0m";
return new Condition<>() {
@Override
public boolean matches(String value) {
return containsString(colorString).matches(value);
}
};
}
protected void doLaunch(String os, String version, String script) throws Exception {
assertThat(doTest(os, version, script)).contains("Launched");
}
protected String doTest(String os, String version, String script) throws Exception {
ToStringConsumer consumer = new ToStringConsumer().withRemoveAnsiCodes(false);
try (LaunchScriptTestContainer container = new LaunchScriptTestContainer(os, version, this.scriptsDir,
script)) {
container.withLogConsumer(consumer);
container.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger("docker")));
container.start();
while (container.isRunning()) {
Thread.sleep(100);
}
}
return consumer.toUtf8String();
}
private static final class LaunchScriptTestContainer extends GenericContainer<LaunchScriptTestContainer> {
private LaunchScriptTestContainer(String os, String version, String scriptsDir, String testScript) {
super(createImage(os, version));
withCopyFileToContainer(MountableFile.forHostPath(findApplication().getAbsolutePath()), "/app.jar");
withCopyFileToContainer(
MountableFile.forHostPath("src/dockerTest/resources/scripts/" + scriptsDir + "test-functions.sh"),
"/test-functions.sh");
withCopyFileToContainer(
MountableFile.forHostPath("src/dockerTest/resources/scripts/" + scriptsDir + 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)));
}
private static ImageFromDockerfile createImage(String os, String version) {
ImageFromDockerfile image = new ImageFromDockerfile(
"spring-boot-launch-script/" + os.toLowerCase(Locale.ROOT) + "-" + version);
image.withFileFromFile("Dockerfile",
new File("src/dockerTest/resources/conf/" + os + "/" + version + "/Dockerfile"));
for (File file : new File("build/downloads/jdk/bellsoft").listFiles()) {
image.withFileFromFile("downloads/" + file.getName(), file);
}
return image;
}
private static File findApplication() {
String name = String.format("build/%1$s/build/libs/%1$s.jar", "spring-boot-launch-script-tests-app");
File jar = new File(name);
Assert.state(jar.isFile(), () -> "Could not find " + name + ". Have you built it?");
return jar;
}
}
}
@@ -1,102 +0,0 @@
/*
* Copyright 2012-present 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.launchscript;
import java.util.List;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.boot.testsupport.container.DisabledIfDockerUnavailable;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests of Spring Boot's launch script when executing the jar directly.
*
* @author Alexey Vinogradov
* @author Andy Wilkinson
*/
@DisabledIfDockerUnavailable
class JarLaunchScriptIntegrationTests extends AbstractLaunchScriptIntegrationTests {
JarLaunchScriptIntegrationTests() {
super("jar/");
}
static List<Object[]> parameters() {
return filterParameters((file) -> true);
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void basicLaunch(String os, String version) throws Exception {
doLaunch(os, version, "basic-launch.sh");
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void launchWithDebugEnv(String os, String version) throws Exception {
final String output = doTest(os, version, "launch-with-debug.sh");
assertThat(output).contains("++ pwd");
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void launchWithDifferentJarFileEnv(String os, String version) throws Exception {
final String output = doTest(os, version, "launch-with-jarfile.sh");
assertThat(output).contains("app-another.jar");
assertThat(output).doesNotContain("spring-boot-launch-script-tests.jar");
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void launchWithSingleCommandLineArgument(String os, String version) throws Exception {
doLaunch(os, version, "launch-with-single-command-line-argument.sh");
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void launchWithMultipleCommandLineArguments(String os, String version) throws Exception {
doLaunch(os, version, "launch-with-multiple-command-line-arguments.sh");
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void launchWithSingleRunArg(String os, String version) throws Exception {
doLaunch(os, version, "launch-with-single-run-arg.sh");
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void launchWithMultipleRunArgs(String os, String version) throws Exception {
doLaunch(os, version, "launch-with-multiple-run-args.sh");
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void launchWithSingleJavaOpt(String os, String version) throws Exception {
doLaunch(os, version, "launch-with-single-java-opt.sh");
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void launchWithMultipleJavaOpts(String os, String version) throws Exception {
doLaunch(os, version, "launch-with-multiple-java-opts.sh");
}
}
@@ -1,291 +0,0 @@
/*
* Copyright 2012-present 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.launchscript;
import java.util.List;
import java.util.regex.Pattern;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.boot.ansi.AnsiColor;
import org.springframework.boot.testsupport.container.DisabledIfDockerUnavailable;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for Spring Boot's launch script on OSs that use SysVinit.
*
* @author Andy Wilkinson
* @author Ali Shahbour
* @author Alexey Vinogradov
* @author Moritz Halbritter
*/
@DisabledIfDockerUnavailable
class SysVinitLaunchScriptIntegrationTests extends AbstractLaunchScriptIntegrationTests {
SysVinitLaunchScriptIntegrationTests() {
super("init.d/");
}
static List<Object[]> parameters() {
return filterParameters((file) -> !file.getName().contains("RedHat"));
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void statusWhenStopped(String os, String version) throws Exception {
String output = doTest(os, version, "status-when-stopped.sh");
assertThat(output).contains("Status: 3");
assertThat(output).has(coloredString(AnsiColor.RED, "Not running"));
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void statusWhenStarted(String os, String version) throws Exception {
String output = doTest(os, version, "status-when-started.sh");
assertThat(output).contains("Status: 0");
assertThat(output).has(coloredString(AnsiColor.GREEN, "Started [" + extractPid(output) + "]"));
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void statusWhenKilled(String os, String version) throws Exception {
String output = doTest(os, version, "status-when-killed.sh");
assertThat(output).contains("Status: 1");
assertThat(output)
.has(coloredString(AnsiColor.RED, "Not running (process " + extractPid(output) + " not found)"));
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void stopWhenStopped(String os, String version) throws Exception {
String output = doTest(os, version, "stop-when-stopped.sh");
assertThat(output).contains("Status: 0");
assertThat(output).has(coloredString(AnsiColor.YELLOW, "Not running (pidfile not found)"));
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void forceStopWhenStopped(String os, String version) throws Exception {
String output = doTest(os, version, "force-stop-when-stopped.sh");
assertThat(output).contains("Status: 0");
assertThat(output).has(coloredString(AnsiColor.YELLOW, "Not running (pidfile not found)"));
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void startWhenStarted(String os, String version) throws Exception {
String output = doTest(os, version, "start-when-started.sh");
assertThat(output).contains("Status: 0");
assertThat(output).has(coloredString(AnsiColor.YELLOW, "Already running [" + extractPid(output) + "]"));
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void restartWhenStopped(String os, String version) throws Exception {
String output = doTest(os, version, "restart-when-stopped.sh");
assertThat(output).contains("Status: 0");
assertThat(output).has(coloredString(AnsiColor.YELLOW, "Not running (pidfile not found)"));
assertThat(output).has(coloredString(AnsiColor.GREEN, "Started [" + extractPid(output) + "]"));
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void restartWhenStarted(String os, String version) throws Exception {
String output = doTest(os, version, "restart-when-started.sh");
assertThat(output).contains("Status: 0");
assertThat(output).has(coloredString(AnsiColor.GREEN, "Started [" + extract("PID1", output) + "]"));
assertThat(output).has(coloredString(AnsiColor.GREEN, "Stopped [" + extract("PID1", output) + "]"));
assertThat(output).has(coloredString(AnsiColor.GREEN, "Started [" + extract("PID2", output) + "]"));
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void startWhenStopped(String os, String version) throws Exception {
String output = doTest(os, version, "start-when-stopped.sh");
assertThat(output).contains("Status: 0");
assertThat(output).has(coloredString(AnsiColor.GREEN, "Started [" + extractPid(output) + "]"));
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void basicLaunch(String os, String version) throws Exception {
String output = doTest(os, version, "basic-launch.sh");
assertThat(output).doesNotContain("PID_FOLDER");
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void launchWithMissingLogFolderGeneratesAWarning(String os, String version) throws Exception {
String output = doTest(os, version, "launch-with-missing-log-folder.sh");
assertThat(output)
.has(coloredString(AnsiColor.YELLOW, "LOG_FOLDER /does/not/exist does not exist. Falling back to /tmp"));
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void launchWithMissingPidFolderGeneratesAWarning(String os, String version) throws Exception {
String output = doTest(os, version, "launch-with-missing-pid-folder.sh");
assertThat(output)
.has(coloredString(AnsiColor.YELLOW, "PID_FOLDER /does/not/exist does not exist. Falling back to /tmp"));
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void launchWithSingleCommandLineArgument(String os, String version) throws Exception {
doLaunch(os, version, "launch-with-single-command-line-argument.sh");
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void launchWithMultipleCommandLineArguments(String os, String version) throws Exception {
doLaunch(os, version, "launch-with-multiple-command-line-arguments.sh");
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void launchWithSingleRunArg(String os, String version) throws Exception {
doLaunch(os, version, "launch-with-single-run-arg.sh");
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void launchWithMultipleRunArgs(String os, String version) throws Exception {
doLaunch(os, version, "launch-with-multiple-run-args.sh");
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void launchWithSingleJavaOpt(String os, String version) throws Exception {
doLaunch(os, version, "launch-with-single-java-opt.sh");
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void launchWithDoubleLinkSingleJavaOpt(String os, String version) throws Exception {
doLaunch(os, version, "launch-with-double-link-single-java-opt.sh");
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void launchWithMultipleJavaOpts(String os, String version) throws Exception {
doLaunch(os, version, "launch-with-multiple-java-opts.sh");
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void launchWithUseOfStartStopDaemonDisabled(String os, String version) throws Exception {
doLaunch(os, version, "launch-with-use-of-start-stop-daemon-disabled.sh");
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void launchWithRelativePidFolder(String os, String version) throws Exception {
String output = doTest(os, version, "launch-with-relative-pid-folder.sh");
assertThat(output).has(coloredString(AnsiColor.GREEN, "Started [" + extractPid(output) + "]"));
assertThat(output).has(coloredString(AnsiColor.GREEN, "Running [" + extractPid(output) + "]"));
assertThat(output).has(coloredString(AnsiColor.GREEN, "Stopped [" + extractPid(output) + "]"));
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void pidFolderOwnership(String os, String version) throws Exception {
String output = doTest(os, version, "pid-folder-ownership.sh");
assertThat(output).contains("phil root");
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void pidFileOwnership(String os, String version) throws Exception {
String output = doTest(os, version, "pid-file-ownership.sh");
assertThat(output).contains("phil root");
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void logFileOwnership(String os, String version) throws Exception {
String output = doTest(os, version, "log-file-ownership.sh");
assertThat(output).contains("phil root");
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void logFileOwnershipIsChangedWhenCreated(String os, String version) throws Exception {
String output = doTest(os, version, "log-file-ownership-is-changed-when-created.sh");
assertThat(output).contains("andy root");
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void logFileOwnershipIsUnchangedWhenExists(String os, String version) throws Exception {
String output = doTest(os, version, "log-file-ownership-is-unchanged-when-exists.sh");
assertThat(output).contains("root root");
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void launchWithRelativeLogFolder(String os, String version) throws Exception {
String output = doTest(os, version, "launch-with-relative-log-folder.sh");
assertThat(output).contains("Log written");
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void launchWithRunAsUser(String os, String version) throws Exception {
String output = doTest(os, version, "launch-with-run-as-user.sh");
assertThat(output).contains("wagner root");
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void whenRunAsUserDoesNotExistLaunchFailsWithInvalidArgument(String os, String version) throws Exception {
String output = doTest(os, version, "launch-with-run-as-invalid-user.sh");
assertThat(output).contains("Status: 2");
assertThat(output).has(coloredString(AnsiColor.RED, "Cannot run as 'johndoe': no such user"));
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void whenJarOwnerAndRunAsUserAreBothSpecifiedRunAsUserTakesPrecedence(String os, String version) throws Exception {
String output = doTest(os, version, "launch-with-run-as-user-preferred-to-jar-owner.sh");
assertThat(output).contains("wagner root");
}
@ParameterizedTest(name = "{0} {1}")
@MethodSource("parameters")
void whenLaunchedUsingNonRootUserWithRunAsUserSpecifiedLaunchFailsWithInsufficientPrivilege(String os,
String version) throws Exception {
String output = doTest(os, version, "launch-with-run-as-user-root-required.sh");
assertThat(output).contains("Status: 4");
assertThat(output).has(coloredString(AnsiColor.RED, "Cannot run as 'wagner': current user is not root"));
}
private String extractPid(String output) {
return extract("PID", output);
}
private String extract(String label, String output) {
Pattern pattern = Pattern.compile(".*" + label + ": ([0-9]+).*", Pattern.DOTALL);
java.util.regex.Matcher matcher = pattern.matcher(output);
if (matcher.matches()) {
return matcher.group(1);
}
throw new IllegalArgumentException("Failed to extract " + label + " from output: " + output);
}
}
@@ -1,10 +0,0 @@
FROM redhat/ubi9:9.3-1476 as prepare
COPY downloads/* /opt/download/
RUN mkdir -p /opt/jdk && \
cd /opt/jdk && \
tar xzf /opt/download/* --strip-components=1
FROM redhat/ubi9:9.3-1476
COPY --from=prepare /opt/jdk /opt/jdk
ENV JAVA_HOME /opt/jdk
ENV PATH $JAVA_HOME/bin:$PATH
@@ -1,11 +0,0 @@
FROM ubuntu:jammy-20240405 as prepare
COPY downloads/* /opt/download/
RUN mkdir -p /opt/jdk && \
cd /opt/jdk && \
tar xzf /opt/download/* --strip-components=1
FROM ubuntu:jammy-20240405
RUN apt-get update && apt-get install -y software-properties-common curl
COPY --from=prepare /opt/jdk /opt/jdk
ENV JAVA_HOME /opt/jdk
ENV PATH $JAVA_HOME/bin:$PATH
@@ -1,11 +0,0 @@
FROM ubuntu:noble-20250404 as prepare
COPY downloads/* /opt/download/
RUN mkdir -p /opt/jdk && \
cd /opt/jdk && \
tar xzf /opt/download/* --strip-components=1
FROM ubuntu:noble-20250404
RUN apt-get update && apt-get install -y software-properties-common curl
COPY --from=prepare /opt/jdk /opt/jdk
ENV JAVA_HOME /opt/jdk
ENV PATH $JAVA_HOME/bin:$PATH
@@ -1,10 +0,0 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
@@ -1,5 +0,0 @@
source ./test-functions.sh
install_service
start_service
await_app
curl -s http://127.0.0.1:8080/
@@ -1,4 +0,0 @@
source ./test-functions.sh
install_service
force_stop_service
echo "Status: $?"
@@ -1,6 +0,0 @@
source ./test-functions.sh
install_double_link_service
echo 'JAVA_OPTS=-Dserver.port=8081' > /test-service/spring-boot-app.conf
start_service
await_app http://127.0.0.1:8081/
curl -s http://127.0.0.1:8081/
@@ -1,6 +0,0 @@
source ./test-functions.sh
install_service
echo 'LOG_FOLDER=/does/not/exist' > /test-service/spring-boot-app.conf
start_service
await_app
curl -s http://127.0.0.1:8080/
@@ -1,6 +0,0 @@
source ./test-functions.sh
install_service
echo 'PID_FOLDER=/does/not/exist' > /test-service/spring-boot-app.conf
start_service
await_app
curl -s http://127.0.0.1:8080/
@@ -1,5 +0,0 @@
source ./test-functions.sh
install_service
start_service --server.port=8081 --server.servlet.context-path=/test
await_app http://127.0.0.1:8081/test/
curl -s http://127.0.0.1:8081/test/
@@ -1,6 +0,0 @@
source ./test-functions.sh
install_service
echo 'JAVA_OPTS="-Dserver.port=8081 -Dserver.servlet.context-path=/test"' > /test-service/spring-boot-app.conf
start_service
await_app http://127.0.0.1:8081/test/
curl -s http://127.0.0.1:8081/test/
@@ -1,6 +0,0 @@
source ./test-functions.sh
install_service
echo 'RUN_ARGS="--server.port=8081 --server.servlet.context-path=/test"' > /test-service/spring-boot-app.conf
start_service
await_app http://127.0.0.1:8081/test/
curl -s http://127.0.0.1:8081/test/
@@ -1,8 +0,0 @@
source ./test-functions.sh
mkdir ./pid
install_service
echo 'LOG_FOLDER=log' > /test-service/spring-boot-app.conf
mkdir -p /test-service/log
start_service
await_app
[[ -s /test-service/log/spring-boot-app.log ]] && echo "Log written"
@@ -1,10 +0,0 @@
source ./test-functions.sh
install_service
mkdir /test-service/pid
echo 'PID_FOLDER=pid' > /test-service/spring-boot-app.conf
start_service
echo "PID: $(cat /test-service/pid/spring-boot-app/spring-boot-app.pid)"
await_app
curl -s http://127.0.0.1:8080/
status_service
stop_service
@@ -1,7 +0,0 @@
source ./test-functions.sh
install_service
echo 'RUN_AS_USER=johndoe' > /test-service/spring-boot-app.conf
start_service
echo "Status: $?"
@@ -1,13 +0,0 @@
source ./test-functions.sh
install_service
useradd wagner
echo 'RUN_AS_USER=wagner' > /test-service/spring-boot-app.conf
useradd phil
chown phil /test-service/spring-boot-app.jar
start_service
await_app
ls -la /var/log/spring-boot-app.log
@@ -1,9 +0,0 @@
source ./test-functions.sh
install_service
useradd wagner
echo 'RUN_AS_USER=wagner' > /test-service/spring-boot-app.conf
echo "JAVA_HOME='$JAVA_HOME'" >> /test-service/spring-boot-app.conf
su - wagner -c "$(which service) spring-boot-app start"
echo "Status: $?"
@@ -1,10 +0,0 @@
source ./test-functions.sh
install_service
useradd wagner
echo 'RUN_AS_USER=wagner' > /test-service/spring-boot-app.conf
start_service
await_app
ls -la /var/log/spring-boot-app.log
@@ -1,5 +0,0 @@
source ./test-functions.sh
install_service
start_service --server.port=8081
await_app http://127.0.0.1:8081/
curl -s http://127.0.0.1:8081/
@@ -1,6 +0,0 @@
source ./test-functions.sh
install_service
echo 'JAVA_OPTS=-Dserver.port=8081' > /test-service/spring-boot-app.conf
start_service
await_app http://127.0.0.1:8081/
curl -s http://127.0.0.1:8081/
@@ -1,6 +0,0 @@
source ./test-functions.sh
install_service
echo 'RUN_ARGS=--server.port=8081' > /test-service/spring-boot-app.conf
start_service
await_app http://127.0.0.1:8081/
curl -s http://127.0.0.1:8081/
@@ -1,7 +0,0 @@
source ./test-functions.sh
chmod -x $(type -p start-stop-daemon)
install_service
echo 'USE_START_STOP_DAEMON=false' > /test-service/spring-boot-app.conf
start_service
await_app
curl -s http://127.0.0.1:8080/
@@ -1,9 +0,0 @@
source ./test-functions.sh
install_service
echo 'LOG_FOLDER=log' > /test-service/spring-boot-app.conf
mkdir -p /test-service/log
useradd andy
chown andy /test-service/spring-boot-app.jar
start_service
await_app
ls -al /test-service/log/spring-boot-app.log
@@ -1,11 +0,0 @@
source ./test-functions.sh
install_service
echo 'LOG_FOLDER=log' > /test-service/spring-boot-app.conf
mkdir -p /test-service/log
touch /test-service/log/spring-boot-app.log
chmod a+w /test-service/log/spring-boot-app.log
useradd andy
chown andy /test-service/spring-boot-app.jar
start_service
await_app
ls -al /test-service/log/spring-boot-app.log
@@ -1,20 +0,0 @@
source ./test-functions.sh
install_service
chmod o+w /var/log
useradd phil
mkdir /phil-files
chown phil /phil-files
useradd andy
chown andy /test-service/spring-boot-app.jar
start_service
stop_service
su - andy -c "ln -s -f /phil-files /var/log/spring-boot-app.log"
start_service
ls -ld /phil-files
@@ -1,18 +0,0 @@
source ./test-functions.sh
install_service
useradd phil
mkdir /phil-files
chown phil /phil-files
useradd andy
chown andy /test-service/spring-boot-app.jar
start_service
stop_service
su - andy -c "ln -s /phil-files /var/run/spring-boot-app/spring-boot-app.pid"
start_service
ls -ld /phil-files
@@ -1,17 +0,0 @@
source ./test-functions.sh
install_service
chmod o+w /var/run
useradd phil
mkdir /phil-files
chown phil /phil-files
useradd andy
chown andy /test-service/spring-boot-app.jar
su - andy -c "ln -s -f /phil-files /var/run/spring-boot-app"
start_service
ls -ld /phil-files
@@ -1,7 +0,0 @@
source ./test-functions.sh
install_service
start_service
echo "PID1: $(cat /var/run/spring-boot-app/spring-boot-app.pid)"
restart_service
echo "Status: $?"
echo "PID2: $(cat /var/run/spring-boot-app/spring-boot-app.pid)"
@@ -1,5 +0,0 @@
source ./test-functions.sh
install_service
restart_service
echo "Status: $?"
echo "PID: $(cat /var/run/spring-boot-app/spring-boot-app.pid)"
@@ -1,6 +0,0 @@
source ./test-functions.sh
install_service
start_service
echo "PID: $(cat /var/run/spring-boot-app/spring-boot-app.pid)"
start_service
echo "Status: $?"
@@ -1,5 +0,0 @@
source ./test-functions.sh
install_service
start_service
echo "Status: $?"
echo "PID: $(cat /var/run/spring-boot-app/spring-boot-app.pid)"
@@ -1,8 +0,0 @@
source ./test-functions.sh
install_service
start_service
pid=$(cat /var/run/spring-boot-app/spring-boot-app.pid)
echo "PID: $pid"
kill -9 $pid
status_service
echo "Status: $?"
@@ -1,6 +0,0 @@
source ./test-functions.sh
install_service
start_service
status_service
echo "Status: $?"
echo "PID: $(cat /var/run/spring-boot-app/spring-boot-app.pid)"
@@ -1,4 +0,0 @@
source ./test-functions.sh
install_service
status_service
echo "Status: $?"
@@ -1,4 +0,0 @@
source ./test-functions.sh
install_service
stop_service
echo "Status: $?"
@@ -1,53 +0,0 @@
await_app() {
if [ -z $1 ]
then
url=http://127.0.0.1:8080
else
url=$1
fi
end=$(date +%s)
let "end+=30"
until curl -s $url > /dev/null
do
now=$(date +%s)
if [[ $now -ge $end ]]; then
break
fi
sleep 1
done
}
install_service() {
mkdir /test-service
mv /app.jar /test-service/spring-boot-app.jar
chmod +x /test-service/spring-boot-app.jar
ln -s /test-service/spring-boot-app.jar /etc/init.d/spring-boot-app
}
install_double_link_service() {
mkdir /test-service
mv /app.jar /test-service/
chmod +x /test-service/app.jar
ln -s /test-service/app.jar /test-service/spring-boot-app.jar
ln -s /test-service/spring-boot-app.jar /etc/init.d/spring-boot-app
}
start_service() {
service spring-boot-app start $@
}
restart_service() {
service spring-boot-app restart
}
status_service() {
service spring-boot-app status
}
stop_service() {
service spring-boot-app stop
}
force_stop_service() {
service spring-boot-app force-stop
}
@@ -1,4 +0,0 @@
source ./test-functions.sh
launch_jar
await_app
curl -s http://127.0.0.1:8080/
@@ -1,5 +0,0 @@
export DEBUG=true
source ./test-functions.sh
launch_jar
await_app
curl -s http://127.0.0.1:8080/
@@ -1,6 +0,0 @@
source ./test-functions.sh
cp app.jar app-another.jar
export JARFILE=app-another.jar
launch_jar
await_app
curl -s http://127.0.0.1:8080/
@@ -1,4 +0,0 @@
source ./test-functions.sh
launch_jar --server.port=8081 --server.servlet.context-path=/test
await_app http://127.0.0.1:8081/test/
curl -s http://127.0.0.1:8081/test/
@@ -1,5 +0,0 @@
source ./test-functions.sh
echo 'JAVA_OPTS="-Dserver.port=8081 -Dserver.servlet.context-path=/test"' > app.conf
launch_jar
await_app http://127.0.0.1:8081/test/
curl -s http://127.0.0.1:8081/test/
@@ -1,5 +0,0 @@
source ./test-functions.sh
echo 'RUN_ARGS="--server.port=8081 --server.servlet.context-path=/test"' > app.conf
launch_jar
await_app http://127.0.0.1:8081/test/
curl -s http://127.0.0.1:8081/test/
@@ -1,4 +0,0 @@
source ./test-functions.sh
launch_jar --server.port=8081
await_app http://127.0.0.1:8081/
curl -s http://127.0.0.1:8081/
@@ -1,5 +0,0 @@
source ./test-functions.sh
echo 'JAVA_OPTS=-Dserver.port=8081' > app.conf
launch_jar
await_app http://127.0.0.1:8081/
curl -s http://127.0.0.1:8081/
@@ -1,5 +0,0 @@
source ./test-functions.sh
echo 'RUN_ARGS=--server.port=8081' > app.conf
launch_jar
await_app http://127.0.0.1:8081/
curl -s http://127.0.0.1:8081/
@@ -1,22 +0,0 @@
await_app() {
if [ -z $1 ]
then
url=http://127.0.0.1:8080
else
url=$1
fi
end=$(date +%s)
let "end+=30"
until curl -s $url > /dev/null
do
now=$(date +%s)
if [[ $now -ge $end ]]; then
break
fi
sleep 1
done
}
launch_jar() {
./app.jar $@ &
}
@@ -1,129 +0,0 @@
/*
* Copyright 2012-present 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.loader.tools;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
/**
* Default implementation of {@link LaunchScript}. Provides the default Spring Boot launch
* script or can load a specific script File. Also support mustache style template
* expansion of the form <code>{{name:default}}</code>.
*
* @author Phillip Webb
* @author Justin Rosenberg
* @since 1.3.0
*/
public class DefaultLaunchScript implements LaunchScript {
private static final int BUFFER_SIZE = 4096;
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{\\{(\\w+)(:.*?)?}}(?!})");
private static final Set<String> FILE_PATH_KEYS = Collections.singleton("inlinedConfScript");
private final String content;
/**
* Create a new {@link DefaultLaunchScript} instance.
* @param file the source script file or {@code null} to use the default
* @param properties an optional set of script properties used for variable expansion
* @throws IOException if the script cannot be loaded
*/
public DefaultLaunchScript(@Nullable File file, @Nullable Map<?, ?> properties) throws IOException {
String content = loadContent(file);
this.content = expandPlaceholders(content, properties);
}
private String loadContent(@Nullable File file) throws IOException {
if (file == null) {
InputStream stream = getClass().getResourceAsStream("launch.script");
Assert.state(stream != null, "Unable to load resource 'launch.script'");
return loadContent(stream);
}
return loadContent(new FileInputStream(file));
}
private String loadContent(InputStream inputStream) throws IOException {
try (inputStream) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
copy(inputStream, outputStream);
return outputStream.toString(StandardCharsets.UTF_8);
}
}
private void copy(InputStream inputStream, OutputStream outputStream) throws IOException {
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
}
private String expandPlaceholders(String content, @Nullable Map<?, ?> properties) throws IOException {
StringBuilder expanded = new StringBuilder();
Matcher matcher = PLACEHOLDER_PATTERN.matcher(content);
while (matcher.find()) {
String name = matcher.group(1);
final String value;
String defaultValue = matcher.group(2);
if (properties != null && properties.containsKey(name)) {
Object propertyValue = properties.get(name);
if (FILE_PATH_KEYS.contains(name)) {
value = parseFilePropertyValue(propertyValue);
}
else {
value = propertyValue.toString();
}
}
else {
value = (defaultValue != null) ? defaultValue.substring(1) : matcher.group(0);
}
matcher.appendReplacement(expanded, value.replace("$", "\\$"));
}
matcher.appendTail(expanded);
return expanded.toString();
}
private String parseFilePropertyValue(Object propertyValue) throws IOException {
if (propertyValue instanceof File file) {
return loadContent(file);
}
return loadContent(new File(propertyValue.toString()));
}
@Override
public byte[] toByteArray() {
return this.content.getBytes(StandardCharsets.UTF_8);
}
}
@@ -57,31 +57,14 @@ public class JarWriter extends AbstractJarWriter implements AutoCloseable {
/**
* Create a new {@link JarWriter} instance.
* @param file the file to write
* @param launchScript an optional launch script to prepend to the front of the jar
* @throws IOException if the file cannot be opened
* @throws FileNotFoundException if the file cannot be found
*/
public JarWriter(File file, @Nullable LaunchScript launchScript) throws FileNotFoundException, IOException {
this(file, launchScript, null);
}
/**
* Create a new {@link JarWriter} instance.
* @param file the file to write
* @param launchScript an optional launch script to prepend to the front of the jar
* @param lastModifiedTime an optional last modified time to apply to the written
* entries
* @throws IOException if the file cannot be opened
* @throws FileNotFoundException if the file cannot be found
* @since 2.3.0
* @since 4.0.0
*/
public JarWriter(File file, @Nullable LaunchScript launchScript, @Nullable FileTime lastModifiedTime)
throws FileNotFoundException, IOException {
public JarWriter(File file, @Nullable FileTime lastModifiedTime) throws FileNotFoundException, IOException {
this.jarOutputStream = new JarArchiveOutputStream(new FileOutputStream(file));
if (launchScript != null) {
this.jarOutputStream.writePreamble(launchScript.toByteArray());
file.setExecutable(true);
}
this.jarOutputStream.setEncoding("UTF-8");
this.lastModifiedTime = lastModifiedTime;
}
@@ -1,34 +0,0 @@
/*
* Copyright 2012-present 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.loader.tools;
/**
* A script that can be prepended to the front of a JAR file to make it executable.
*
* @author Phillip Webb
* @since 1.3.0
*/
@FunctionalInterface
public interface LaunchScript {
/**
* The content of the launch script as a byte array.
* @return the script bytes
*/
byte[] toByteArray();
}
@@ -101,28 +101,13 @@ public class Repackager extends Packager {
* {@literal java -jar}'.
* @param destination the destination file (may be the same as the source)
* @param libraries the libraries required to run the archive
* @param launchScript an optional launch script prepended to the front of the jar
* @throws IOException if the file cannot be repackaged
* @since 1.3.0
*/
public void repackage(File destination, Libraries libraries, @Nullable LaunchScript launchScript)
throws IOException {
repackage(destination, libraries, launchScript, null);
}
/**
* Repackage to the given destination so that it can be launched using '
* {@literal java -jar}'.
* @param destination the destination file (may be the same as the source)
* @param libraries the libraries required to run the archive
* @param launchScript an optional launch script prepended to the front of the jar
* @param lastModifiedTime an optional last modified time to apply to the archive and
* its contents
* @throws IOException if the file cannot be repackaged
* @since 2.3.0
* @since 4.0.0
*/
public void repackage(File destination, Libraries libraries, @Nullable LaunchScript launchScript,
@Nullable FileTime lastModifiedTime) throws IOException {
public void repackage(File destination, Libraries libraries, @Nullable FileTime lastModifiedTime)
throws IOException {
Assert.isTrue(destination != null && !destination.isDirectory(), "Invalid destination");
getLayout(); // get layout early
destination = destination.getAbsoluteFile();
@@ -139,7 +124,7 @@ public class Repackager extends Packager {
destination.delete();
try {
try (JarFile sourceJar = new JarFile(workingSource)) {
repackage(sourceJar, destination, libraries, launchScript, lastModifiedTime);
repackage(sourceJar, destination, libraries, lastModifiedTime);
}
}
finally {
@@ -150,8 +135,8 @@ public class Repackager extends Packager {
}
private void repackage(JarFile sourceJar, File destination, Libraries libraries,
@Nullable LaunchScript launchScript, @Nullable FileTime lastModifiedTime) throws IOException {
try (JarWriter writer = new JarWriter(destination, launchScript, lastModifiedTime)) {
@Nullable FileTime lastModifiedTime) throws IOException {
try (JarWriter writer = new JarWriter(destination, lastModifiedTime)) {
write(sourceJar, libraries, writer, lastModifiedTime != null);
}
if (lastModifiedTime != null) {
@@ -1,309 +0,0 @@
#!/bin/bash
#
# . ____ _ __ _ _
# /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
# ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
# \\/ ___)| |_)| | | | | || (_| | ) ) ) )
# ' |____| .__|_| |_|_| |_\__, | / / / /
# =========|_|==============|___/=/_/_/_/
# :: Spring Boot Startup Script ::
#
### BEGIN INIT INFO
# Provides: {{initInfoProvides:spring-boot-application}}
# Required-Start: {{initInfoRequiredStart:$remote_fs $syslog $network}}
# Required-Stop: {{initInfoRequiredStop:$remote_fs $syslog $network}}
# Default-Start: {{initInfoDefaultStart:2 3 4 5}}
# Default-Stop: {{initInfoDefaultStop:0 1 6}}
# Short-Description: {{initInfoShortDescription:Spring Boot Application}}
# Description: {{initInfoDescription:Spring Boot Application}}
# chkconfig: {{initInfoChkconfig:2345 99 01}}
### END INIT INFO
[[ -n "$DEBUG" ]] && set -x
# Initialize variables that cannot be provided by a .conf file
WORKING_DIR="$(pwd)"
# shellcheck disable=SC2153
[[ -n "$JARFILE" ]] && jarfile="$JARFILE"
[[ -n "$APP_NAME" ]] && identity="$APP_NAME"
# Follow symlinks to find the real jar and detect init.d script
cd "$(dirname "$0")" || exit 1
[[ -z "$jarfile" ]] && jarfile=$(pwd)/$(basename "$0")
while [[ -L "$jarfile" ]]; do
if [[ "$jarfile" =~ init\.d ]]; then
init_script=$(basename "$jarfile")
else
configfile="${jarfile%.*}.conf"
# shellcheck source=/dev/null
[[ -r ${configfile} ]] && source "${configfile}"
fi
jarfile=$(readlink "$jarfile")
cd "$(dirname "$jarfile")" || exit 1
jarfile=$(pwd)/$(basename "$jarfile")
done
jarfolder="$( (cd "$(dirname "$jarfile")" && pwd -P) )"
cd "$WORKING_DIR" || exit 1
# Inline script specified in build properties
{{inlinedConfScript:}}
# Source any config file
configfile="$(basename "${jarfile%.*}.conf")"
# Initialize CONF_FOLDER location defaulting to jarfolder
[[ -z "$CONF_FOLDER" ]] && CONF_FOLDER="{{confFolder:${jarfolder}}}"
# shellcheck source=/dev/null
[[ -r "${CONF_FOLDER}/${configfile}" ]] && source "${CONF_FOLDER}/${configfile}"
# ANSI Colors
echoRed() { echo $'\e[0;31m'"$1"$'\e[0m'; }
echoGreen() { echo $'\e[0;32m'"$1"$'\e[0m'; }
echoYellow() { echo $'\e[0;33m'"$1"$'\e[0m'; }
# Initialize PID/LOG locations if they weren't provided by the config file
[[ -z "$PID_FOLDER" ]] && PID_FOLDER="{{pidFolder:/var/run}}"
[[ -z "$LOG_FOLDER" ]] && LOG_FOLDER="{{logFolder:/var/log}}"
! [[ "$PID_FOLDER" == /* ]] && PID_FOLDER="$(dirname "$jarfile")"/"$PID_FOLDER"
! [[ "$LOG_FOLDER" == /* ]] && LOG_FOLDER="$(dirname "$jarfile")"/"$LOG_FOLDER"
! [[ -x "$PID_FOLDER" ]] && echoYellow "PID_FOLDER $PID_FOLDER does not exist. Falling back to /tmp" && PID_FOLDER="/tmp"
! [[ -x "$LOG_FOLDER" ]] && echoYellow "LOG_FOLDER $LOG_FOLDER does not exist. Falling back to /tmp" && LOG_FOLDER="/tmp"
# Set up defaults
[[ -z "$MODE" ]] && MODE="{{mode:auto}}" # modes are "auto", "service" or "run"
[[ -z "$USE_START_STOP_DAEMON" ]] && USE_START_STOP_DAEMON="{{useStartStopDaemon:true}}"
# Create an identity for log/pid files
if [[ -z "$identity" ]]; then
if [[ -n "$init_script" ]]; then
identity="${init_script}"
else
identity=$(basename "${jarfile%.*}")_${jarfolder//\//}
fi
fi
# Initialize log file name if not provided by the config file
[[ -z "$LOG_FILENAME" ]] && LOG_FILENAME="{{logFilename:${identity}.log}}"
# Initialize stop wait time if not provided by the config file
[[ -z "$STOP_WAIT_TIME" ]] && STOP_WAIT_TIME="{{stopWaitTime:60}}"
# Utility functions
checkPermissions() {
touch "$pid_file" &> /dev/null || { echoRed "Operation not permitted (cannot access pid file)"; return 4; }
touch "$log_file" &> /dev/null || { echoRed "Operation not permitted (cannot access log file)"; return 4; }
}
isRunning() {
ps -p "$1" &> /dev/null
}
await_file() {
end=$(date +%s)
let "end+=10"
while [[ ! -s "$1" ]]
do
now=$(date +%s)
if [[ $now -ge $end ]]; then
break
fi
sleep 1
done
}
# Determine the script mode
action="run"
if [[ "$MODE" == "auto" && -n "$init_script" ]] || [[ "$MODE" == "service" ]]; then
action="$1"
shift
fi
# Build the pid and log filenames
PID_FOLDER="$PID_FOLDER/${identity}"
pid_file="$PID_FOLDER/{{pidFilename:${identity}.pid}}"
log_file="$LOG_FOLDER/$LOG_FILENAME"
# Determine the user to run as if we are root
# shellcheck disable=SC2012
[[ $(id -u) == "0" ]] && run_user=$(ls -ld "$jarfile" | awk '{print $3}')
# Ensure the user actually exists
id -u "$run_user" &> /dev/null || unset run_user
# Run as user specified in RUN_AS_USER
if [[ -n "$RUN_AS_USER" ]]; then
if ! [[ "$action" =~ ^(status|run)$ ]]; then
id -u "$RUN_AS_USER" || {
echoRed "Cannot run as '$RUN_AS_USER': no such user"
exit 2
}
[[ $(id -u) == 0 ]] || {
echoRed "Cannot run as '$RUN_AS_USER': current user is not root"
exit 4
}
fi
run_user="$RUN_AS_USER"
fi
# Issue a warning if the application will run as root
[[ $(id -u ${run_user}) == "0" ]] && { echoYellow "Application is running as root (UID 0). This is considered insecure."; }
# Find Java
if [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then
javaexe="$JAVA_HOME/bin/java"
elif type -p java > /dev/null 2>&1; then
javaexe=$(type -p java)
elif [[ -x "/usr/bin/java" ]]; then
javaexe="/usr/bin/java"
else
echo "Unable to find Java"
exit 1
fi
arguments=(-Dsun.misc.URLClassPath.disableJarChecking=true $JAVA_OPTS -jar "$jarfile" $RUN_ARGS "$@")
# Action functions
start() {
if [[ -f "$pid_file" ]]; then
pid=$(cat "$pid_file")
isRunning "$pid" && { echoYellow "Already running [$pid]"; return 0; }
fi
do_start "$@"
}
do_start() {
working_dir=$(dirname "$jarfile")
pushd "$working_dir" > /dev/null
if [[ ! -e "$PID_FOLDER" ]]; then
mkdir -p "$PID_FOLDER" &> /dev/null
if [[ -n "$run_user" ]]; then
chown "$run_user" "$PID_FOLDER"
fi
fi
if [[ ! -e "$log_file" ]]; then
touch "$log_file" &> /dev/null
if [[ -n "$run_user" ]]; then
chown "$run_user" "$log_file"
fi
fi
if [[ -n "$run_user" ]]; then
checkPermissions || return $?
if [ $USE_START_STOP_DAEMON = true ] && type start-stop-daemon > /dev/null 2>&1; then
start-stop-daemon --start --quiet \
--chuid "$run_user" \
--name "$identity" \
--make-pidfile --pidfile "$pid_file" \
--background --no-close \
--startas "$javaexe" \
--chdir "$working_dir" \
-- "${arguments[@]}" \
>> "$log_file" 2>&1
await_file "$pid_file"
else
su -s /bin/sh -c "$javaexe $(printf "\"%s\" " "${arguments[@]}") >> \"$log_file\" 2>&1 & echo \$!" "$run_user" > "$pid_file"
fi
pid=$(cat "$pid_file")
else
checkPermissions || return $?
"$javaexe" "${arguments[@]}" >> "$log_file" 2>&1 &
pid=$!
disown $pid
echo "$pid" > "$pid_file"
fi
[[ -z $pid ]] && { echoRed "Failed to start"; return 1; }
echoGreen "Started [$pid]"
}
stop() {
working_dir=$(dirname "$jarfile")
pushd "$working_dir" > /dev/null
[[ -f $pid_file ]] || { echoYellow "Not running (pidfile not found)"; return 0; }
pid=$(cat "$pid_file")
isRunning "$pid" || { echoYellow "Not running (process ${pid}). Removing stale pid file."; rm -f "$pid_file"; return 0; }
do_stop "$pid" "$pid_file"
}
do_stop() {
kill "$1" &> /dev/null || { echoRed "Unable to kill process $1"; return 1; }
for ((i = 1; i <= STOP_WAIT_TIME; i++)); do
isRunning "$1" || { echoGreen "Stopped [$1]"; rm -f "$2"; return 0; }
[[ $i -eq STOP_WAIT_TIME/2 ]] && kill "$1" &> /dev/null
sleep 1
done
echoRed "Unable to kill process $1";
return 1;
}
force_stop() {
[[ -f $pid_file ]] || { echoYellow "Not running (pidfile not found)"; return 0; }
pid=$(cat "$pid_file")
isRunning "$pid" || { echoYellow "Not running (process ${pid}). Removing stale pid file."; rm -f "$pid_file"; return 0; }
do_force_stop "$pid" "$pid_file"
}
do_force_stop() {
kill -9 "$1" &> /dev/null || { echoRed "Unable to kill process $1"; return 1; }
for ((i = 1; i <= STOP_WAIT_TIME; i++)); do
isRunning "$1" || { echoGreen "Stopped [$1]"; rm -f "$2"; return 0; }
[[ $i -eq STOP_WAIT_TIME/2 ]] && kill -9 "$1" &> /dev/null
sleep 1
done
echoRed "Unable to kill process $1";
return 1;
}
restart() {
stop && start
}
force_reload() {
working_dir=$(dirname "$jarfile")
pushd "$working_dir" > /dev/null
[[ -f $pid_file ]] || { echoRed "Not running (pidfile not found)"; return 7; }
pid=$(cat "$pid_file")
rm -f "$pid_file"
isRunning "$pid" || { echoRed "Not running (process ${pid} not found)"; return 7; }
do_stop "$pid" "$pid_file"
do_start
}
status() {
working_dir=$(dirname "$jarfile")
pushd "$working_dir" > /dev/null
[[ -f "$pid_file" ]] || { echoRed "Not running"; return 3; }
pid=$(cat "$pid_file")
isRunning "$pid" || { echoRed "Not running (process ${pid} not found)"; return 1; }
echoGreen "Running [$pid]"
return 0
}
run() {
pushd "$(dirname "$jarfile")" > /dev/null
"$javaexe" "${arguments[@]}"
result=$?
popd > /dev/null
return "$result"
}
# Call the appropriate action function
case "$action" in
start)
start "$@"; exit $?;;
stop)
stop "$@"; exit $?;;
force-stop)
force_stop "$@"; exit $?;;
restart)
restart "$@"; exit $?;;
force-reload)
force_reload "$@"; exit $?;;
status)
status "$@"; exit $?;;
run)
run "$@"; exit $?;;
*)
echo "Usage: $0 {start|stop|force-stop|restart|force-reload|status|run}"; exit 1;
esac
exit 0
@@ -1,238 +0,0 @@
/*
* Copyright 2012-present 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.loader.tools;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.util.FileCopyUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link DefaultLaunchScript}.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @author Justin Rosenberg
*/
class DefaultLaunchScriptTests {
@TempDir
@SuppressWarnings("NullAway.Init")
File tempDir;
@Test
void loadsDefaultScript() throws Exception {
DefaultLaunchScript script = new DefaultLaunchScript(null, null);
String content = new String(script.toByteArray());
assertThat(content).contains("Spring Boot Startup Script");
}
@Test
void logFilenameCanBeReplaced() throws Exception {
assertThatPlaceholderCanBeReplaced("logFilename");
}
@Test
void pidFilenameCanBeReplaced() throws Exception {
assertThatPlaceholderCanBeReplaced("pidFilename");
}
@Test
void initInfoProvidesCanBeReplaced() throws Exception {
assertThatPlaceholderCanBeReplaced("initInfoProvides");
}
@Test
void initInfoRequiredStartCanBeReplaced() throws Exception {
assertThatPlaceholderCanBeReplaced("initInfoRequiredStart");
}
@Test
void initInfoRequiredStopCanBeReplaced() throws Exception {
assertThatPlaceholderCanBeReplaced("initInfoRequiredStop");
}
@Test
void initInfoDefaultStartCanBeReplaced() throws Exception {
assertThatPlaceholderCanBeReplaced("initInfoDefaultStart");
}
@Test
void initInfoDefaultStopCanBeReplaced() throws Exception {
assertThatPlaceholderCanBeReplaced("initInfoDefaultStop");
}
@Test
void initInfoShortDescriptionCanBeReplaced() throws Exception {
assertThatPlaceholderCanBeReplaced("initInfoShortDescription");
}
@Test
void initInfoDescriptionCanBeReplaced() throws Exception {
assertThatPlaceholderCanBeReplaced("initInfoDescription");
}
@Test
void initInfoChkconfigCanBeReplaced() throws Exception {
assertThatPlaceholderCanBeReplaced("initInfoChkconfig");
}
@Test
void modeCanBeReplaced() throws Exception {
assertThatPlaceholderCanBeReplaced("mode");
}
@Test
void useStartStopDaemonCanBeReplaced() throws Exception {
assertThatPlaceholderCanBeReplaced("useStartStopDaemon");
}
@Test
void logFolderCanBeReplaced() throws Exception {
assertThatPlaceholderCanBeReplaced("logFolder");
}
@Test
void pidFolderCanBeReplaced() throws Exception {
assertThatPlaceholderCanBeReplaced("pidFolder");
}
@Test
void confFolderCanBeReplaced() throws Exception {
assertThatPlaceholderCanBeReplaced("confFolder");
}
@Test
void stopWaitTimeCanBeReplaced() throws Exception {
assertThatPlaceholderCanBeReplaced("stopWaitTime");
}
@Test
void inlinedConfScriptFileLoad() throws IOException {
DefaultLaunchScript script = new DefaultLaunchScript(null,
createProperties("inlinedConfScript:src/test/resources/example.script"));
String content = new String(script.toByteArray());
assertThat(content).contains("FOO=BAR");
}
@Test
void defaultForUseStartStopDaemonIsTrue() throws Exception {
DefaultLaunchScript script = new DefaultLaunchScript(null, null);
String content = new String(script.toByteArray());
assertThat(content).contains("USE_START_STOP_DAEMON=\"true\"");
}
@Test
void defaultForModeIsAuto() throws Exception {
DefaultLaunchScript script = new DefaultLaunchScript(null, null);
String content = new String(script.toByteArray());
assertThat(content).contains("MODE=\"auto\"");
}
@Test
void defaultForStopWaitTimeIs60() throws Exception {
DefaultLaunchScript script = new DefaultLaunchScript(null, null);
String content = new String(script.toByteArray());
assertThat(content).contains("STOP_WAIT_TIME=\"60\"");
}
@Test
void loadFromFile() throws Exception {
File file = new File(this.tempDir, "script");
FileCopyUtils.copy("ABC".getBytes(), file);
DefaultLaunchScript script = new DefaultLaunchScript(file, null);
String content = new String(script.toByteArray());
assertThat(content).isEqualTo("ABC");
}
@Test
void expandVariables() throws Exception {
File file = new File(this.tempDir, "script");
FileCopyUtils.copy("h{{a}}ll{{b}}".getBytes(), file);
DefaultLaunchScript script = new DefaultLaunchScript(file, createProperties("a:e", "b:o"));
String content = new String(script.toByteArray());
assertThat(content).isEqualTo("hello");
}
@Test
void expandVariablesMultiLine() throws Exception {
File file = new File(this.tempDir, "script");
FileCopyUtils.copy("h{{a}}l\nl{{b}}".getBytes(), file);
DefaultLaunchScript script = new DefaultLaunchScript(file, createProperties("a:e", "b:o"));
String content = new String(script.toByteArray());
assertThat(content).isEqualTo("hel\nlo");
}
@Test
void expandVariablesWithDefaults() throws Exception {
File file = new File(this.tempDir, "script");
FileCopyUtils.copy("h{{a:e}}ll{{b:o}}".getBytes(), file);
DefaultLaunchScript script = new DefaultLaunchScript(file, null);
String content = new String(script.toByteArray());
assertThat(content).isEqualTo("hello");
}
@Test
void expandVariablesCanDefaultToBlank() throws Exception {
File file = new File(this.tempDir, "script");
FileCopyUtils.copy("s{{p:}}{{r:}}ing".getBytes(), file);
DefaultLaunchScript script = new DefaultLaunchScript(file, null);
String content = new String(script.toByteArray());
assertThat(content).isEqualTo("sing");
}
@Test
void expandVariablesWithDefaultsOverride() throws Exception {
File file = new File(this.tempDir, "script");
FileCopyUtils.copy("h{{a:e}}ll{{b:o}}".getBytes(), file);
DefaultLaunchScript script = new DefaultLaunchScript(file, createProperties("a:a"));
String content = new String(script.toByteArray());
assertThat(content).isEqualTo("hallo");
}
@Test
void expandVariablesMissingAreUnchanged() throws Exception {
File file = new File(this.tempDir, "script");
FileCopyUtils.copy("h{{a}}ll{{b}}".getBytes(), file);
DefaultLaunchScript script = new DefaultLaunchScript(file, null);
String content = new String(script.toByteArray());
assertThat(content).isEqualTo("h{{a}}ll{{b}}");
}
private void assertThatPlaceholderCanBeReplaced(String placeholder) throws Exception {
DefaultLaunchScript script = new DefaultLaunchScript(null, createProperties(placeholder + ":__test__"));
String content = new String(script.toByteArray());
assertThat(content).contains("__test__");
}
private Map<?, ?> createProperties(String... pairs) {
Map<Object, Object> properties = new HashMap<>();
for (String pair : pairs) {
String[] keyValue = pair.split(":");
properties.put(keyValue[0], keyValue[1]);
}
return properties;
}
}
@@ -19,9 +19,7 @@ package org.springframework.boot.loader.tools;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.attribute.FileTime;
import java.nio.file.attribute.PosixFilePermission;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
@@ -156,30 +154,6 @@ class RepackagerTests extends AbstractPackagerTests<Repackager> {
assertThat(hasLauncherClasses(this.destination)).isTrue();
}
@Test
void addLauncherScript() throws Exception {
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
File source = this.testJarFile.getFile();
Repackager repackager = createRepackager(source, true);
LaunchScript script = new MockLauncherScript("ABC");
assertThat(this.destination).isNotNull();
repackager.repackage(this.destination, NO_LIBRARIES, script);
byte[] bytes = FileCopyUtils.copyToByteArray(this.destination);
assertThat(new String(bytes)).startsWith("ABC");
assertThat(hasLauncherClasses(source)).isFalse();
assertThat(hasLauncherClasses(this.destination)).isTrue();
try (ZipFile zipFile = ZipFile.builder().setFile(this.destination).get()) {
assertThat(zipFile.getEntries().hasMoreElements()).isTrue();
}
try {
assertThat(Files.getPosixFilePermissions(this.destination.toPath()))
.contains(PosixFilePermission.OWNER_EXECUTE);
}
catch (UnsupportedOperationException ex) {
// Probably running the test on Windows
}
}
@Test
void allLoaderDirectoriesAndFilesUseSameTimestamp() throws IOException {
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
@@ -208,7 +182,7 @@ class RepackagerTests extends AbstractPackagerTests<Repackager> {
Repackager repackager = createRepackager(this.testJarFile.getFile(), true);
long timestamp = OffsetDateTime.of(2000, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC).toInstant().toEpochMilli();
assertThat(this.destination).isNotNull();
repackager.repackage(this.destination, NO_LIBRARIES, null, FileTime.fromMillis(timestamp));
repackager.repackage(this.destination, NO_LIBRARIES, FileTime.fromMillis(timestamp));
long offsetTimestamp = DefaultTimeZoneOffset.INSTANCE.removeFrom(timestamp);
for (ZipArchiveEntry entry : getAllPackagedEntries()) {
assertThat(entry.getTime()).isEqualTo(offsetTimestamp);
@@ -223,7 +197,7 @@ class RepackagerTests extends AbstractPackagerTests<Repackager> {
ClassWithMainMethod.class);
Repackager repackager = createRepackager(this.testJarFile.getFile(), true);
assertThat(this.destination).isNotNull();
repackager.repackage(this.destination, NO_LIBRARIES, null, null);
repackager.repackage(this.destination, NO_LIBRARIES, null);
stopWatch.stop();
assertThat(stopWatch.getTotalTimeMillis()).isLessThan(5000);
}
@@ -306,21 +280,6 @@ class RepackagerTests extends AbstractPackagerTests<Repackager> {
}
}
static class MockLauncherScript implements LaunchScript {
private final byte[] bytes;
MockLauncherScript(String script) {
this.bytes = script.getBytes();
}
@Override
public byte[] toByteArray() {
return this.bytes;
}
}
static class TestLayoutFactory implements LayoutFactory {
@Override
-1
View File
@@ -472,7 +472,6 @@ include ":smoke-test:spring-boot-smoke-test-xml"
include ":integration-test:spring-boot-actuator-integration-tests"
include ":integration-test:spring-boot-configuration-processor-integration-tests"
include ":integration-test:spring-boot-integration-tests"
include ":integration-test:spring-boot-launch-script-integration-tests"
include ":integration-test:spring-boot-loader-integration-tests"
include ":integration-test:spring-boot-server-integration-tests"
include ":integration-test:spring-boot-sni-integration-tests"