Skip to content

Relocatable Python with $ORIGIN

 3 min A runtime library path relative to the executable lets a built interpreter move as a tree—verified in the ELF policy and the loader’s result.
Published

Sep 13, 2012

A shared libpython can be found through an embedded DT_RPATH or DT_RUNPATH, LD_LIBRARY_PATH, the loader cache, or the loader’s default directories. The relocation problem appears when a private-prefix Python build relies on an absolute runtime path or a fixed LD_LIBRARY_PATH entry: either pins the tree to a location. The alternative, from a Stack Overflow answer of mine about pythonbrew builds, is to embed a path relative to the executable itself—set in the build’s environment so the linker records it in the binary’s dynamic search path:

LD_RUN_PATH='$ORIGIN/../lib'

$ORIGIN is an ELF loader token that expands, at load time, to the directory of the running executable. With $ORIGIN/../lib recorded as the runtime path, a binary at prefix/bin/python resolves prefix/lib/libpython wherever prefix happens to sit—the whole directory tree moves together, internal linkage intact, no environment variable required at runtime. The single quotes are load-bearing: the literal string $ORIGIN must survive the shell and reach the linker unexpanded.

Verification is two separate tests, because two separate claims are in play:

$ readelf -dW ./python | grep -E 'RPATH|RUNPATH'
$ ldd ./python

readelf proves what path was embedded in the binary—the declared policy. ldd proves which libpython the loader chose on this machine—the observed resolution. They can disagree: a correct runtime path with the library missing from the relative location, or a resolution that looks right only because LD_LIBRARY_PATH or a system copy is silently winning. When they disagree, investigate both the artifact layout and the loader’s higher-precedence environment and search policy; neither test alone would have told you which one won. Declared policy and observed behavior are separate facts—the same discipline as the 2011 KVM diagnostic rather than the package name.

The edge case that makes the point: virtualenv, in that era, copied the interpreter binary into the new environment’s bin/. Copying the executable moves its $ORIGIN—the runtime path still says $ORIGIN/../lib, but that now names the virtualenv’s lib/, where no libpython lives. The flag did not break; the layout assumption behind it did. The answer proposed restoring the relationship: place a symlink or copy of the library where the new origin expects it, or extend the runtime path with an additional relative path.

The same problem appears in my comments on pythonbrew issue 43 and virtualenv issue 216. The virtualenv discussion agreed that a shared libpython was effectively part of the interpreter and welcomed a patch to copy it.

$ORIGIN does not make a binary relocatable; it makes a tree relocatable, and only while the tree keeps its internal shape. Relocatability is a property of the complete artifact layout, not of one linker flag—and anything that moves a single file out of formation, a copying virtualenv included, silently revokes it.

This is from 2012, and the copying behavior described is that era’s virtualenv. The $ORIGIN token still works, but the dynamic tag is not timeless. Modern GNU linkers commonly emit DT_RUNPATH under new dtags where older builds emitted DT_RPATH; the loader gives them different precedence and applies RUNPATH only to direct dependencies. The loader manual documents both. Inspect either tag, then separately inspect the libraries selected.