Skip to content

Repository files navigation

elfgather

Collect Linux ELF and macOS Mach-O runtime dependencies without executing the input binary.

Installation

With Go installed, download, build, and install the utility directly from GitHub:

go install github.com/meatproxy/elfgather@latest

No repository checkout is required. Run the same command again to update to the latest version selected by Go.

The executable is installed in GOBIN if configured, otherwise in GOPATH/bin (normally $HOME/go/bin). Make sure that directory is in your PATH. For the default location:

export PATH="$PATH:$HOME/go/bin"
elfgather --help

Add the export line to your shell configuration to keep it for future sessions. If you prefer to build from a local checkout:

go build -o elfgather .
./elfgather --help

Usage

# Inspect an executable and its transitive dependencies.
elfgather /absolute/path/app --json

# Copy dependencies, retaining their absolute directory layout under ./deps.
elfgather /absolute/path/app --dest ./deps

# Inspect only the arm64 slice of a macOS universal executable.
elfgather /absolute/path/app --arch arm64 --dry-run --json

# Include a known dlopen plugin and copy the executable too.
elfgather /absolute/path/app --dest ./deps \
  --extra-lib /absolute/path/plugin.dylib --include /absolute/path/app

Pass exactly one executable as a positional argument, before or after the options. Options use Go's standard flag package and accept one or two leading hyphens (-dest or --dest). Put all options together, before or after the executable; interleaving options on both sides is not supported. Use -- after the options before a filename beginning with -. Boolean values use --json=false, not --json false.

Without --dest, the command lists dependencies without writing, just like --dry-run. With --dest, files are copied unless --dry-run is also set. Successful copying is silent unless --json explicitly requests a report. The destination may already contain files: matching files and symlinks are replaced, while unrelated files are preserved. The destination itself must be a real directory. Conflicting directories or symlinks in parent paths are rejected rather than followed or deleted. Files are staged and destination conflicts are checked before replacement; updates to an existing directory are atomic per file, not for the entire directory. The executable itself is not copied unless added with --include. --extra-lib and --include can be repeated.

Linux

The ELF resolver follows DT_NEEDED, RPATH/RUNPATH, loader tokens, the system library cache, and default search directories. The interpreter from PT_INTERP is included. --library-path defaults to LD_LIBRARY_PATH for ELF; pass --library-path '' to ignore it. $LIB and $PLATFORM, when used, require --lib-token and --platform-token respectively.

macOS

Format detection uses the input signature, not the host operating system. Mach-O inspection also works on Linux if the referenced third-party files are available at the paths being resolved; there is no cross-system sysroot remapping.

Supported features:

  • Thin Mach-O and standard universal (FAT_MAGIC) files for arm64 and x86_64, including their arm64e/x86_64h subtypes. Non-macOS platforms are rejected when identified by their platform load commands.
  • --arch all (default), arm64, amd64, or the alias x86_64. Dependencies are resolved separately for each selected executable slice; matching subtypes are preferred, with generic-library fallback. Files are copied once and are not thinned, so selecting one architecture does not prepare the other slices for execution.
  • Recursive LC_LOAD_DYLIB, LC_LOAD_WEAK_DYLIB, LC_REEXPORT_DYLIB, and LC_LOAD_UPWARD_DYLIB dependencies. LC_ID_DYLIB is not treated as an import.
  • Absolute install names, @executable_path, @loader_path, and @rpath, with inherited LC_RPATH entries expanded relative to the object declaring them. Image paths are canonicalized for loader-token expansion.
  • Optional explicit --library-path /dir1:/dir2 overrides: dylibs are looked up by basename, frameworks by their Name.framework/... suffix, before ordinary non-system paths. Bare dylib names require these explicit search directories.
  • Full third-party framework trees, including resources and internal version symlinks. A framework symlink whose resolved target escapes the framework is rejected. Absolute symlinks are rebased inside the copied tree.
  • Additional dylibs and Mach-O bundles via --extra-lib; their dependencies use the main executable's runpaths as the initial context.

The JSON report includes format, architectures, and per-dependency arch, status, and (when applicable) weak. Mach-O statuses are:

Status Meaning
resolved A compatible third-party dylib was found and scheduled for copying.
system A path under /usr/lib/ or /System/Library/ is left to macOS.
missing-weak An optional import has no file or compatible architecture.

System paths are an exclusion policy, not an existence or compatibility check. macOS can provide these libraries through its dyld shared cache even when there is no file at the recorded path. They are neither copied nor traversed; dyld itself is not copied. /usr/local/lib and Homebrew libraries are not excluded by this policy.

A missing required import, malformed library, or unsupported input fails the collection. Missing weak imports are reported rather than silently discarded.

Scope and limitations

This is a dependency collector, not a relocatable app bundler. It does not rewrite install names or runpaths, sign code, extract the dyld shared cache, preserve extended attributes, or verify macOS deployment-target/ABI compatibility. The result retains source paths under the destination; moving it elsewhere may require install_name_tool and subsequent signing.

The Mach-O resolver uses a deterministic static search model; it does not reproduce all dyld process policies. DYLD_* environment overrides, insertion, fallback directories, SIP/hardened-runtime restrictions, legacy framework path fallbacks, and working-directory-relative paths are not emulated. LD_LIBRARY_PATH is ignored for Mach-O. Use explicit --library-path when needed. Nested @rpath in LC_RPATH, lazy-load commands, 32-bit CPUs, and nonstandard/64-bit universal headers are currently unsupported. All slices parsed from a universal file must use supported CPU/platform formats, even when --arch selects a subset.

Neither format can discover arbitrary runtime dlopen() paths. Supply known plugins with --extra-lib and other runtime data with --include. Extra Mach-O plugins must contain compatible slices for every selected executable architecture.

Tests

go test ./...
go vet ./...

Cross-platform tests generate Mach-O fixtures for transitive resolution, inherited runpaths, weak imports, universal files, cycles, malformed commands, and framework copying. Linux tests build and execute ELF fixtures with GCC. On macOS, an additional integration test uses Clang to build a real executable plus two dylibs, collects them, removes the original libraries, and launches the copied executable. Native tests require their respective compiler.

Mach-O format and lookup references: Go debug/macho, Apple run-path libraries, dyld loader source.

About

Collect Linux ELF and macOS Mach-O runtime dependencies

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages