diff --git a/components/native-loader/build.gradle.kts b/components/native-loader/build.gradle.kts
new file mode 100644
index 00000000000..a4178348a71
--- /dev/null
+++ b/components/native-loader/build.gradle.kts
@@ -0,0 +1,9 @@
+plugins {
+ `java-library`
+}
+
+apply(from = "$rootDir/gradle/java.gradle")
+
+dependencies {
+ implementation(project(":components:environment"))
+}
diff --git a/components/native-loader/src/main/java/datadog/nativeloader/ClassLoaderResourcePathLocator.java b/components/native-loader/src/main/java/datadog/nativeloader/ClassLoaderResourcePathLocator.java
new file mode 100644
index 00000000000..4563facfb81
--- /dev/null
+++ b/components/native-loader/src/main/java/datadog/nativeloader/ClassLoaderResourcePathLocator.java
@@ -0,0 +1,34 @@
+package datadog.nativeloader;
+
+import java.net.URL;
+import java.util.Objects;
+
+/** ClassLoaderResourcePathLocator locates library paths inside a {@link ClassLoader} */
+final class ClassLoaderResourcePathLocator implements PathLocator {
+ private final ClassLoader classLoader;
+ private final String baseResource;
+
+ public ClassLoaderResourcePathLocator(final ClassLoader classLoader, final String baseResource) {
+ this.classLoader = classLoader;
+ this.baseResource = baseResource;
+ }
+
+ @Override
+ public URL locate(String component, String path) {
+ return this.classLoader.getResource(PathUtils.concatPath(component, this.baseResource, path));
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.classLoader, this.baseResource);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof ClassLoaderResourcePathLocator)) return false;
+
+ ClassLoaderResourcePathLocator that = (ClassLoaderResourcePathLocator) obj;
+ return this.classLoader.equals(that.classLoader)
+ && Objects.equals(this.baseResource, that.baseResource);
+ }
+}
diff --git a/components/native-loader/src/main/java/datadog/nativeloader/FlatDirLibraryResolver.java b/components/native-loader/src/main/java/datadog/nativeloader/FlatDirLibraryResolver.java
new file mode 100644
index 00000000000..29b8f5e16c5
--- /dev/null
+++ b/components/native-loader/src/main/java/datadog/nativeloader/FlatDirLibraryResolver.java
@@ -0,0 +1,55 @@
+package datadog.nativeloader;
+
+import java.net.URL;
+
+/**
+ * FlatDirLibraryResolver - uses flat directories to provide more specific libraries to load
+ * {os}-{arch}-{libc/musl}
+ */
+public final class FlatDirLibraryResolver implements LibraryResolver {
+ public static final FlatDirLibraryResolver INSTANCE = new FlatDirLibraryResolver();
+
+ private FlatDirLibraryResolver() {}
+
+ @Override
+ public final URL resolve(
+ PathLocator pathLocator, String component, PlatformSpec platformSpec, String libName)
+ throws Exception {
+ PathLocatorHelper pathLocatorHelper = new PathLocatorHelper(libName, pathLocator);
+
+ String libFileName = PathUtils.libFileName(platformSpec, libName);
+
+ String osPath = PathUtils.osPartOf(platformSpec);
+ String archPath = PathUtils.archPartOf(platformSpec);
+ String libcPath = PathUtils.libcPartOf(platformSpec);
+
+ URL url;
+ String regularPath = osPath + "-" + archPath;
+
+ if (libcPath != null) {
+ String specializedPath = regularPath + "-" + libcPath;
+ url = pathLocatorHelper.locate(component, specializedPath + "/" + libFileName);
+ if (url != null) return url;
+ }
+
+ url = pathLocatorHelper.locate(component, regularPath + "/" + libFileName);
+ if (url != null) return url;
+
+ url = pathLocatorHelper.locate(component, osPath + "/" + libFileName);
+ if (url != null) return url;
+
+ // fallback to searching at top-level, mostly concession to good out-of-box behavior
+ // with java.library.path
+ url = pathLocatorHelper.locate(component, libFileName);
+ if (url != null) return url;
+
+ if (component != null) {
+ url = pathLocatorHelper.locate(null, libFileName);
+ if (url != null) return url;
+ }
+
+ pathLocatorHelper.tryThrow();
+
+ return null;
+ }
+}
diff --git a/components/native-loader/src/main/java/datadog/nativeloader/IntrospectPlatformSpec.java b/components/native-loader/src/main/java/datadog/nativeloader/IntrospectPlatformSpec.java
new file mode 100644
index 00000000000..00ef82d6d69
--- /dev/null
+++ b/components/native-loader/src/main/java/datadog/nativeloader/IntrospectPlatformSpec.java
@@ -0,0 +1,65 @@
+package datadog.nativeloader;
+
+import datadog.environment.OperatingSystem;
+import datadog.environment.OperatingSystem.Architecture;
+
+/*
+ * Default PlatformSpec used in dd-trace-java -- wraps detection code in component:environment
+ */
+final class IntrospectPlatformSpec extends PlatformSpec {
+ static final PlatformSpec INSTANCE = new IntrospectPlatformSpec();
+
+ @Override
+ public boolean isLinux() {
+ return OperatingSystem.isLinux();
+ }
+
+ @Override
+ public boolean isMac() {
+ return OperatingSystem.isMacOs();
+ }
+
+ @Override
+ public boolean isWindows() {
+ return OperatingSystem.isWindows();
+ }
+
+ @Override
+ public boolean isMusl() {
+ return OperatingSystem.isMusl();
+ }
+
+ @Override
+ public boolean isAarch64() {
+ return isArch(Architecture.ARM64);
+ }
+
+ @Override
+ public boolean isArm32() {
+ return isArch(Architecture.ARM);
+ }
+
+ @Override
+ public boolean isX86_32() {
+ return isArch(Architecture.X86);
+ }
+
+ @Override
+ public boolean isX86_64() {
+ return isArch(Architecture.X64);
+ }
+
+ static final boolean isArch(OperatingSystem.Architecture arch) {
+ return (OperatingSystem.architecture() == arch);
+ }
+
+ @Override
+ public int hashCode() {
+ return IntrospectPlatformSpec.class.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return (obj instanceof IntrospectPlatformSpec);
+ }
+}
diff --git a/components/native-loader/src/main/java/datadog/nativeloader/LibDirBasedPathLocator.java b/components/native-loader/src/main/java/datadog/nativeloader/LibDirBasedPathLocator.java
new file mode 100644
index 00000000000..5199ae0fc3a
--- /dev/null
+++ b/components/native-loader/src/main/java/datadog/nativeloader/LibDirBasedPathLocator.java
@@ -0,0 +1,49 @@
+package datadog.nativeloader;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Arrays;
+
+/** LibDirBasedPathLocator locates libraries inside a list of library directories */
+final class LibDirBasedPathLocator implements PathLocator {
+ private final File[] libDirs;
+
+ public LibDirBasedPathLocator(File... libDirs) {
+ this.libDirs = libDirs;
+ }
+
+ @Override
+ public URL locate(String component, String path) {
+ String fullPath = PathUtils.concatPath(component, path);
+
+ for (File libDir : this.libDirs) {
+ File libFile = new File(libDir, fullPath);
+ if (libFile.exists()) return toUrl(libFile);
+ }
+
+ return null;
+ }
+
+ @SuppressWarnings("deprecation")
+ private static final URL toUrl(File file) {
+ try {
+ return file.toURL();
+ } catch (MalformedURLException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(this.libDirs);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof LibDirBasedPathLocator)) return false;
+
+ LibDirBasedPathLocator that = (LibDirBasedPathLocator) obj;
+ return Arrays.equals(this.libDirs, that.libDirs);
+ }
+}
diff --git a/components/native-loader/src/main/java/datadog/nativeloader/LibFile.java b/components/native-loader/src/main/java/datadog/nativeloader/LibFile.java
new file mode 100644
index 00000000000..fb2e27d61d4
--- /dev/null
+++ b/components/native-loader/src/main/java/datadog/nativeloader/LibFile.java
@@ -0,0 +1,81 @@
+package datadog.nativeloader;
+
+import java.io.File;
+import java.nio.file.Path;
+
+/**
+ * Represents a resolved library
+ *
+ *
+ * - library may be preloaded - with no backing file
+ *
- regular file - that doesn't require clean-up
+ *
- temporary file - copying from another source - that does require clean-up
+ *
+ */
+public final class LibFile implements AutoCloseable {
+ static final boolean NO_CLEAN_UP = false;
+ static final boolean CLEAN_UP = true;
+
+ static final LibFile preloaded(String libName) {
+ return new LibFile(libName, null, NO_CLEAN_UP);
+ }
+
+ static final LibFile fromFile(String libName, File file) {
+ return new LibFile(libName, file, NO_CLEAN_UP);
+ }
+
+ static final LibFile fromTempFile(String libName, File file) {
+ return new LibFile(libName, file, CLEAN_UP);
+ }
+
+ final String libName;
+
+ final File file;
+ final boolean needsCleanup;
+
+ LibFile(String libName, File file, boolean needsCleanup) {
+ this.libName = libName;
+
+ this.file = file;
+ this.needsCleanup = needsCleanup;
+ }
+
+ /** Indicates if this library was "preloaded" */
+ public boolean isPreloaded() {
+ return (this.file == null);
+ }
+
+ /** Loads the underlying library into the JVM */
+ public void load() throws LibraryLoadException {
+ if (this.isPreloaded()) return;
+
+ try {
+ Runtime.getRuntime().load(this.getAbsolutePath());
+ } catch (Throwable t) {
+ throw new LibraryLoadException(this.libName, t);
+ }
+ }
+
+ /** Provides a File to the library -- returns null for pre-loaded libraries */
+ public final File toFile() {
+ return this.file;
+ }
+
+ /** Provides a Path to the library -- return null for pre-loaded libraries */
+ public final Path toPath() {
+ return this.file == null ? null : this.file.toPath();
+ }
+
+ /** Provides the an absolute path to the library -- returns null for pre-loaded libraries */
+ public final String getAbsolutePath() {
+ return this.file == null ? null : this.file.getAbsolutePath();
+ }
+
+ /** Schedules clean-up of underlying file -- if the file is a temp file */
+ @Override
+ public void close() {
+ if (this.needsCleanup) {
+ NativeLoader.delete(this.file);
+ }
+ }
+}
diff --git a/components/native-loader/src/main/java/datadog/nativeloader/LibraryLoadException.java b/components/native-loader/src/main/java/datadog/nativeloader/LibraryLoadException.java
new file mode 100644
index 00000000000..12a8779958e
--- /dev/null
+++ b/components/native-loader/src/main/java/datadog/nativeloader/LibraryLoadException.java
@@ -0,0 +1,29 @@
+package datadog.nativeloader;
+
+/** Exception raised when NativeLoader fails to resolve or load a library */
+public class LibraryLoadException extends Exception {
+ static final String UNSUPPORTED_OS = "Unsupported OS";
+ static final String UNSUPPORTED_ARCH = "Unsupported arch";
+
+ private static final long serialVersionUID = 1L;
+
+ public LibraryLoadException(String libName) {
+ super(message(libName));
+ }
+
+ public LibraryLoadException(String libName, Throwable cause) {
+ this(message(libName), cause.getMessage(), cause);
+ }
+
+ public LibraryLoadException(String libName, String message) {
+ super(message(libName) + " - " + message);
+ }
+
+ public LibraryLoadException(String libName, String message, Throwable cause) {
+ super(message(libName) + " - " + message, cause);
+ }
+
+ static final String message(String libName) {
+ return "Unable to resolve library " + libName;
+ }
+}
diff --git a/components/native-loader/src/main/java/datadog/nativeloader/LibraryResolver.java b/components/native-loader/src/main/java/datadog/nativeloader/LibraryResolver.java
new file mode 100644
index 00000000000..4d28aae2794
--- /dev/null
+++ b/components/native-loader/src/main/java/datadog/nativeloader/LibraryResolver.java
@@ -0,0 +1,20 @@
+package datadog.nativeloader;
+
+import java.net.URL;
+
+/**
+ * LibraryResolver encapsulates a library resolution strategy
+ *
+ * The LibraryResolver should use the provided {@link PathLocator} to locate the desired
+ * resources. The LibraryResolver may try multiple locations to find the best possible library to
+ * use.
+ */
+@FunctionalInterface
+public interface LibraryResolver {
+ default boolean isPreloaded(PlatformSpec platform, String libName) {
+ return false;
+ }
+
+ URL resolve(PathLocator pathLocator, String component, PlatformSpec platformSpec, String libName)
+ throws Exception;
+}
diff --git a/components/native-loader/src/main/java/datadog/nativeloader/LibraryResolvers.java b/components/native-loader/src/main/java/datadog/nativeloader/LibraryResolvers.java
new file mode 100644
index 00000000000..6ef8cec3c4b
--- /dev/null
+++ b/components/native-loader/src/main/java/datadog/nativeloader/LibraryResolvers.java
@@ -0,0 +1,44 @@
+package datadog.nativeloader;
+
+import java.net.URL;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+public final class LibraryResolvers {
+ private LibraryResolvers() {}
+
+ public static final LibraryResolver defaultLibraryResolver() {
+ return flatDirs();
+ }
+
+ public static final LibraryResolver withPreloaded(
+ LibraryResolver baseResolver, String... preloadedLibNames) {
+ return withPreloaded(baseResolver, new HashSet<>(Arrays.asList(preloadedLibNames)));
+ }
+
+ public static final LibraryResolver withPreloaded(
+ LibraryResolver baseResolver, Set preloadedLibNames) {
+ return new LibraryResolver() {
+ @Override
+ public boolean isPreloaded(PlatformSpec platform, String libName) {
+ return preloadedLibNames.contains(libName);
+ }
+
+ @Override
+ public URL resolve(
+ PathLocator pathLocator, String component, PlatformSpec platformSpec, String libName)
+ throws Exception {
+ return baseResolver.resolve(pathLocator, component, platformSpec, libName);
+ }
+ };
+ }
+
+ public static final LibraryResolver flatDirs() {
+ return FlatDirLibraryResolver.INSTANCE;
+ }
+
+ public static final LibraryResolver nestedDirs() {
+ return NestedDirLibraryResolver.INSTANCE;
+ }
+}
diff --git a/components/native-loader/src/main/java/datadog/nativeloader/NativeLoader.java b/components/native-loader/src/main/java/datadog/nativeloader/NativeLoader.java
new file mode 100644
index 00000000000..17af0b6f7ab
--- /dev/null
+++ b/components/native-loader/src/main/java/datadog/nativeloader/NativeLoader.java
@@ -0,0 +1,304 @@
+package datadog.nativeloader;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.util.Set;
+
+/**
+ * NativeLoader is intended as more feature rich replacement for calling {@link
+ * System#loadLibrary(String)} directly. NativeLoader can be used to find the corresponding platform
+ * specific library using pluggable strategies -- for both path determination {@link
+ * LibraryResolver} and path resolution {@link PathLocator}
+ */
+public final class NativeLoader {
+ public static final class Builder {
+ private PlatformSpec platformSpec;
+ private Path tempDir;
+ private String[] preloadedLibNames;
+ private LibraryResolver libResolver;
+ private PathLocator pathLocator;
+
+ Builder() {}
+
+ /** Sets the default {@link PlatformSpec} used by the {@link NativeLoader} */
+ public Builder platformSpec(PlatformSpec platform) {
+ this.platformSpec = platform;
+ return this;
+ }
+
+ /** Uses a nested directory layout -- {@link LibraryResolvers#nestedDirs()} */
+ public Builder nestedLayout() {
+ return this.libResolver(LibraryResolvers.nestedDirs());
+ }
+
+ /** Uses a flat directory layout -- {@link LibraryResolvers#flatDirs()} */
+ public Builder flatLayout() {
+ return this.libResolver(LibraryResolvers.flatDirs());
+ }
+
+ /**
+ * Indicates that libraries (or signatures from those libraries) are already loaded into the JVM
+ * {@link LibraryResolver#isPreloaded}
+ *
+ * @param libNames - lib names
+ */
+ public Builder preloaded(String... libNames) {
+ this.preloadedLibNames = libNames;
+ return this;
+ }
+
+ /**
+ * Uses the specified {@link LibraryResolver} can be used to implement an alternate file layout
+ */
+ public Builder libResolver(LibraryResolver libResolver) {
+ this.libResolver = libResolver;
+ return this;
+ }
+
+ /** Searches for the native libraries in the provided {@link ClassLoader} */
+ public Builder fromClassLoader(ClassLoader classLoader) {
+ return this.pathLocator(PathLocators.fromClassLoader(classLoader));
+ }
+
+ /**
+ * Searches for the native libraries in the provided {@link ClassLoader} using the specified
+ * baseResource
+ */
+ public Builder fromClassLoader(ClassLoader classLoader, String baseResource) {
+ return this.pathLocator(PathLocators.fromClassLoader(classLoader, baseResource));
+ }
+
+ /** Searches for the native libraries in the specified directory */
+ public Builder fromDir(String includeDir) {
+ return this.pathLocator(PathLocators.fromLibDirs(includeDir));
+ }
+
+ /** Searches for the native libraries in the specified directories */
+ public Builder fromDirs(String... includeDirs) {
+ return this.pathLocator(PathLocators.fromLibDirs(includeDirs));
+ }
+
+ /** Searches for the native libraries in the specified directory */
+ public Builder fromDir(File includeDir) {
+ return this.pathLocator(PathLocators.fromLibDirs(includeDir));
+ }
+
+ /** Searches for the native libraries in the specified directories */
+ public Builder fromDirs(File... includeDirs) {
+ return this.pathLocator(PathLocators.fromLibDirs(includeDirs));
+ }
+
+ /** Searches for the native libraries in the specified directory */
+ public Builder fromDir(Path includeDir) {
+ return this.pathLocator(PathLocators.fromLibDirs(includeDir));
+ }
+
+ /** Searches for the native libraries in the specified directories */
+ public Builder fromDirs(Path... paths) {
+ return this.pathLocator(PathLocators.fromLibDirs(paths));
+ }
+
+ /** Searches for the native libraries using the provided {@link PathLocator} */
+ public Builder pathLocator(PathLocator pathLocator) {
+ this.pathLocator = pathLocator;
+ return this;
+ }
+
+ /**
+ * Specifies temporary directory where native libraries are copied if the {@link PathLocator}
+ * returns a non-file {@link URL}
+ */
+ public Builder tempDir(File tempDir) {
+ return this.tempDir(tempDir.toPath());
+ }
+
+ /**
+ * Specifies temporary directory where native libraries are copied if the {@link PathLocator}
+ * returns a non-file {@link URL}
+ */
+ public Builder tempDir(Path tempPath) {
+ this.tempDir = tempPath;
+ return this;
+ }
+
+ /**
+ * Specifies temporary directory where native libraries are copied if the {@link PathLocator}
+ * returns a non-file {@link URL}
+ */
+ public Builder tempDir(String tmpPath) {
+ return this.tempDir(Paths.get(tmpPath));
+ }
+
+ /** Constructs and returns the {@link NativeLoader} */
+ public NativeLoader build() {
+ return new NativeLoader(this);
+ }
+
+ PlatformSpec platformSpec() {
+ return (this.platformSpec == null) ? PlatformSpec.defaultPlatformSpec() : this.platformSpec;
+ }
+
+ PathLocator pathLocator() {
+ return (this.pathLocator == null) ? PathLocators.defaultPathLocator() : this.pathLocator;
+ }
+
+ LibraryResolver libResolver() {
+ LibraryResolver baseResolver =
+ (this.libResolver == null) ? LibraryResolvers.defaultLibraryResolver() : this.libResolver;
+
+ return (this.preloadedLibNames == null)
+ ? baseResolver
+ : LibraryResolvers.withPreloaded(baseResolver, this.preloadedLibNames);
+ }
+
+ Path tempDir() {
+ return this.tempDir;
+ }
+ }
+
+ public static final Builder builder() {
+ return new Builder();
+ }
+
+ private final PlatformSpec defaultPlatformSpec;
+ private final LibraryResolver libResolver;
+ private final PathLocator pathResolver;
+ private final Path tempDir;
+
+ private NativeLoader(Builder builder) {
+ this.defaultPlatformSpec = builder.platformSpec();
+ this.libResolver = builder.libResolver();
+ this.pathResolver = builder.pathLocator();
+ this.tempDir = builder.tempDir();
+ }
+
+ /** Indicates if a library is considered "pre-loaded" */
+ public boolean isPreloaded(String libName) {
+ return this.libResolver.isPreloaded(this.defaultPlatformSpec, libName);
+ }
+
+ /** Indicates if a library is considered "pre-loaded" for the specified {@link PlatformSpec} */
+ public boolean isPreloaded(PlatformSpec platformSpec, String libName) {
+ return this.libResolver.isPreloaded(platformSpec, libName);
+ }
+
+ /** Loads a library */
+ public void load(String libName) throws LibraryLoadException {
+ this.load(null, libName);
+ }
+
+ /** Loads a library associated with an associated component */
+ public void load(String component, String libName) throws LibraryLoadException {
+ try (LibFile libFile = this.resolveDynamic(component, libName)) {
+ libFile.load();
+ }
+ }
+
+ /** Resolves a library to a LibFile - creating a temporary file if necessary */
+ public LibFile resolveDynamic(String libName) throws LibraryLoadException {
+ return this.resolveDynamic((String) null, libName);
+ }
+
+ /** Resolves a library with an associated component */
+ public LibFile resolveDynamic(String component, String libName) throws LibraryLoadException {
+ return this.resolveDynamic(component, this.defaultPlatformSpec, libName);
+ }
+
+ /**
+ * Resolves a library using a different {@link PlatformSpec} than the default for this {@link
+ * NativeLoader}
+ */
+ public LibFile resolveDynamic(PlatformSpec platformSpec, String libName)
+ throws LibraryLoadException {
+ return this.resolveDynamic(null, platformSpec, libName);
+ }
+
+ /**
+ * Resolves a library with an associated component with a different {@link PlatformSpec} than the
+ * default
+ */
+ public LibFile resolveDynamic(String component, PlatformSpec platformSpec, String libName)
+ throws LibraryLoadException {
+ if (platformSpec.isUnknownOs() || platformSpec.isUnknownArch()) {
+ throw new LibraryLoadException(libName, "Unsupported platform");
+ }
+
+ if (this.isPreloaded(platformSpec, libName)) {
+ return LibFile.preloaded(libName);
+ }
+
+ URL url;
+ try {
+ url = this.libResolver.resolve(this.pathResolver, component, platformSpec, libName);
+ } catch (LibraryLoadException e) {
+ // don't wrap if it is already a LibraryLoadException
+ throw e;
+ } catch (Throwable t) {
+ throw new LibraryLoadException(libName, t);
+ }
+
+ if (url == null) {
+ throw new LibraryLoadException(libName);
+ }
+ return toLibFile(platformSpec, libName, url);
+ }
+
+ private LibFile toLibFile(PlatformSpec platformSpec, String libName, URL url)
+ throws LibraryLoadException {
+ if (url.getProtocol().equals("file")) {
+ return LibFile.fromFile(libName, new File(url.getPath()));
+ } else {
+ String libExt = PathUtils.dynamicLibExtension(platformSpec);
+
+ try {
+ Path tempFile = TempFileHelper.createTempFile(this.tempDir, libName, libExt);
+
+ try (InputStream in = url.openStream()) {
+ Files.copy(in, tempFile, StandardCopyOption.REPLACE_EXISTING);
+ }
+
+ return LibFile.fromTempFile(libName, tempFile.toFile());
+ } catch (Throwable t) {
+ throw new LibraryLoadException(libName, t);
+ }
+ }
+ }
+
+ static void delete(File tempFile) {
+ TempFileHelper.delete(tempFile);
+ }
+
+ static final class TempFileHelper {
+ private TempFileHelper() {}
+
+ static Path createTempFile(Path tempDir, String libname, String libExt)
+ throws IOException, SecurityException {
+ FileAttribute> permAttrs =
+ PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"));
+
+ if (tempDir == null) {
+ return Files.createTempFile(libname, "." + libExt, permAttrs);
+ } else {
+ Files.createDirectories(
+ tempDir,
+ PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------")));
+
+ return Files.createTempFile(tempDir, libname, "." + libExt, permAttrs);
+ }
+ }
+
+ static void delete(File tempFile) {
+ boolean deleted = tempFile.delete();
+ if (!deleted) tempFile.deleteOnExit();
+ }
+ }
+}
diff --git a/components/native-loader/src/main/java/datadog/nativeloader/NestedDirLibraryResolver.java b/components/native-loader/src/main/java/datadog/nativeloader/NestedDirLibraryResolver.java
new file mode 100644
index 00000000000..966fd2c03e8
--- /dev/null
+++ b/components/native-loader/src/main/java/datadog/nativeloader/NestedDirLibraryResolver.java
@@ -0,0 +1,54 @@
+package datadog.nativeloader;
+
+import java.net.URL;
+
+/**
+ * NestedDirLibraryResolver - uses nested directories to provide more specific libraries to load
+ * {os} / {arch} / {libc}
+ */
+final class NestedDirLibraryResolver implements LibraryResolver {
+ public static final NestedDirLibraryResolver INSTANCE = new NestedDirLibraryResolver();
+
+ @Override
+ public final URL resolve(
+ PathLocator pathLocator, String component, PlatformSpec platformSpec, String libName)
+ throws Exception {
+ PathLocatorHelper pathLocatorHelper = new PathLocatorHelper(libName, pathLocator);
+
+ String libFileName = PathUtils.libFileName(platformSpec, libName);
+
+ String osPath = PathUtils.osPartOf(platformSpec);
+ String archPath = PathUtils.archPartOf(platformSpec);
+
+ String libcPath = PathUtils.libcPartOf(platformSpec);
+
+ URL url;
+ String regularPath = osPath + "/" + archPath;
+
+ if (libcPath != null) {
+ String specializedPath = regularPath + "/" + libcPath;
+ url = pathLocatorHelper.locate(component, specializedPath + "/" + libFileName);
+ if (url != null) return url;
+ }
+
+ url = pathLocatorHelper.locate(component, regularPath + "/" + libFileName);
+ if (url != null) return url;
+
+ url = pathLocatorHelper.locate(component, osPath + "/" + libFileName);
+ if (url != null) return url;
+
+ // fallback to searching at top-level, mostly concession to good out-of-box behavior
+ // with java.library.path
+ url = pathLocatorHelper.locate(component, libFileName);
+ if (url != null) return url;
+
+ if (component != null) {
+ url = pathLocatorHelper.locate(null, libFileName);
+ if (url != null) return url;
+ }
+
+ pathLocatorHelper.tryThrow();
+
+ return null;
+ }
+}
diff --git a/components/native-loader/src/main/java/datadog/nativeloader/PathLocator.java b/components/native-loader/src/main/java/datadog/nativeloader/PathLocator.java
new file mode 100644
index 00000000000..e7cc2e14f20
--- /dev/null
+++ b/components/native-loader/src/main/java/datadog/nativeloader/PathLocator.java
@@ -0,0 +1,18 @@
+package datadog.nativeloader;
+
+import java.net.URL;
+
+/** Resolves a component / path pair to a {@link URL} - called by a {@link LibraryResolver} */
+@FunctionalInterface
+public interface PathLocator {
+ /**
+ * URL to the requested resource -- or null if the resource could not be found
+ *
+ * If the returned URL uses file protocol, then {@link NativeLoader} will provide direct access
+ * to the file
+ *
+ *
If the returned URL uses a non-file protocol, then {@link NativeLoader} will call {@link
+ * URL#openStream()} and copy the contents to a temporary file
+ */
+ URL locate(String component, String path) throws Exception;
+}
diff --git a/components/native-loader/src/main/java/datadog/nativeloader/PathLocatorHelper.java b/components/native-loader/src/main/java/datadog/nativeloader/PathLocatorHelper.java
new file mode 100644
index 00000000000..38c976c5c86
--- /dev/null
+++ b/components/native-loader/src/main/java/datadog/nativeloader/PathLocatorHelper.java
@@ -0,0 +1,38 @@
+package datadog.nativeloader;
+
+import java.net.URL;
+
+/**
+ * Helper {@link PathLocator} that can be useful when doing multiple look-ups. PathLocatorHelper can
+ * be used to wrap another {@link PathLocator} and make the exception handling easier.
+ */
+public final class PathLocatorHelper implements PathLocator {
+ final String libName;
+ final PathLocator locator;
+
+ private Throwable firstCause;
+
+ public PathLocatorHelper(String libName, PathLocator locator) {
+ this.libName = libName;
+ this.locator = locator;
+ }
+
+ @Override
+ public URL locate(String component, String path) {
+ try {
+ return this.locator.locate(component, path);
+ } catch (Throwable t) {
+ if (this.firstCause == null) this.firstCause = t;
+ return null;
+ }
+ }
+
+ /** Raises a LibraryLoadException if an exception occurred during a prior call to locate */
+ public void tryThrow() throws LibraryLoadException {
+ if (this.firstCause instanceof LibraryLoadException) {
+ throw (LibraryLoadException) this.firstCause;
+ } else if (this.firstCause != null) {
+ throw new LibraryLoadException(this.libName, this.firstCause);
+ }
+ }
+}
diff --git a/components/native-loader/src/main/java/datadog/nativeloader/PathLocators.java b/components/native-loader/src/main/java/datadog/nativeloader/PathLocators.java
new file mode 100644
index 00000000000..f3b368f98ea
--- /dev/null
+++ b/components/native-loader/src/main/java/datadog/nativeloader/PathLocators.java
@@ -0,0 +1,58 @@
+package datadog.nativeloader;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.regex.Pattern;
+
+/** Helper factory class for common {@link PathLocator} */
+public final class PathLocators {
+ private PathLocators() {}
+
+ public static final PathLocator defaultPathLocator() {
+ return fromJavaLibraryPath();
+ }
+
+ public static final PathLocator fromLibDirs(Path... libDirs) {
+ File[] libDirFiles = new File[libDirs.length];
+ for (int i = 0; i < libDirs.length; ++i) {
+ libDirFiles[i] = libDirs[i].toFile();
+ }
+ return fromLibDirs(libDirFiles);
+ }
+
+ public static final PathLocator fromLibDirs(String... libDirs) {
+ File[] libDirFiles = new File[libDirs.length];
+ for (int i = 0; i < libDirs.length; ++i) {
+ libDirFiles[i] = new File(libDirs[i]);
+ }
+ return fromLibDirs(libDirFiles);
+ }
+
+ public static final PathLocator fromLibDirs(File... libDirs) {
+ return new LibDirBasedPathLocator(libDirs);
+ }
+
+ public static final PathLocator fromJavaLibraryPath() {
+ String libPaths;
+ try {
+ libPaths = System.getProperty("java.library.path");
+ } catch (SecurityException e) {
+ return new LibDirBasedPathLocator();
+ }
+ return fromLibPathString(libPaths);
+ }
+
+ public static final PathLocator fromLibPathString(String javaLibPath) {
+ // Typically, this method will be called at most once per run,
+ // so not storing pattern in a static because we don't want memory consumed forever
+ return fromLibDirs(Pattern.compile("\\:").split(javaLibPath));
+ }
+
+ public static final PathLocator fromClassLoader(ClassLoader classLoader) {
+ return new ClassLoaderResourcePathLocator(classLoader, null);
+ }
+
+ public static final PathLocator fromClassLoader(ClassLoader classLoader, String baseResource) {
+ return new ClassLoaderResourcePathLocator(classLoader, baseResource);
+ }
+}
diff --git a/components/native-loader/src/main/java/datadog/nativeloader/PathUtils.java b/components/native-loader/src/main/java/datadog/nativeloader/PathUtils.java
new file mode 100644
index 00000000000..66e69ed357f
--- /dev/null
+++ b/components/native-loader/src/main/java/datadog/nativeloader/PathUtils.java
@@ -0,0 +1,98 @@
+package datadog.nativeloader;
+
+import static datadog.nativeloader.LibraryLoadException.UNSUPPORTED_ARCH;
+import static datadog.nativeloader.LibraryLoadException.UNSUPPORTED_OS;
+
+/** Utilities for generating library file paths to be requested from a {@link PathLocator} */
+public final class PathUtils {
+ private PathUtils() {}
+
+ public static final String libPrefix(PlatformSpec platformSpec) {
+ if (platformSpec.isMac() || platformSpec.isLinux()) {
+ return "lib";
+ } else if (platformSpec.isWindows()) {
+ return "";
+ } else {
+ throw new IllegalArgumentException(UNSUPPORTED_OS);
+ }
+ }
+
+ public static final String libFileName(PlatformSpec platformSpec, String libName) {
+ return libPrefix(platformSpec) + libName + "." + dynamicLibExtension(platformSpec);
+ }
+
+ public static final String dynamicLibExtension(PlatformSpec platformSpec) {
+ if (platformSpec.isLinux()) {
+ return "so";
+ } else if (platformSpec.isWindows()) {
+ return "dll";
+ } else if (platformSpec.isMac()) {
+ return "dylib";
+ } else {
+ throw new IllegalArgumentException(UNSUPPORTED_OS);
+ }
+ }
+
+ public static final String osPartOf(PlatformSpec platformSpec) {
+ if (platformSpec.isLinux()) {
+ return "linux";
+ } else if (platformSpec.isWindows()) {
+ return "win";
+ } else if (platformSpec.isMac()) {
+ return "macos";
+ } else {
+ throw new IllegalArgumentException(UNSUPPORTED_OS);
+ }
+ }
+
+ public static final String archPartOf(PlatformSpec platformSpec) {
+ if (platformSpec.isX86_64()) {
+ return "x86_64";
+ } else if (platformSpec.isAarch64()) {
+ return "aarch64";
+ } else if (platformSpec.isX86_32()) {
+ return "x86_32";
+ } else if (platformSpec.isArm32()) {
+ return "arm32";
+ } else {
+ throw new IllegalArgumentException(UNSUPPORTED_ARCH);
+ }
+ }
+
+ public static final String libcPartOf(PlatformSpec platformSpec) {
+ if (!platformSpec.isLinux()) {
+ return null;
+ } else if (platformSpec.isMusl()) {
+ return "musl";
+ } else {
+ return "libc";
+ }
+ }
+
+ /** Helper for concatenating paths with / Handles null & empty for both parts */
+ public static final String concatPath(String pathPart1, String pathPart2) {
+ if (isEmpty(pathPart1)) {
+ return pathPart2;
+ } else if (isEmpty(pathPart2)) {
+ return pathPart1;
+ } else {
+ return pathPart1 + "/" + pathPart2;
+ }
+ }
+
+ /** Helper for concatenating parts with / Handles null & empty anywhere in the var-arg array */
+ public static final String concatPath(String... pathParts) {
+ StringBuilder builder = new StringBuilder();
+ for (String pathPart : pathParts) {
+ if (isEmpty(pathPart)) continue;
+
+ if (builder.length() != 0) builder.append('/');
+ builder.append(pathPart);
+ }
+ return builder.toString();
+ }
+
+ static final boolean isEmpty(String pathPart) {
+ return (pathPart == null) || pathPart.isEmpty();
+ }
+}
diff --git a/components/native-loader/src/main/java/datadog/nativeloader/PlatformSpec.java b/components/native-loader/src/main/java/datadog/nativeloader/PlatformSpec.java
new file mode 100644
index 00000000000..c8e0a9e5902
--- /dev/null
+++ b/components/native-loader/src/main/java/datadog/nativeloader/PlatformSpec.java
@@ -0,0 +1,45 @@
+package datadog.nativeloader;
+
+/**
+ * PlatformSpec describes a native library "platform" -- including operating system, architecture,
+ * and libc variation
+ */
+public abstract class PlatformSpec {
+ public static final PlatformSpec defaultPlatformSpec() {
+ return IntrospectPlatformSpec.INSTANCE;
+ }
+
+ /** Is the target OS MacOS? */
+ public abstract boolean isMac();
+
+ /** Is the target OS Windows? */
+ public abstract boolean isWindows();
+
+ /** Is the target OS Linux? */
+ public abstract boolean isLinux();
+
+ /** Is the target OS unknown / not handled by {@link NativeLoader}? */
+ public final boolean isUnknownOs() {
+ return !this.isLinux() && !this.isMac() && !this.isWindows();
+ }
+
+ /** Is the target architecture x86-32 bit? */
+ public abstract boolean isX86_32();
+
+ /** Is the target architecture x86-64 bit? */
+ public abstract boolean isX86_64();
+
+ /** Is the target architecture ARM 32-bit? */
+ public abstract boolean isArm32();
+
+ /** Is the target architecture ARM 64-bit? */
+ public abstract boolean isAarch64();
+
+ /** Is the target architecture unknown / not handled by {@link NativeLoader}? */
+ public final boolean isUnknownArch() {
+ return !this.isX86_64() && !this.isAarch64() && !this.isX86_32() && !this.isArm32();
+ }
+
+ /** Is the target using MUSL libc? */
+ public abstract boolean isMusl();
+}
diff --git a/components/native-loader/src/test/java/datadog/nativeloader/CapturingPathLocator.java b/components/native-loader/src/test/java/datadog/nativeloader/CapturingPathLocator.java
new file mode 100644
index 00000000000..257e5ba9958
--- /dev/null
+++ b/components/native-loader/src/test/java/datadog/nativeloader/CapturingPathLocator.java
@@ -0,0 +1,143 @@
+package datadog.nativeloader;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Deque;
+import java.util.LinkedList;
+
+public final class CapturingPathLocator implements PathLocator {
+ public static final boolean WITH_OMIT_COMP_FALLBACK = true;
+ public static final boolean WITHOUT_OMIT_COMP_FALLBACK = false;
+
+ public static final void testFailOnExceptions(
+ LibraryResolver resolver,
+ PlatformSpec platformSpec,
+ boolean withSkipCompFallback,
+ String... expectedPaths) {
+ try {
+ test(resolver, platformSpec, withSkipCompFallback, expectedPaths);
+ } catch (Exception e) {
+ throw new IllegalStateException("unexpected exception", e);
+ }
+ }
+
+ public static final void test(
+ LibraryResolver resolver,
+ PlatformSpec platformSpec,
+ boolean withSkipCompFallback,
+ String... expectedPaths)
+ throws Exception {
+ String comp = "comp";
+
+ CapturingPathLocator fullCaptureLocator = new CapturingPathLocator(Integer.MAX_VALUE);
+ resolver.resolve(fullCaptureLocator, comp, platformSpec, "test");
+
+ for (int i = 0; !fullCaptureLocator.isEmpty(); ++i) {
+ if (i >= expectedPaths.length) {
+ // checking the final fallback here was confusing when debugging tests
+
+ if (!withSkipCompFallback) {
+ fullCaptureLocator.assertDone();
+ } else {
+ // checked at at the end of the method
+ fullCaptureLocator.nextRequest();
+ fullCaptureLocator.assertDone();
+ }
+ } else {
+ fullCaptureLocator.assertRequested(comp, expectedPaths[i]);
+ }
+ }
+
+ for (int i = 0; i < expectedPaths.length; ++i) {
+ CapturingPathLocator fallbackLocator = new CapturingPathLocator(i);
+ resolver.resolve(fallbackLocator, comp, platformSpec, "test");
+
+ for (int j = 0; j <= i; ++j) {
+ fallbackLocator.assertRequested(comp, expectedPaths[j]);
+ }
+ fallbackLocator.assertDone();
+ }
+
+ if (withSkipCompFallback) {
+ CapturingPathLocator fallbackLocator = new CapturingPathLocator(expectedPaths.length);
+ resolver.resolve(fallbackLocator, comp, platformSpec, "test");
+
+ for (int j = 0; j < expectedPaths.length; ++j) {
+ fallbackLocator.assertRequested(comp, expectedPaths[j]);
+ }
+ fallbackLocator.assertRequested(null, expectedPaths[expectedPaths.length - 1]);
+ fallbackLocator.assertDone();
+ }
+ }
+
+ final int simulateNotFoundCount;
+ int numRequests;
+
+ final Deque locateRequests = new LinkedList<>();
+
+ public CapturingPathLocator() {
+ this(0);
+ }
+
+ public CapturingPathLocator(int simulateNotFoundCount) {
+ this.numRequests = 0;
+ this.simulateNotFoundCount = simulateNotFoundCount;
+ }
+
+ @Override
+ public URL locate(String component, String path) {
+ this.locateRequests.addLast(new LocateRequest(component, path));
+
+ if (this.numRequests++ < this.simulateNotFoundCount) return null;
+ try {
+ return new URL("http://localhost");
+ } catch (MalformedURLException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ int numLocateRequests() {
+ return this.locateRequests.size();
+ }
+
+ boolean isEmpty() {
+ return this.locateRequests.isEmpty();
+ }
+
+ LocateRequest nextRequest() {
+ return this.locateRequests.removeFirst();
+ }
+
+ void assertRequested(LocateRequest request) {
+ this.assertRequested(request.requestedComponent, request.requestedPath);
+ }
+
+ void assertRequested(String expectedComponent, String expectedPath) {
+ this.nextRequest().assertRequested(expectedComponent, expectedPath);
+ }
+
+ void assertDone() {
+ // written this way to aid in debugging and fleshing out the test
+ if (!this.isEmpty()) {
+ this.assertRequested("", "");
+ }
+ }
+
+ final class LocateRequest {
+ private final String requestedComponent;
+ private final String requestedPath;
+
+ LocateRequest(String requestedComponent, String requestedPath) {
+ this.requestedComponent = requestedComponent;
+ this.requestedPath = requestedPath;
+ }
+
+ void assertRequested(String expectedComponent, String expectedPath) {
+ // order is inverted, since usually test writer is worrying about comp directly
+ assertEquals(expectedPath, this.requestedPath);
+ assertEquals(expectedComponent, this.requestedComponent);
+ }
+ }
+}
diff --git a/components/native-loader/src/test/java/datadog/nativeloader/FlatDirLibraryResolverTest.java b/components/native-loader/src/test/java/datadog/nativeloader/FlatDirLibraryResolverTest.java
new file mode 100644
index 00000000000..2438770288e
--- /dev/null
+++ b/components/native-loader/src/test/java/datadog/nativeloader/FlatDirLibraryResolverTest.java
@@ -0,0 +1,64 @@
+package datadog.nativeloader;
+
+import static datadog.nativeloader.TestPlatformSpec.AARCH64;
+import static datadog.nativeloader.TestPlatformSpec.GLIBC;
+import static datadog.nativeloader.TestPlatformSpec.LINUX;
+import static datadog.nativeloader.TestPlatformSpec.MAC;
+import static datadog.nativeloader.TestPlatformSpec.MUSL;
+import static datadog.nativeloader.TestPlatformSpec.WINDOWS;
+import static datadog.nativeloader.TestPlatformSpec.X86_64;
+
+import org.junit.jupiter.api.Test;
+
+public class FlatDirLibraryResolverTest {
+ @Test
+ public void linux_x86_64_libc() {
+ test(
+ TestPlatformSpec.of(LINUX, X86_64, GLIBC),
+ "linux-x86_64-libc/libtest.so",
+ "linux-x86_64/libtest.so",
+ "linux/libtest.so",
+ "libtest.so");
+ }
+
+ @Test
+ public void linux_x86_64_musl() {
+ test(
+ TestPlatformSpec.of(LINUX, X86_64, MUSL),
+ "linux-x86_64-musl/libtest.so",
+ "linux-x86_64/libtest.so",
+ "linux/libtest.so",
+ "libtest.so");
+ }
+
+ @Test
+ public void osx_x86_64() {
+ test(
+ TestPlatformSpec.of(MAC, X86_64),
+ "macos-x86_64/libtest.dylib",
+ "macos/libtest.dylib",
+ "libtest.dylib");
+ }
+
+ @Test
+ public void osx_aarch64() {
+ test(
+ TestPlatformSpec.of(MAC, AARCH64),
+ "macos-aarch64/libtest.dylib",
+ "macos/libtest.dylib",
+ "libtest.dylib");
+ }
+
+ @Test
+ public void windows_x86_64() {
+ test(TestPlatformSpec.of(WINDOWS, X86_64), "win-x86_64/test.dll", "win/test.dll", "test.dll");
+ }
+
+ static final void test(PlatformSpec platformSpec, String... expectedPaths) {
+ CapturingPathLocator.testFailOnExceptions(
+ FlatDirLibraryResolver.INSTANCE,
+ platformSpec,
+ CapturingPathLocator.WITH_OMIT_COMP_FALLBACK,
+ expectedPaths);
+ }
+}
diff --git a/components/native-loader/src/test/java/datadog/nativeloader/IntrospectPlatformSpecTest.java b/components/native-loader/src/test/java/datadog/nativeloader/IntrospectPlatformSpecTest.java
new file mode 100644
index 00000000000..75da5ae18cd
--- /dev/null
+++ b/components/native-loader/src/test/java/datadog/nativeloader/IntrospectPlatformSpecTest.java
@@ -0,0 +1,50 @@
+package datadog.nativeloader;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import datadog.environment.OperatingSystem;
+import datadog.environment.OperatingSystem.Architecture;
+import org.junit.jupiter.api.Test;
+
+public class IntrospectPlatformSpecTest {
+ @Test
+ public void os() {
+ PlatformSpec platformSpec = IntrospectPlatformSpec.INSTANCE;
+
+ // a bit silly since this just mirrors the underlying implementation
+ assertEquals(OperatingSystem.isMacOs(), platformSpec.isMac());
+ assertEquals(OperatingSystem.isLinux(), platformSpec.isLinux());
+ assertEquals(OperatingSystem.isWindows(), platformSpec.isWindows());
+ }
+
+ @Test
+ public void arch() {
+ PlatformSpec platformSpec = IntrospectPlatformSpec.INSTANCE;
+
+ assertEquals(OperatingSystem.architecture() == Architecture.X86, platformSpec.isX86_32());
+ assertEquals(OperatingSystem.architecture() == Architecture.X64, platformSpec.isX86_64());
+ assertEquals(OperatingSystem.architecture() == Architecture.ARM, platformSpec.isArm32());
+ assertEquals(OperatingSystem.architecture() == Architecture.ARM64, platformSpec.isAarch64());
+ }
+
+ @Test
+ public void musl() {
+ PlatformSpec platformSpec = IntrospectPlatformSpec.INSTANCE;
+
+ assertEquals(OperatingSystem.isMusl(), platformSpec.isMusl());
+ }
+
+ @Test
+ public void equals() {
+ // just a sanity check, since assertEquals is used in other tests
+ assertTrue(IntrospectPlatformSpec.INSTANCE.equals(IntrospectPlatformSpec.INSTANCE));
+ }
+
+ @Test
+ public void notEquals_diffType() {
+ // just a sanity check, since assertEquals is used in other tests
+ assertFalse(IntrospectPlatformSpec.INSTANCE.equals(TestPlatformSpec.unsupportedOs()));
+ }
+}
diff --git a/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderBuilderTest.java b/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderBuilderTest.java
new file mode 100644
index 00000000000..db04ea859e1
--- /dev/null
+++ b/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderBuilderTest.java
@@ -0,0 +1,141 @@
+package datadog.nativeloader;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.nio.file.Paths;
+import org.junit.jupiter.api.Test;
+
+public class NativeLoaderBuilderTest {
+ @Test
+ public void defaultPlatformSpec() {
+ NativeLoader.Builder builder = NativeLoader.builder();
+
+ assertEquals(PlatformSpec.defaultPlatformSpec(), builder.platformSpec());
+ }
+
+ @Test
+ public void customPlatformSpec() {
+ PlatformSpec platformSpec =
+ TestPlatformSpec.of(TestPlatformSpec.LINUX, TestPlatformSpec.ARM32, TestPlatformSpec.GLIBC);
+
+ NativeLoader.Builder builder = NativeLoader.builder().platformSpec(platformSpec);
+ assertSame(platformSpec, builder.platformSpec());
+ }
+
+ @Test
+ public void defaultLibraryResolver() {
+ NativeLoader.Builder builder = NativeLoader.builder();
+
+ assertEquals(LibraryResolvers.defaultLibraryResolver(), builder.libResolver());
+ }
+
+ @Test
+ public void flatLayout() {
+ NativeLoader.Builder builder = NativeLoader.builder().flatLayout();
+
+ assertEquals(LibraryResolvers.flatDirs(), builder.libResolver());
+ }
+
+ @Test
+ public void nestedLayout() {
+ NativeLoader.Builder builder = NativeLoader.builder().nestedLayout();
+
+ assertEquals(LibraryResolvers.nestedDirs(), builder.libResolver());
+ }
+
+ @Test
+ public void preloaded() {
+ PlatformSpec platformSpec =
+ TestPlatformSpec.of(TestPlatformSpec.LINUX, TestPlatformSpec.ARM32, TestPlatformSpec.GLIBC);
+
+ NativeLoader.Builder builder =
+ NativeLoader.builder().platformSpec(platformSpec).preloaded("foo", "bar");
+
+ LibraryResolver libResolver = builder.libResolver();
+ assertTrue(libResolver.isPreloaded(platformSpec, "foo"));
+ assertTrue(libResolver.isPreloaded(platformSpec, "bar"));
+
+ assertFalse(libResolver.isPreloaded(platformSpec, "not-preloaded"));
+ }
+
+ @Test
+ public void defaultPathLocator() {
+ NativeLoader.Builder builder = NativeLoader.builder();
+
+ assertEquals(PathLocators.defaultPathLocator(), builder.pathLocator());
+ }
+
+ @Test
+ public void dirBasedLocator_string() {
+ NativeLoader.Builder builder = NativeLoader.builder().fromDir("libs");
+
+ assertEquals(PathLocators.fromLibDirs("libs"), builder.pathLocator());
+ }
+
+ @Test
+ public void dirBasedLocator_strings_multiple() {
+ NativeLoader.Builder builder = NativeLoader.builder().fromDirs("libs1", "libs2");
+
+ assertEquals(PathLocators.fromLibDirs("libs1", "libs2"), builder.pathLocator());
+ }
+
+ @Test
+ public void dirBasedLocator_file() {
+ NativeLoader.Builder builder = NativeLoader.builder().fromDir(new File("libs"));
+
+ assertEquals(PathLocators.fromLibDirs("libs"), builder.pathLocator());
+ }
+
+ @Test
+ public void dirBasedLocator_files_multiple() {
+ NativeLoader.Builder builder =
+ NativeLoader.builder().fromDirs(new File("libs1"), new File("libs2"));
+
+ assertEquals(PathLocators.fromLibDirs("libs1", "libs2"), builder.pathLocator());
+ }
+
+ @Test
+ public void dirBasedLocator_path() {
+ NativeLoader.Builder builder = NativeLoader.builder().fromDir(Paths.get("libs"));
+
+ assertEquals(PathLocators.fromLibDirs("libs"), builder.pathLocator());
+ }
+
+ @Test
+ public void dirBasedLocator_paths_multiple() {
+ NativeLoader.Builder builder =
+ NativeLoader.builder().fromDirs(Paths.get("libs1"), Paths.get("libs2"));
+
+ assertEquals(PathLocators.fromLibDirs("libs1", "libs2"), builder.pathLocator());
+ }
+
+ @Test
+ public void classLoaderBasedLocator() {
+ ClassLoader sysClassLoader = ClassLoader.getSystemClassLoader();
+ NativeLoader.Builder builder = NativeLoader.builder().fromClassLoader(sysClassLoader);
+
+ assertEquals(PathLocators.fromClassLoader(sysClassLoader), builder.pathLocator());
+ }
+
+ @Test
+ public void tempDir_string() {
+ NativeLoader.Builder builder = NativeLoader.builder().tempDir("tmp");
+ assertEquals(builder.tempDir(), Paths.get("tmp"));
+ }
+
+ @Test
+ public void tempDir_file() {
+ NativeLoader.Builder builder = NativeLoader.builder().tempDir(new File("tmp"));
+ assertEquals(builder.tempDir(), Paths.get("tmp"));
+ }
+
+ @Test
+ public void tempDir_path() {
+ NativeLoader.Builder builder = NativeLoader.builder().tempDir(Paths.get("tmp"));
+ assertEquals(builder.tempDir(), Paths.get("tmp"));
+ }
+}
diff --git a/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderTest.java b/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderTest.java
new file mode 100644
index 00000000000..a122e88bea4
--- /dev/null
+++ b/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderTest.java
@@ -0,0 +1,392 @@
+package datadog.nativeloader;
+
+import static datadog.nativeloader.TestPlatformSpec.AARCH64;
+import static datadog.nativeloader.TestPlatformSpec.LINUX;
+import static datadog.nativeloader.TestPlatformSpec.UNSUPPORTED_ARCH;
+import static datadog.nativeloader.TestPlatformSpec.UNSUPPORTED_OS;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.util.Set;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+import org.junit.jupiter.api.Test;
+
+public class NativeLoaderTest {
+ @Test
+ public void preloaded() throws LibraryLoadException {
+ NativeLoader loader = NativeLoader.builder().preloaded("dne1", "dne2").build();
+
+ assertTrue(loader.isPreloaded("dne1"));
+ assertTrue(loader.isPreloaded("dne2"));
+
+ assertFalse(loader.isPreloaded("dne3"));
+
+ try (LibFile lib = loader.resolveDynamic("dne1")) {
+ assertPreloaded(lib);
+
+ // already considered loaded -- so this is a nop
+ lib.load();
+ }
+
+ // already considered loaded -- so this is a nop
+ loader.load("dne2");
+
+ // not already loaded - so passes through to underlying resolver
+ assertThrows(LibraryLoadException.class, () -> loader.load("dne3"));
+ }
+
+ @Test
+ public void unsupportedPlatform() {
+ PlatformSpec unsupportedOsSpec = TestPlatformSpec.of(UNSUPPORTED_OS, AARCH64);
+ NativeLoader loader = NativeLoader.builder().platformSpec(unsupportedOsSpec).build();
+
+ assertThrows(LibraryLoadException.class, () -> loader.resolveDynamic("dummy"));
+ }
+
+ @Test
+ public void unsupportArch() {
+ PlatformSpec unsupportedOsSpec = TestPlatformSpec.of(LINUX, UNSUPPORTED_ARCH);
+ NativeLoader loader = NativeLoader.builder().platformSpec(unsupportedOsSpec).build();
+
+ assertThrows(LibraryLoadException.class, () -> loader.resolveDynamic("dummy"));
+ }
+
+ @Test
+ public void loadFailure() throws LibraryLoadException {
+ NativeLoader loader = NativeLoader.builder().build();
+
+ // test libraries are just text files, so they shouldn't load & link properly
+ // NativeLoader is supposed to wrap the loading failures, so that we
+ // remember to handle them
+ assertThrows(LibraryLoadException.class, () -> loader.load("dummy"));
+ }
+
+ @Test
+ public void fromDir() throws LibraryLoadException {
+ NativeLoader loader = NativeLoader.builder().fromDir("test-data").build();
+
+ try (LibFile lib = loader.resolveDynamic("dummy")) {
+ // loaded directly from directory, so no clean-up required
+ assertRegularFile(lib);
+
+ // file isn't actually a dynamic library
+ assertThrows(LibraryLoadException.class, () -> lib.load());
+ }
+ }
+
+ @Test
+ public void fromDir_override_windows() throws LibraryLoadException {
+ NativeLoader loader = NativeLoader.builder().fromDir("test-data").build();
+
+ try (LibFile lib = loader.resolveDynamic(TestPlatformSpec.windows(), "dummy")) {
+ // loaded directly from directory, so no clean-up required
+ assertRegularFile(lib);
+ assertTrue(lib.getAbsolutePath().endsWith("dummy.dll"));
+ }
+ }
+
+ @Test
+ public void fromDir_override_mac() throws LibraryLoadException {
+ NativeLoader loader = NativeLoader.builder().fromDir("test-data").build();
+
+ try (LibFile lib = loader.resolveDynamic(TestPlatformSpec.mac(), "dummy")) {
+ // loaded directly from directory, so no clean-up required
+ assertRegularFile(lib);
+ assertTrue(lib.getAbsolutePath().endsWith("libdummy.dylib"));
+ }
+ }
+
+ @Test
+ public void fromDir_override_linux() throws LibraryLoadException {
+ NativeLoader loader = NativeLoader.builder().fromDir("test-data").build();
+
+ try (LibFile lib = loader.resolveDynamic(TestPlatformSpec.linux(), "dummy")) {
+ // loaded directly from directory, so no clean-up required
+ assertRegularFile(lib);
+ assertTrue(lib.getAbsolutePath().endsWith("libdummy.so"));
+ }
+ }
+
+ @Test
+ public void fromDirList() throws LibraryLoadException {
+ NativeLoader loader = NativeLoader.builder().fromDirs("dne1", "dne2", "test-data").build();
+
+ try (LibFile lib = loader.resolveDynamic("dummy")) {
+ // loaded directly from directory, so no clean-up required
+ assertRegularFile(lib);
+ }
+ }
+
+ @Test
+ public void fromDir_with_component() throws LibraryLoadException {
+ NativeLoader loader = NativeLoader.builder().fromDir("test-data").build();
+
+ try (LibFile lib = loader.resolveDynamic("comp1", "dummy")) {
+ assertRegularFile(lib);
+ assertTrue(lib.getAbsolutePath().contains("comp1"));
+ }
+
+ try (LibFile lib = loader.resolveDynamic("comp2", "dummy")) {
+ assertRegularFile(lib);
+ assertTrue(lib.getAbsolutePath().contains("comp2"));
+ }
+ }
+
+ @Test
+ public void fromDirBackedClassLoader() throws IOException, LibraryLoadException {
+ // ClassLoader pulling from a directory, so there's still a normal file
+ URL[] urls = {new File("test-data").toURL()};
+
+ try (URLClassLoader classLoader = new URLClassLoader(urls)) {
+ NativeLoader loader = NativeLoader.builder().fromClassLoader(classLoader).build();
+ try (LibFile lib = loader.resolveDynamic("dummy")) {
+ // since there's a normal file, no need to copy to a temp file and clean-up
+ assertRegularFile(lib);
+ }
+ }
+ }
+
+ @Test
+ public void fromDirBackedClassLoader_with_component() throws IOException, LibraryLoadException {
+ // ClassLoader pulling from a directory, so there's still a normal file
+ URL[] urls = {new File("test-data").toURL()};
+
+ try (URLClassLoader classLoader = new URLClassLoader(urls)) {
+ NativeLoader loader = NativeLoader.builder().fromClassLoader(classLoader).build();
+
+ try (LibFile lib = loader.resolveDynamic("comp1", "dummy")) {
+ assertRegularFile(lib);
+ assertTrue(lib.getAbsolutePath().contains("comp1"));
+ }
+
+ try (LibFile lib = loader.resolveDynamic("comp2", "dummy")) {
+ assertRegularFile(lib);
+ assertTrue(lib.getAbsolutePath().contains("comp2"));
+ }
+ }
+ }
+
+ @Test
+ public void fromDirBackedClassLoader_with_subResource() throws IOException, LibraryLoadException {
+ // ClassLoader pulling from a directory, so there's still a normal file
+ URL[] urls = {new File("test-data").toURL()};
+
+ try (URLClassLoader classLoader = new URLClassLoader(urls)) {
+ NativeLoader loader = NativeLoader.builder().fromClassLoader(classLoader, "resource").build();
+ try (LibFile lib = loader.resolveDynamic("dummy")) {
+ // since there's a normal file, no need to copy to a temp file and clean-up
+ assertRegularFile(lib);
+ assertTrue(lib.getAbsolutePath().contains("resource"));
+ }
+ }
+ }
+
+ @Test
+ public void fromDirBackedClassLoader_with_subResource_and_comp()
+ throws IOException, LibraryLoadException {
+ // ClassLoader pulling from a directory, so there's still a normal file
+ try (URLClassLoader classLoader = createClassLoader(Paths.get("test-data"))) {
+ NativeLoader loader = NativeLoader.builder().fromClassLoader(classLoader, "resource").build();
+ try (LibFile lib = loader.resolveDynamic("comp1", "dummy")) {
+ // since there's a normal file, no need to copy to a temp file and clean-up
+ assertRegularFile(lib);
+ assertTrue(lib.getAbsolutePath().contains("comp1"));
+ assertTrue(lib.getAbsolutePath().contains("resource"));
+ }
+ }
+ }
+
+ @Test
+ public void fromJarBackedClassLoader() throws IOException, LibraryLoadException {
+ Path jar = jar("test-data");
+ try {
+ try (URLClassLoader classLoader = createClassLoader(jar)) {
+ NativeLoader loader = NativeLoader.builder().fromClassLoader(classLoader).build();
+ try (LibFile lib = loader.resolveDynamic("dummy")) {
+ // loaded from a jar, so copied to temp file
+ assertTempFile(lib);
+ }
+ }
+ } finally {
+ deleteHelper(jar);
+ }
+ }
+
+ @Test
+ public void fromJarBackedClassLoader_with_tempDir() throws IOException, LibraryLoadException {
+ Path jar = jar("test-data");
+ try {
+ Path tempDir = Paths.get("temp");
+ deleteHelper(tempDir);
+
+ try (URLClassLoader classLoader = createClassLoader(jar)) {
+ NativeLoader loader =
+ NativeLoader.builder().fromClassLoader(classLoader).tempDir(tempDir).build();
+ try (LibFile lib = loader.resolveDynamic("dummy")) {
+ // loaded from a jar, so copied to temp file
+ assertTempFile(lib);
+ }
+ } finally {
+ deleteHelper(tempDir);
+ }
+ } finally {
+ deleteHelper(jar);
+ }
+ }
+
+ @Test
+ public void fromJarBackedClassLoader_with_unwritable_tempDir()
+ throws IOException, LibraryLoadException {
+ Path jar = jar("test-data");
+ try {
+ Path noWriteDir = Paths.get("no-write-temp");
+ deleteHelper(noWriteDir);
+ Files.createDirectories(noWriteDir, posixAttr("r-x------"));
+
+ try (URLClassLoader classLoader = createClassLoader(jar)) {
+ NativeLoader loader =
+ NativeLoader.builder().fromClassLoader(classLoader).tempDir(noWriteDir).build();
+
+ // unable to resolve to a File because tempDir isn't writable
+ assertThrows(LibraryLoadException.class, () -> loader.resolveDynamic("dummy"));
+ } finally {
+ deleteHelper(noWriteDir);
+ }
+ } finally {
+ deleteHelper(jar);
+ }
+ }
+
+ @Test
+ public void fromJarBackedClassLoader_with_locked_file() throws IOException, LibraryLoadException {
+ Path jar = jar("test-data");
+ try {
+ Path tempDir = Paths.get("temp");
+ deleteHelper(tempDir);
+
+ try (URLClassLoader classLoader = createClassLoader(jar)) {
+ NativeLoader loader =
+ NativeLoader.builder().fromClassLoader(classLoader).tempDir(tempDir).build();
+ try (LibFile lib = loader.resolveDynamic("dummy")) {
+ // loaded from a jar, so copied to temp file
+ assertTempFile(lib);
+
+ // simulating lock - by blocking ability to delete from parent dir
+ // forces fallback to deleteOnExit
+ Files.setPosixFilePermissions(lib.toPath().getParent(), posixPerms("r-x------"));
+ }
+ } finally {
+ deleteHelper(tempDir);
+ }
+ } finally {
+ deleteHelper(jar);
+ }
+ }
+
+ void deleteHelper(Path dir) {
+ try {
+ Files.setPosixFilePermissions(dir, posixPerms("rwx------"));
+ Files.delete(dir);
+ } catch (IOException e) {
+ }
+ }
+
+ static URLClassLoader createClassLoader(Path... paths) {
+ return new URLClassLoader(urls(paths));
+ }
+
+ static URL[] urls(Path... paths) {
+ URL[] urls = new URL[paths.length];
+ for (int i = 0; i < urls.length; ++i) {
+ try {
+ urls[i] = paths[i].toUri().toURL();
+ } catch (MalformedURLException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ return urls;
+ }
+
+ static Path jar(String dirName) {
+ return jar(Paths.get(dirName));
+ }
+
+ static Path jar(Path dir) {
+ try {
+ return jarHelper(dir);
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ static Path jarHelper(Path dir) throws IOException {
+ Path jarPath = Files.createTempFile(dir.toFile().getName(), ".jar", posixAttr("rwx------"));
+
+ try (JarOutputStream jarStream = new JarOutputStream(Files.newOutputStream(jarPath))) {
+ Files.walk(dir)
+ .filter(path -> !Files.isDirectory(path))
+ .forEach(
+ path -> {
+ try {
+ JarEntry jarEntry = new JarEntry(dir.relativize(path).toString());
+ jarStream.putNextEntry(jarEntry);
+ Files.copy(path, jarStream);
+ jarStream.closeEntry();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ });
+ }
+ return jarPath;
+ }
+
+ static FileAttribute> posixAttr(String posixStr) {
+ return PosixFilePermissions.asFileAttribute(posixPerms(posixStr));
+ }
+
+ static Set posixPerms(String posixStr) {
+ return PosixFilePermissions.fromString(posixStr);
+ }
+
+ static void assertPreloaded(LibFile lib) {
+ assertTrue(lib.isPreloaded());
+ assertNull(lib.toFile());
+ assertNull(lib.toPath());
+ assertNull(lib.getAbsolutePath());
+ assertFalse(lib.needsCleanup);
+ }
+
+ static void assertRegularFile(LibFile lib) {
+ assertFalse(lib.isPreloaded());
+ assertNotNull(lib.toFile());
+ assertNotNull(lib.toPath());
+ assertTrue(lib.toFile().exists());
+ assertNotNull(lib.getAbsolutePath());
+ assertFalse(lib.needsCleanup);
+ }
+
+ static void assertTempFile(LibFile lib) {
+ assertFalse(lib.isPreloaded());
+ assertNotNull(lib.toFile());
+ assertNotNull(lib.toPath());
+ assertTrue(lib.toFile().exists());
+ assertNotNull(lib.getAbsolutePath());
+ assertTrue(lib.needsCleanup);
+ }
+}
diff --git a/components/native-loader/src/test/java/datadog/nativeloader/NestedDirLibraryResolverTest.java b/components/native-loader/src/test/java/datadog/nativeloader/NestedDirLibraryResolverTest.java
new file mode 100644
index 00000000000..a85e68a269a
--- /dev/null
+++ b/components/native-loader/src/test/java/datadog/nativeloader/NestedDirLibraryResolverTest.java
@@ -0,0 +1,64 @@
+package datadog.nativeloader;
+
+import static datadog.nativeloader.TestPlatformSpec.AARCH64;
+import static datadog.nativeloader.TestPlatformSpec.GLIBC;
+import static datadog.nativeloader.TestPlatformSpec.LINUX;
+import static datadog.nativeloader.TestPlatformSpec.MAC;
+import static datadog.nativeloader.TestPlatformSpec.MUSL;
+import static datadog.nativeloader.TestPlatformSpec.WINDOWS;
+import static datadog.nativeloader.TestPlatformSpec.X86_64;
+
+import org.junit.jupiter.api.Test;
+
+public class NestedDirLibraryResolverTest {
+ @Test
+ public void linux_x86_64_libc() {
+ test(
+ TestPlatformSpec.of(LINUX, X86_64, GLIBC),
+ "linux/x86_64/libc/libtest.so",
+ "linux/x86_64/libtest.so",
+ "linux/libtest.so",
+ "libtest.so");
+ }
+
+ @Test
+ public void linux_x86_64_musl() {
+ test(
+ TestPlatformSpec.of(LINUX, X86_64, MUSL),
+ "linux/x86_64/musl/libtest.so",
+ "linux/x86_64/libtest.so",
+ "linux/libtest.so",
+ "libtest.so");
+ }
+
+ @Test
+ public void osx_x86_64() {
+ test(
+ TestPlatformSpec.of(MAC, X86_64),
+ "macos/x86_64/libtest.dylib",
+ "macos/libtest.dylib",
+ "libtest.dylib");
+ }
+
+ @Test
+ public void osx_aarch() {
+ test(
+ TestPlatformSpec.of(MAC, AARCH64),
+ "macos/aarch64/libtest.dylib",
+ "macos/libtest.dylib",
+ "libtest.dylib");
+ }
+
+ @Test
+ public void windows_x86_64() {
+ test(TestPlatformSpec.of(WINDOWS, X86_64), "win/x86_64/test.dll", "win/test.dll", "test.dll");
+ }
+
+ static final void test(PlatformSpec platformSpec, String... expectedPaths) {
+ CapturingPathLocator.testFailOnExceptions(
+ NestedDirLibraryResolver.INSTANCE,
+ platformSpec,
+ CapturingPathLocator.WITH_OMIT_COMP_FALLBACK,
+ expectedPaths);
+ }
+}
diff --git a/components/native-loader/src/test/java/datadog/nativeloader/PathLocationHelperTest.java b/components/native-loader/src/test/java/datadog/nativeloader/PathLocationHelperTest.java
new file mode 100644
index 00000000000..dae51056efd
--- /dev/null
+++ b/components/native-loader/src/test/java/datadog/nativeloader/PathLocationHelperTest.java
@@ -0,0 +1,105 @@
+package datadog.nativeloader;
+
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import org.junit.jupiter.api.Test;
+
+public final class PathLocationHelperTest {
+ @Test
+ public void happyPath() throws MalformedURLException {
+ URL expected = new URL("http://localhost");
+
+ PathLocator pathLocator = (comp, path) -> expected;
+ PathLocatorHelper helper = new PathLocatorHelper("test", pathLocator);
+
+ URL result = helper.locate(null, "path");
+ assertSame(expected, result);
+ }
+
+ @Test
+ public void throwingException() {
+ Exception expectedCause = new IllegalStateException("wrong!");
+ PathLocator throwingPathLocator =
+ (comp, path) -> {
+ throw expectedCause;
+ };
+
+ PathLocatorHelper helper = new PathLocatorHelper("test", throwingPathLocator);
+
+ // on exception, PathLocator returns null, but stores the exception for use when tryThrow is
+ // called later
+ URL result = helper.locate(null, "path");
+ assertNull(result);
+
+ try {
+ // should throw, since an exception occurred earlier
+ helper.tryThrow();
+
+ fail("should raise a LibraryLoadException");
+ } catch (LibraryLoadException e) {
+ assertSame(expectedCause, e.getCause());
+ }
+ }
+
+ @Test
+ public void throwingLibraryLoadException() {
+ Exception expectedCause = new LibraryLoadException("test", "wrong!");
+ PathLocator throwingPathLocator =
+ (comp, path) -> {
+ throw expectedCause;
+ };
+
+ PathLocatorHelper helper = new PathLocatorHelper("test", throwingPathLocator);
+
+ // on exception, PathLocator returns null, but stores the exception for use when tryThrow is
+ // called later
+ URL result = helper.locate(null, "path");
+ assertNull(result);
+
+ try {
+ // should throw, since an exception occurred earlier
+ helper.tryThrow();
+
+ fail("should raise a LibraryLoadException");
+ } catch (LibraryLoadException e) {
+ assertSame(expectedCause, e);
+ }
+ }
+
+ @Test
+ public void throwingMultipleExceptions() {
+ Exception firstCause = new IllegalStateException("wrong!");
+ Exception secondCause = new IllegalStateException("wrong again!");
+
+ PathLocator throwingPathLocator =
+ (comp, path) -> {
+ if (path.equals("firstPath")) {
+ throw firstCause;
+ } else {
+ throw secondCause;
+ }
+ };
+
+ // on exception, PathLocator returns null, but stores the first exception
+ PathLocatorHelper helper = new PathLocatorHelper("test", throwingPathLocator);
+ URL firstResult = helper.locate(null, "firstPath");
+ assertNull(firstResult);
+
+ // still retgurns null, but doesn't store exception
+ URL secondResult = helper.locate(null, "secondPath");
+ assertNull(secondResult);
+
+ try {
+ helper.tryThrow();
+
+ fail("should raise a LibraryLoadException");
+ } catch (LibraryLoadException e) {
+ // expect just the first cause to be captured
+ assertSame(firstCause, e.getCause());
+ }
+ }
+}
diff --git a/components/native-loader/src/test/java/datadog/nativeloader/PathLocatorsTest.java b/components/native-loader/src/test/java/datadog/nativeloader/PathLocatorsTest.java
new file mode 100644
index 00000000000..84fb1134a70
--- /dev/null
+++ b/components/native-loader/src/test/java/datadog/nativeloader/PathLocatorsTest.java
@@ -0,0 +1,112 @@
+package datadog.nativeloader;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Paths;
+import org.junit.jupiter.api.Test;
+
+/*
+ * These tests mostly exist to satisfy coverage checks.
+ * Although equals is used in other tests & usual contract obliges defining hashCode, too
+ */
+public class PathLocatorsTest {
+ @Test
+ public void dirBased_equals() {
+ PathLocator dirLocator1 = PathLocators.fromLibDirs("foo");
+ PathLocator dirLocator2 = PathLocators.fromLibDirs(new File("foo"));
+ PathLocator dirLocator3 = PathLocators.fromLibDirs(Paths.get("foo"));
+
+ assertEqualsAndHashCode(dirLocator1, dirLocator2);
+ assertEqualsAndHashCode(dirLocator1, dirLocator3);
+ }
+
+ @Test
+ public void dirBased_notEquals() {
+ PathLocator dirLocator1 = PathLocators.fromLibDirs("foo1");
+ PathLocator dirLocator2 = PathLocators.fromLibDirs(new File("foo2"));
+ PathLocator dirLocator3 = PathLocators.fromLibDirs(Paths.get("foo3"));
+
+ assertNotEquals(dirLocator1, dirLocator2);
+ assertNotEquals(dirLocator1, dirLocator3);
+ }
+
+ @Test
+ public void dirBased_diffType_notEquals() {
+ PathLocator dirLocator = PathLocators.fromLibDirs("foo1");
+ PathLocator otherLocator = (comp, path) -> null;
+
+ // be explicit about which equals is being used
+ assertFalse(dirLocator.equals(otherLocator));
+ }
+
+ @Test
+ public void libPath() {
+ PathLocator dirLocator1 = PathLocators.fromLibPathString("foo:bar:baz");
+ PathLocator dirLocator2 = PathLocators.fromLibDirs("foo", "bar", "baz");
+
+ assertEqualsAndHashCode(dirLocator1, dirLocator2);
+ }
+
+ @Test
+ public void classLoaderBased_equals() {
+ ClassLoader loader = ClassLoader.getSystemClassLoader();
+
+ PathLocator locator1 = PathLocators.fromClassLoader(loader);
+ PathLocator locator2 = PathLocators.fromClassLoader(loader);
+
+ assertEqualsAndHashCode(locator1, locator2);
+ }
+
+ @Test
+ public void classLoaderBased_with_resource_equals() {
+ ClassLoader loader = ClassLoader.getSystemClassLoader();
+
+ PathLocator locator1 = PathLocators.fromClassLoader(loader, "resource");
+ PathLocator locator2 = PathLocators.fromClassLoader(loader, "resource");
+
+ assertEqualsAndHashCode(locator1, locator2);
+ }
+
+ @Test
+ public void classLoaderBased_notEequals_loader() {
+ ClassLoader loader1 = ClassLoader.getSystemClassLoader();
+ ClassLoader loader2 = new URLClassLoader(new URL[0]);
+
+ PathLocator locator1 = PathLocators.fromClassLoader(loader1);
+ PathLocator locator2 = PathLocators.fromClassLoader(loader2);
+
+ assertNotEquals(locator1, locator2);
+ }
+
+ @Test
+ public void classLoaderBased_notEequals_resource() {
+ ClassLoader loader = ClassLoader.getSystemClassLoader();
+
+ PathLocator locator1 = PathLocators.fromClassLoader(loader, "resource1");
+ PathLocator locator2 = PathLocators.fromClassLoader(loader, "resource2");
+
+ assertNotEquals(locator1, locator2);
+ }
+
+ @Test
+ public void classLoaderBased_diffType_notEequals() {
+ ClassLoader loader = ClassLoader.getSystemClassLoader();
+
+ PathLocator locator1 = PathLocators.fromClassLoader(loader, "resource1");
+ PathLocator locator2 = (comp, path) -> null;
+
+ // be explicit about which equals is being used
+ assertFalse(locator1.equals(locator2));
+ }
+
+ static final void assertEqualsAndHashCode(Object lhs, Object rhs) {
+ assertEquals(lhs.hashCode(), rhs.hashCode());
+ assertTrue(lhs.equals(rhs));
+ }
+}
diff --git a/components/native-loader/src/test/java/datadog/nativeloader/PathUtilsTest.java b/components/native-loader/src/test/java/datadog/nativeloader/PathUtilsTest.java
new file mode 100644
index 00000000000..f2432eaabdb
--- /dev/null
+++ b/components/native-loader/src/test/java/datadog/nativeloader/PathUtilsTest.java
@@ -0,0 +1,166 @@
+package datadog.nativeloader;
+
+import static datadog.nativeloader.TestPlatformSpec.linux;
+import static datadog.nativeloader.TestPlatformSpec.linux_arm32;
+import static datadog.nativeloader.TestPlatformSpec.linux_arm64;
+import static datadog.nativeloader.TestPlatformSpec.linux_glibc;
+import static datadog.nativeloader.TestPlatformSpec.linux_musl;
+import static datadog.nativeloader.TestPlatformSpec.linux_x86_32;
+import static datadog.nativeloader.TestPlatformSpec.linux_x86_64;
+import static datadog.nativeloader.TestPlatformSpec.mac;
+import static datadog.nativeloader.TestPlatformSpec.unsupportedArch;
+import static datadog.nativeloader.TestPlatformSpec.unsupportedOs;
+import static datadog.nativeloader.TestPlatformSpec.windows;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+public class PathUtilsTest {
+ @Test
+ public void libFileName_mac() {
+ assertEquals("libtest.dylib", PathUtils.libFileName(mac(), "test"));
+ }
+
+ @Test
+ public void libFileName_linux() {
+ assertEquals("libtest.so", PathUtils.libFileName(linux(), "test"));
+ }
+
+ @Test
+ public void libFileName_windows() {
+ assertEquals("test.dll", PathUtils.libFileName(windows(), "test"));
+ }
+
+ @Test
+ public void libPrefix_mac() {
+ assertEquals("lib", PathUtils.libPrefix(mac()));
+ }
+
+ @Test
+ public void libPrefix_linux() {
+ assertEquals("lib", PathUtils.libPrefix(linux()));
+ }
+
+ @Test
+ public void libPrefix_windows() {
+ assertEquals("", PathUtils.libPrefix(windows()));
+ }
+
+ @Test
+ public void libPrefix_unsupportedOs() {
+ assertThrows(IllegalArgumentException.class, () -> PathUtils.libPrefix(unsupportedOs()));
+ }
+
+ @Test
+ public void dynamicLibExtension_mac() {
+ assertEquals("dylib", PathUtils.dynamicLibExtension(mac()));
+ }
+
+ @Test
+ public void dynamicLibExtension_linux() {
+ assertEquals("so", PathUtils.dynamicLibExtension(linux()));
+ }
+
+ @Test
+ public void dynamicLibExtension_windows() {
+ assertEquals("dll", PathUtils.dynamicLibExtension(windows()));
+ }
+
+ @Test
+ public void dynamicLibExtension_unsupportedOs() {
+ assertThrows(
+ IllegalArgumentException.class, () -> PathUtils.dynamicLibExtension(unsupportedOs()));
+ }
+
+ @Test
+ public void osPart_linux() {
+ assertEquals("linux", PathUtils.osPartOf(linux()));
+ }
+
+ @Test
+ public void osPart_mac() {
+ assertEquals("macos", PathUtils.osPartOf(mac()));
+ }
+
+ @Test
+ public void osPart_windows() {
+ assertEquals("win", PathUtils.osPartOf(windows()));
+ }
+
+ @Test
+ public void osPart_unsupportedOs() {
+ assertThrows(IllegalArgumentException.class, () -> PathUtils.osPartOf(unsupportedOs()));
+ }
+
+ @Test
+ public void archPart_x86_32() {
+ assertEquals("x86_32", PathUtils.archPartOf(linux_x86_32()));
+ }
+
+ @Test
+ public void archPart_x86_64() {
+ assertEquals("x86_64", PathUtils.archPartOf(linux_x86_64()));
+ }
+
+ @Test
+ public void archPart_arm32() {
+ assertEquals("arm32", PathUtils.archPartOf(linux_arm32()));
+ }
+
+ @Test
+ public void archPart_arm64() {
+ assertEquals("aarch64", PathUtils.archPartOf(linux_arm64()));
+ }
+
+ @Test
+ public void archPart_unsupportedArch() {
+ assertThrows(IllegalArgumentException.class, () -> PathUtils.archPartOf(unsupportedArch()));
+ }
+
+ @Test
+ public void libcPart_linux_glibc() {
+ assertEquals("libc", PathUtils.libcPartOf(linux_glibc()));
+ }
+
+ @Test
+ public void libcPart_linux_musl() {
+ assertEquals("musl", PathUtils.libcPartOf(linux_musl()));
+ }
+
+ @Test
+ public void libcPart_mac() {
+ assertNull(PathUtils.libcPartOf(mac()));
+ }
+
+ @Test
+ public void libcPart_windows() {
+ assertNull(PathUtils.libcPartOf(windows()));
+ }
+
+ @Test
+ public void concat_nonEmpty_nonEmpty() {
+ assertEquals("foo/bar", PathUtils.concatPath("foo", "bar"));
+ }
+
+ @Test
+ public void concat_null_nonEmpty() {
+ assertEquals("bar", PathUtils.concatPath(null, "bar"));
+ }
+
+ @Test
+ public void concat_nonEmpty_null() {
+ assertEquals("foo", PathUtils.concatPath("foo", null));
+ }
+
+ @Test
+ public void concat_empty_nonEmpty() {
+ assertEquals("bar", PathUtils.concatPath("", "bar"));
+ }
+
+ @Test
+ public void concat_null_empty_nonEmpty() {
+ assertEquals("bar", PathUtils.concatPath(null, "", "bar"));
+ }
+}
diff --git a/components/native-loader/src/test/java/datadog/nativeloader/PlatformSpecTest.java b/components/native-loader/src/test/java/datadog/nativeloader/PlatformSpecTest.java
new file mode 100644
index 00000000000..3dce67b4d03
--- /dev/null
+++ b/components/native-loader/src/test/java/datadog/nativeloader/PlatformSpecTest.java
@@ -0,0 +1,76 @@
+package datadog.nativeloader;
+
+import static datadog.nativeloader.TestPlatformSpec.linux;
+import static datadog.nativeloader.TestPlatformSpec.linux_arm32;
+import static datadog.nativeloader.TestPlatformSpec.linux_arm64;
+import static datadog.nativeloader.TestPlatformSpec.linux_x86_32;
+import static datadog.nativeloader.TestPlatformSpec.linux_x86_64;
+import static datadog.nativeloader.TestPlatformSpec.mac;
+import static datadog.nativeloader.TestPlatformSpec.unsupportedArch;
+import static datadog.nativeloader.TestPlatformSpec.unsupportedOs;
+import static datadog.nativeloader.TestPlatformSpec.windows;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+public class PlatformSpecTest {
+ @Test
+ public void macSpec() {
+ assertTrue(mac().isMac());
+ assertFalse(mac().isUnknownOs());
+ }
+
+ @Test
+ public void linuxSpec() {
+ assertTrue(linux().isLinux());
+ assertFalse(linux().isUnknownOs());
+ }
+
+ @Test
+ public void windowsSpec() {
+ assertTrue(windows().isWindows());
+ assertFalse(windows().isUnknownOs());
+ }
+
+ @Test
+ public void unsupportedOsSpec() {
+ assertFalse(unsupportedOs().isMac());
+ assertFalse(unsupportedOs().isLinux());
+ assertFalse(unsupportedOs().isWindows());
+ assertTrue(unsupportedOs().isUnknownOs());
+ }
+
+ @Test
+ public void arm32Spec() {
+ assertTrue(linux_arm32().isArm32());
+ assertFalse(linux_arm32().isUnknownArch());
+ }
+
+ @Test
+ public void arm64Spec() {
+ assertTrue(linux_arm64().isAarch64());
+ assertFalse(linux_arm64().isUnknownArch());
+ }
+
+ @Test
+ public void x86_32Spec() {
+ assertTrue(linux_x86_32().isX86_32());
+ assertFalse(linux_x86_32().isUnknownArch());
+ }
+
+ @Test
+ public void x86_64Spec() {
+ assertTrue(linux_x86_64().isX86_64());
+ assertFalse(linux_x86_64().isUnknownArch());
+ }
+
+ @Test
+ public void unsupportedArchSpec() {
+ assertFalse(unsupportedArch().isArm32());
+ assertFalse(unsupportedArch().isAarch64());
+ assertFalse(unsupportedArch().isX86_32());
+ assertFalse(unsupportedArch().isX86_64());
+ assertTrue(unsupportedArch().isUnknownArch());
+ }
+}
diff --git a/components/native-loader/src/test/java/datadog/nativeloader/TestPlatformSpec.java b/components/native-loader/src/test/java/datadog/nativeloader/TestPlatformSpec.java
new file mode 100644
index 00000000000..dd875453045
--- /dev/null
+++ b/components/native-loader/src/test/java/datadog/nativeloader/TestPlatformSpec.java
@@ -0,0 +1,130 @@
+package datadog.nativeloader;
+
+public class TestPlatformSpec extends PlatformSpec {
+ public static final String MAC = "mac";
+ public static final String WINDOWS = "win";
+ public static final String LINUX = "linux";
+
+ public static final String UNSUPPORTED_OS = "unsupported";
+
+ public static final String X86_32 = "x86_32";
+ public static final String X86_64 = "x86_64";
+
+ public static final String ARM32 = "arm32";
+ public static final String AARCH64 = "aarch64";
+
+ public static final String UNSUPPORTED_ARCH = "unsupported";
+
+ public static final boolean GLIBC = false;
+ public static final boolean MUSL = true;
+
+ public static final PlatformSpec of(String os, String arch) {
+ return new TestPlatformSpec(os, arch, false);
+ }
+
+ public static final TestPlatformSpec of(String os, String arch, boolean isMusl) {
+ return new TestPlatformSpec(os, arch, isMusl);
+ }
+
+ public static final PlatformSpec mac() {
+ return TestPlatformSpec.of(MAC, AARCH64);
+ }
+
+ public static final PlatformSpec linux() {
+ return TestPlatformSpec.of(LINUX, X86_64);
+ }
+
+ public static final PlatformSpec windows() {
+ return TestPlatformSpec.of(WINDOWS, X86_64);
+ }
+
+ public static final PlatformSpec unsupportedOs() {
+ return TestPlatformSpec.of(UNSUPPORTED_OS, AARCH64);
+ }
+
+ public static final PlatformSpec linux_x86_32() {
+ return TestPlatformSpec.of(WINDOWS, X86_32);
+ }
+
+ public static final PlatformSpec linux_x86_64() {
+ return TestPlatformSpec.of(MAC, X86_64);
+ }
+
+ public static final PlatformSpec linux_arm32() {
+ return TestPlatformSpec.of(LINUX, ARM32);
+ }
+
+ public static final PlatformSpec linux_arm64() {
+ return TestPlatformSpec.of(LINUX, AARCH64);
+ }
+
+ public static final PlatformSpec linux_glibc() {
+ return TestPlatformSpec.of(LINUX, AARCH64, GLIBC);
+ }
+
+ public static final PlatformSpec linux_musl() {
+ return TestPlatformSpec.of(LINUX, AARCH64, MUSL);
+ }
+
+ public static final PlatformSpec unsupportedArch() {
+ return TestPlatformSpec.of(LINUX, UNSUPPORTED_ARCH);
+ }
+
+ private final String os;
+ private final String arch;
+ private final boolean isMusl;
+
+ private TestPlatformSpec(String os, String arch, boolean isMusl) {
+ this.os = os;
+ this.arch = arch;
+ this.isMusl = isMusl;
+ }
+
+ @Override
+ public boolean isLinux() {
+ return this.isOs(LINUX);
+ }
+
+ @Override
+ public boolean isMac() {
+ return this.isOs(MAC);
+ }
+
+ @Override
+ public boolean isWindows() {
+ return this.isOs(WINDOWS);
+ }
+
+ private boolean isOs(String osConst) {
+ return osConst.equals(this.os);
+ }
+
+ @Override
+ public boolean isX86_32() {
+ return this.isArch(X86_32);
+ }
+
+ @Override
+ public boolean isAarch64() {
+ return this.isArch(AARCH64);
+ }
+
+ @Override
+ public boolean isArm32() {
+ return this.isArch(ARM32);
+ }
+
+ @Override
+ public boolean isX86_64() {
+ return this.isArch(X86_64);
+ }
+
+ @Override
+ public boolean isMusl() {
+ return this.isMusl;
+ }
+
+ private boolean isArch(String archConst) {
+ return archConst.equals(this.arch);
+ }
+}
diff --git a/components/native-loader/test-data/comp1/dummy.dll b/components/native-loader/test-data/comp1/dummy.dll
new file mode 100644
index 00000000000..ccbb965f72a
--- /dev/null
+++ b/components/native-loader/test-data/comp1/dummy.dll
@@ -0,0 +1 @@
+not a library
\ No newline at end of file
diff --git a/components/native-loader/test-data/comp1/libdummy.dylib b/components/native-loader/test-data/comp1/libdummy.dylib
new file mode 100644
index 00000000000..ccbb965f72a
--- /dev/null
+++ b/components/native-loader/test-data/comp1/libdummy.dylib
@@ -0,0 +1 @@
+not a library
\ No newline at end of file
diff --git a/components/native-loader/test-data/comp1/libdummy.so b/components/native-loader/test-data/comp1/libdummy.so
new file mode 100644
index 00000000000..ccbb965f72a
--- /dev/null
+++ b/components/native-loader/test-data/comp1/libdummy.so
@@ -0,0 +1 @@
+not a library
\ No newline at end of file
diff --git a/components/native-loader/test-data/comp1/resource/dummy.dll b/components/native-loader/test-data/comp1/resource/dummy.dll
new file mode 100644
index 00000000000..ccbb965f72a
--- /dev/null
+++ b/components/native-loader/test-data/comp1/resource/dummy.dll
@@ -0,0 +1 @@
+not a library
\ No newline at end of file
diff --git a/components/native-loader/test-data/comp1/resource/libdummy.dylib b/components/native-loader/test-data/comp1/resource/libdummy.dylib
new file mode 100644
index 00000000000..ccbb965f72a
--- /dev/null
+++ b/components/native-loader/test-data/comp1/resource/libdummy.dylib
@@ -0,0 +1 @@
+not a library
\ No newline at end of file
diff --git a/components/native-loader/test-data/comp1/resource/libdummy.so b/components/native-loader/test-data/comp1/resource/libdummy.so
new file mode 100644
index 00000000000..ccbb965f72a
--- /dev/null
+++ b/components/native-loader/test-data/comp1/resource/libdummy.so
@@ -0,0 +1 @@
+not a library
\ No newline at end of file
diff --git a/components/native-loader/test-data/comp2/dummy.dll b/components/native-loader/test-data/comp2/dummy.dll
new file mode 100644
index 00000000000..ccbb965f72a
--- /dev/null
+++ b/components/native-loader/test-data/comp2/dummy.dll
@@ -0,0 +1 @@
+not a library
\ No newline at end of file
diff --git a/components/native-loader/test-data/comp2/libdummy.dylib b/components/native-loader/test-data/comp2/libdummy.dylib
new file mode 100644
index 00000000000..ccbb965f72a
--- /dev/null
+++ b/components/native-loader/test-data/comp2/libdummy.dylib
@@ -0,0 +1 @@
+not a library
\ No newline at end of file
diff --git a/components/native-loader/test-data/comp2/libdummy.so b/components/native-loader/test-data/comp2/libdummy.so
new file mode 100644
index 00000000000..ccbb965f72a
--- /dev/null
+++ b/components/native-loader/test-data/comp2/libdummy.so
@@ -0,0 +1 @@
+not a library
\ No newline at end of file
diff --git a/components/native-loader/test-data/dummy.dll b/components/native-loader/test-data/dummy.dll
new file mode 100644
index 00000000000..ccbb965f72a
--- /dev/null
+++ b/components/native-loader/test-data/dummy.dll
@@ -0,0 +1 @@
+not a library
\ No newline at end of file
diff --git a/components/native-loader/test-data/libdummy.dylib b/components/native-loader/test-data/libdummy.dylib
new file mode 100644
index 00000000000..ccbb965f72a
--- /dev/null
+++ b/components/native-loader/test-data/libdummy.dylib
@@ -0,0 +1 @@
+not a library
\ No newline at end of file
diff --git a/components/native-loader/test-data/libdummy.so b/components/native-loader/test-data/libdummy.so
new file mode 100644
index 00000000000..ccbb965f72a
--- /dev/null
+++ b/components/native-loader/test-data/libdummy.so
@@ -0,0 +1 @@
+not a library
\ No newline at end of file
diff --git a/components/native-loader/test-data/resource/dummy.dll b/components/native-loader/test-data/resource/dummy.dll
new file mode 100644
index 00000000000..ccbb965f72a
--- /dev/null
+++ b/components/native-loader/test-data/resource/dummy.dll
@@ -0,0 +1 @@
+not a library
\ No newline at end of file
diff --git a/components/native-loader/test-data/resource/libdummy.dylib b/components/native-loader/test-data/resource/libdummy.dylib
new file mode 100644
index 00000000000..ccbb965f72a
--- /dev/null
+++ b/components/native-loader/test-data/resource/libdummy.dylib
@@ -0,0 +1 @@
+not a library
\ No newline at end of file
diff --git a/components/native-loader/test-data/resource/libdummy.so b/components/native-loader/test-data/resource/libdummy.so
new file mode 100644
index 00000000000..ccbb965f72a
--- /dev/null
+++ b/components/native-loader/test-data/resource/libdummy.so
@@ -0,0 +1 @@
+not a library
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 62807b078f9..4820489d970 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -104,6 +104,7 @@ include(
":components:context",
":components:environment",
":components:json",
+ ":components:native-loader",
":components:yaml",
":telemetry",
":remote-config:remote-config-api",