[GERONIMO-6799] enable to use ldd to build container image with library dependencies
diff --git a/arthur-maven-plugin/pom.xml b/arthur-maven-plugin/pom.xml
index 9e08a04..818897c 100644
--- a/arthur-maven-plugin/pom.xml
+++ b/arthur-maven-plugin/pom.xml
@@ -71,7 +71,7 @@
<dependency>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-core</artifactId>
- <version>0.15.0</version>
+ <version>0.16.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
@@ -86,7 +86,7 @@
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
- <version>28.0-jre</version>
+ <version>30.0-jre</version>
</dependency>
<dependency>
diff --git a/arthur-maven-plugin/src/main/java/org/apache/geronimo/arthur/maven/mojo/JibMojo.java b/arthur-maven-plugin/src/main/java/org/apache/geronimo/arthur/maven/mojo/JibMojo.java
index 4f48286..e5e40e2 100644
--- a/arthur-maven-plugin/src/main/java/org/apache/geronimo/arthur/maven/mojo/JibMojo.java
+++ b/arthur-maven-plugin/src/main/java/org/apache/geronimo/arthur/maven/mojo/JibMojo.java
@@ -32,8 +32,12 @@
import com.google.cloud.tools.jib.api.buildplan.FilePermissions;
import org.apache.maven.plugins.annotations.Parameter;
+import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -45,6 +49,7 @@
import java.util.Collection;
import java.util.List;
import java.util.Map;
+import java.util.TreeMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -58,12 +63,16 @@
import static java.util.Objects.requireNonNull;
import static java.util.Optional.ofNullable;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
public abstract class JibMojo extends ArthurMojo {
/**
- * Base image to use. Scratch will ensure it starts from an empty image.
+ * Base image to use. Scratch will ensure it starts from an empty image and is the most minimal option.
* For a partially linked use busybox:glibc.
+ * Note that using scratch can require you to turn on useLDD flag (not by default since it depends in your build OS).
+ * On the opposite, using an existing distribution (debian, fedora, ...) enables to not do that at the cost of a bigger overall image.
+ * However, not only the overall image size is important, the reusable layers can save network time so pick what fits the best your case.
*/
@Parameter(property = "arthur.from", defaultValue = "scratch")
private String from;
@@ -113,7 +122,7 @@
/**
* Where is the binary to include. It defaults on native-image output if done before in the same execution
*/
- @Parameter(property = "arthur.binarySource")
+ @Parameter(property = "arthur.binarySource", defaultValue = "${project.build.directory}/${project.artifactId}.graal.bin")
private File binarySource;
/**
@@ -184,6 +193,21 @@
@Parameter(property = "arthur.cacertsDir", defaultValue = "/certificates/cacerts")
protected String cacertsTarget;
+ /**
+ * If true, the created binary will be passed to ldd to detect the needed libraries.
+ * It enables to use FROM scratch even when the binary requires dynamic linking.
+ * Note that if ld-linux libraries is found by that processing it is set as first argument of the entrypoint
+ * until skipLdLinuxInEntrypoint is set to true.
+ */
+ @Parameter(property = "arthur.useLDD", defaultValue = "false")
+ protected boolean useLDD;
+
+ /**
+ * If true, and even if useLDD is true, ld-linux will not be in entrypoint.
+ */
+ @Parameter(property = "arthur.skipLdLinuxInEntrypoint", defaultValue = "false")
+ protected boolean skipLdLinuxInEntrypoint;
+
protected abstract Containerizer createContainer() throws InvalidImageReferenceException;
@Override
@@ -267,9 +291,6 @@
if (ports != null) {
from.setExposedPorts(Ports.parse(ports));
}
- if (environment != null) {
- from.setEnvironment(environment);
- }
if (labels != null) {
from.setLabels(labels);
}
@@ -278,35 +299,55 @@
}
from.setCreationTime(creationTimestamp < 0 ? Instant.now() : Instant.ofEpochMilli(creationTimestamp));
- final boolean hasNatives = includeNatives != null && !includeNatives.isEmpty() && !singletonList("false").equals(includeNatives);
- if (entrypoint == null || entrypoint.size() < 1) {
- throw new IllegalArgumentException("No entrypoint set");
- }
- from.setEntrypoint(Stream.concat(Stream.concat(
- entrypoint.stream(),
- hasNatives ? Stream.of("-Djava.library.path=" + nativeRootDir) : Stream.empty()),
- includeCacerts ? Stream.of("-Djavax.net.ssl.trustStore=" + cacertsTarget) : Stream.empty())
- .collect(toList()));
-
+ final boolean hasNatives = useLDD || (includeNatives != null && !includeNatives.isEmpty() && !singletonList("false").equals(includeNatives));
final Path source = ofNullable(binarySource)
.map(File::toPath)
.orElseGet(() -> Paths.get(requireNonNull(
project.getProperties().getProperty(propertiesPrefix + "binary.path"),
"No binary path found, ensure to run native-image before or set entrypoint")));
- from.setFileEntriesLayers(Stream.concat(Stream.concat(Stream.concat(
- includeCacerts ?
- Stream.of(findCertificates()) : Stream.empty(),
- hasNatives ?
- Stream.of(findNatives()) : Stream.empty()),
- otherFiles != null && !otherFiles.isEmpty() ?
- Stream.of(createOthersLayer()) :
- Stream.empty()),
- Stream.of(FileEntriesLayer.builder()
- .setName("Binary")
- .addEntry(new FileEntry(
- source, AbsoluteUnixPath.get(entrypoint.iterator().next()), FilePermissions.fromOctalString("755"),
- getTimestamp(source)))
- .build()))
+
+ final Map<String, String> env = environment == null ? new TreeMap<>() : new TreeMap<>(environment);
+
+ final List<FileEntriesLayer> layers = new ArrayList<>(8);
+ if (includeCacerts) {
+ layers.add(findCertificates());
+ }
+
+ String ldLinux = null;
+ if (hasNatives) {
+ if (!singletonList("false").equals(includeNatives)) {
+ layers.add(findNatives());
+ }
+
+ if (useLDD) {
+ ldLinux = addLddLibsAndFindLdLinux(env, layers);
+ }
+ }
+
+ if (otherFiles != null && !otherFiles.isEmpty()) {
+ layers.add(createOthersLayer());
+ }
+ layers.add(FileEntriesLayer.builder()
+ .setName("Binary")
+ .addEntry(new FileEntry(
+ source, AbsoluteUnixPath.get(entrypoint.iterator().next()), FilePermissions.fromOctalString("755"),
+ getTimestamp(source)))
+ .build());
+
+ from.setFileEntriesLayers(layers);
+
+ if (!env.isEmpty()) {
+ from.setEnvironment(env);
+ }
+
+ if (entrypoint == null || entrypoint.size() < 1) {
+ throw new IllegalArgumentException("No entrypoint set");
+ }
+ from.setEntrypoint(Stream.concat(Stream.concat(Stream.concat(
+ useLDD && ldLinux != null && !skipLdLinuxInEntrypoint ? Stream.of(ldLinux) : Stream.empty(),
+ entrypoint.stream()),
+ hasNatives ? Stream.of("-Djava.library.path=" + nativeRootDir) : Stream.empty()),
+ includeCacerts ? Stream.of("-Djavax.net.ssl.trustStore=" + cacertsTarget) : Stream.empty())
.collect(toList()));
return from;
@@ -315,6 +356,92 @@
}
}
+ // todo: enrich to be able to use manual resolution using LD_LIBRARY_PATH or default without doing an exec
+ private String addLddLibsAndFindLdLinux(final Map<String, String> env, final List<FileEntriesLayer> layers) throws IOException {
+ getLog().info("Running ldd on " + binarySource.getName());
+ final Process ldd = new ProcessBuilder("ldd", binarySource.getAbsolutePath()).start();
+ try {
+ final int status = ldd.waitFor();
+ if (status != 0) {
+ throw new IllegalArgumentException("LDD failed with status " + status + ": " + slurp(ldd.getErrorStream()));
+ }
+ } catch (final InterruptedException e) {
+ throw new IllegalStateException(e);
+ }
+ final Collection<Path> files;
+ try (final BufferedReader reader = new BufferedReader(new InputStreamReader(ldd.getInputStream(), StandardCharsets.UTF_8))) {
+ files = reader.lines()
+ .filter(it -> it.contains("/"))
+ .map(it -> {
+ final int start = it.indexOf('/');
+ int end = it.indexOf(' ', start);
+ if (end < 0) {
+ end = it.indexOf('(', start);
+ if (end < 0) {
+ end = it.length();
+ }
+ }
+ return it.substring(start, end);
+ })
+ .map(Paths::get)
+ .filter(Files::exists)
+ .collect(toList());
+ } catch (final IOException e) {
+ throw new IllegalStateException(e);
+ }
+ String ldLinux = null;
+ if (!files.isEmpty()) {
+ final FileEntriesLayer.Builder libraries = FileEntriesLayer.builder()
+ .setName("Libraries");
+ // copy libs + tries to extract ld-linux-x86-64.so.2 if present
+ ldLinux = files.stream()
+ .map(file -> {
+ final String fileName = file.getFileName().toString();
+ try {
+ libraries.addEntry(
+ file, AbsoluteUnixPath.get(nativeRootDir).resolve(fileName),
+ FilePermissions.fromPosixFilePermissions(Files.getPosixFilePermissions(file)), getTimestamp(file));
+ } catch (final IOException e) {
+ throw new IllegalStateException(e);
+ }
+ return fileName;
+ })
+ .filter(it -> it.startsWith("ld-linux"))
+ .min((a, b) -> { // best is "ld-linux-x86-64.so.2"
+ if ("ld-linux-x86-64.so.2".equals(a)) {
+ return -1;
+ }
+ if ("ld-linux-x86-64.so.2".equals(b)) {
+ return 1;
+ }
+ if (a.endsWith(".so.2")) {
+ return -1;
+ }
+ if (b.endsWith(".so.2")) {
+ return -1;
+ }
+ return a.compareTo(b);
+ })
+ .map(it -> AbsoluteUnixPath.get(nativeRootDir).resolve(it).toString()) // make it absolute since it will be added to the entrypoint
+ .orElse(null);
+
+ layers.add(libraries.build());
+ env.putIfAbsent("LD_LIBRARY_PATH", AbsoluteUnixPath.get(nativeRootDir).toString());
+ }
+ return ldLinux;
+ }
+
+ private String slurp(final InputStream stream) {
+ if (stream == null) {
+ return "-";
+ }
+ try (final BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
+ return reader.lines().collect(joining("\n"));
+ } catch (final IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
private Path findHome() {
if (nativeImage == null) {
return createInstaller().install();
@@ -340,11 +467,18 @@
getLog().info("Using natives from '" + home + "'");
final Path jreLib = home.resolve("jre/lib");
final boolean isWin = Files.exists(jreLib.resolve("java.lib"));
- final Path nativeFolder = isWin ?
+ Path nativeFolder = isWin ?
jreLib /* win/cygwin */ :
- jreLib.resolve(System.getProperty("os.arch", "amd64"));
+ jreLib.resolve(System.getProperty("os.arch", "amd64")); // older graalvm, for 20.x it is no more needed
if (!Files.exists(nativeFolder)) {
- throw new IllegalArgumentException("No native folder '" + nativeFolder + "' found.");
+ nativeFolder = nativeFolder.getParent();
+ try {
+ if (!Files.exists(nativeFolder) || !Files.list(nativeFolder).anyMatch(it -> it.getFileName().toString().endsWith(".so"))) {
+ throw new IllegalArgumentException("No native folder '" + nativeFolder + "' found.");
+ }
+ } catch (final IOException e) {
+ throw new IllegalStateException(e);
+ }
}
final boolean includeAll = singletonList("true").equals(includeNatives) || singletonList("*").equals(includeNatives);
final Predicate<Path> include = includeAll ?
@@ -354,6 +488,7 @@
};
final FileEntriesLayer.Builder builder = FileEntriesLayer.builder();
final Collection<String> collected = new ArrayList<>();
+ final Path nativeDir = nativeFolder; // ref for lambda
try {
Files.walkFileTree(nativeFolder, new SimpleFileVisitor<Path>() {
@Override
@@ -361,7 +496,7 @@
if (include.test(file)) {
collected.add(file.getFileName().toString());
builder.addEntry(
- file, AbsoluteUnixPath.get(nativeRootDir).resolve(nativeFolder.relativize(file).toString()),
+ file, AbsoluteUnixPath.get(nativeRootDir).resolve(nativeDir.relativize(file).toString()),
FilePermissions.DEFAULT_FILE_PERMISSIONS, getTimestamp(file));
}
return super.visitFile(file, attrs);
diff --git a/documentation/src/content/maven.adoc b/documentation/src/content/maven.adoc
index 2f0c793..681438d 100644
--- a/documentation/src/content/maven.adoc
+++ b/documentation/src/content/maven.adoc
@@ -253,6 +253,29 @@
[INFO] ------------------------------------------------------------------------
----
+==== LDD or not
+
+By default Arthur maven docker/image plugins assume the binary is self executable by itself (statically linked).
+If it is not the case, you need to ensure the from/base image has the needed libraries.
+It is not always trivial nor convenient so we have a mode using the build machine `ldd` to detect which libs are needed and add them in the image parsing `ldd` output.
+
+IMPORTANT: this mode enables to build very light native images using `scratch` virtual base image (empty) but it can conflict with other base images since it sets `LD_LIBRARY_PATH` if not present int he `environment` configuration.
+
+Here is what the `ldd` output look like - if not the case ensure to set a PATH before running the plugin which has a compliant ldd:
+
+[source]
+----
+$ ldd doc.graal.bin
+ linux-vdso.so.1 (0x00007ffcc41ba000)
+ libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f4ad85e7000)
+ libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f4ad85e1000)
+ libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4ad83ef000)
+ /lib64/ld-linux-x86-64.so.2 (0x00007f4adba6b000)
+----
+
+The parser will take all line containing a `/` and the first string without any space starting by a `/` will be considered as a path of a library to include.
+With previous output, the image will get `libpthread.so.0`, `libdl.so.2`, `libc.so.6` and `ld-linux-x86-64.so.2` included (but not `linux-vdso.so.1` since it is not a path / it has no slash in the line).
+
==== Configuration
include::{generated_dir}/generated_docker_mojo.adoc[]
@@ -314,7 +337,7 @@
</plugin>
----
-Then you can just run `mvn arthur:native-image arthur:docker` to get a ready to deploy image.
+Then you can just run `mvn [package] arthur:native-image arthur:docker` to get a ready to deploy image.
---