[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.
 
 ---