$ORIGIN in rpath Portable Application Distribution with Relative Library Paths

 

$ORIGIN in rpath
TLPI Chapter 41 · Section 41.10 — Portable Application Distribution with Relative Library Paths
🌐
Portable Apps
📦
Self-contained
📍
$ORIGIN

Key Concepts
$ORIGIN ${ORIGIN} Relative rpath Self-contained package LD_LIBRARY_PATH workaround Installation script Portable binary

The Problem: Distributing Applications with Private Libraries

Imagine you are distributing an application called MyApp that ships with its own set of shared libraries (perhaps custom versions that differ from what the system has installed). You want users to be able to:

  • Unpack the application into any directory they choose (e.g., /home/user/myapp/ or /opt/myapp/ or D:\Programs\MyApp on different systems)
  • Run it immediately without any extra configuration
  • Not require root access to install to standard directories

The problem with hardcoded rpath like -Wl,-rpath,/opt/myapp/lib is that if the user installs to /home/alice/myapp/, the hardcoded path doesn’t match and the binary fails to find its libraries.

Naive solutions that don’t work well:
• Requiring users to set LD_LIBRARY_PATH before every run — cumbersome and error-prone
• Providing a shell wrapper script — adds complexity
• Running an installation script to patch the binary — defeats the purpose

The Solution: $ORIGIN Special Token

The dynamic linker (ld.so) understands a special token $ORIGIN (or equivalently ${ORIGIN}) in rpath specifications. At runtime, the dynamic linker expands $ORIGIN to the directory containing the executable (or the containing directory of the shared library, if $ORIGIN appears in a shared library’s rpath).

rpath = $ORIGIN/lib
dynamic linker expands $ORIGIN
Binary is at /home/alice/myapp/bin/prog
$ORIGIN = /home/alice/myapp/bin
rpath expands to /home/alice/myapp/bin/lib
Libraries found here! ✓
$ORIGIN vs ${ORIGIN}: Both are equivalent. Use ${ORIGIN} when embedding in shell scripts to prevent shell variable expansion. When passing directly to the linker, you can use either form.

Typical Self-Contained Application Directory Structure
# User can install anywhere — e.g., /home/alice/myapp or /opt/myapp
myapp/                  ← installation root (any location)
bin/
prog                  ← executable (rpath = $ORIGIN/../lib)
lib/
libfoo.so.1           ← private library v1
libbar.so.2           ← private library v2
libfoo.so.1.2.3       ← real file
libbar.so.2.0.1       ← real file

When prog is in bin/ and libraries are in lib/ (sibling directories), the rpath should be:

# $ORIGIN/../lib means: directory of binary / up one level / lib
# This works whether installed at /home/alice/myapp or /opt/tools/myapp

gcc -o bin/prog main.c \
    -Wl,-rpath,'$ORIGIN/../lib' \
    -L./lib -lfoo -lbar
Shell quoting is critical! You MUST use single quotes around '$ORIGIN/../lib' when passing to the shell, or use \$ORIGIN. If you use double quotes, the shell will try to expand $ORIGIN as a shell variable (which is empty), and the rpath will be wrong.

Coding Example 1: Complete $ORIGIN-Based Portable Application
#!/bin/bash
# origin_demo.sh — builds a fully portable self-contained application

set -e
BASE=/tmp/origin_demo

# Create the portable app directory structure
mkdir -p $BASE/bin $BASE/lib

# ---- Library source ----
cat > $BASE/myengine.c << 'EOF'
#include <stdio.h>

void engine_init(void) {
    printf("[libengine] Initializing engine...\n");
}

void engine_run(const char *task) {
    printf("[libengine] Running task: %s\n", task);
}
EOF

# ---- Main program source ----
cat > $BASE/main.c << 'EOF'
#include <stdio.h>

extern void engine_init(void);
extern void engine_run(const char *task);

int main(int argc, char *argv[]) {
    printf("[myapp] Starting at: %s\n", argv[0]);
    engine_init();
    engine_run("data_processing");
    engine_run("report_generation");
    printf("[myapp] Complete!\n");
    return 0;
}
EOF

echo "=== Step 1: Build the shared library ==="
cd $BASE
gcc -g -c -fPIC -Wall myengine.c
gcc -g -shared -Wl,-soname,libengine.so.1 \
    -o lib/libengine.so.1.0.0 myengine.o
ln -sf libengine.so.1.0.0 lib/libengine.so.1
ln -sf libengine.so.1 lib/libengine.so

echo ""
echo "=== Step 2: Build the executable with \$ORIGIN rpath ==="
# IMPORTANT: Single quotes prevent shell from expanding $ORIGIN
gcc -g -Wall \
    -Wl,-rpath,'$ORIGIN/../lib' \
    -o bin/myapp main.c \
    -L./lib -lengine

echo ""
echo "=== Step 3: Verify rpath ==="
objdump -p bin/myapp | grep RPATH
# Should show: RPATH  $ORIGIN/../lib
# (the literal string, NOT expanded yet)

echo ""
echo "=== Step 4: Show ldd with $ORIGIN expanded at runtime ==="
ldd bin/myapp
# Shows: libengine.so.1 => /tmp/origin_demo/lib/libengine.so.1

echo ""
echo "=== Step 5: Run from installation location ==="
./bin/myapp

echo ""
echo "=== Step 6: Simulate installing to a different directory ==="
INSTALL_DIR=/tmp/myapp_install_$(date +%s)
cp -r $BASE $INSTALL_DIR
echo "Installed to: $INSTALL_DIR"
echo "Running from new location:"
$INSTALL_DIR/bin/myapp   # works without any LD_LIBRARY_PATH!

echo ""
echo "=== Done! Works from any install directory ==="
The key line is -Wl,-rpath,'$ORIGIN/../lib' with single quotes. The binary stores the literal string $ORIGIN/../lib. At runtime, the dynamic linker expands $ORIGIN to the actual directory of the binary.

Coding Example 2: $ORIGIN Inside a Shared Library’s Own rpath

$ORIGIN can also appear in a shared library’s rpath. In that case, $ORIGIN expands to the directory containing the shared library, not the main executable.

#!/bin/bash
# origin_in_lib.sh — $ORIGIN in a shared library's rpath
# Structure:
#   myapp/
#     bin/prog
#     lib/
#       libmain.so      (rpath = $ORIGIN/extra)
#       extra/
#         libextra.so

BASE=/tmp/origin_lib_demo
mkdir -p $BASE/bin $BASE/lib/extra

# ---- libextra.so source (leaf) ----
cat > $BASE/extra_utils.c << 'EOF'
#include <stdio.h>
void extra_func(void) {
    printf("[libextra] extra_func called\n");
}
EOF

# ---- libmain.so source (calls libextra) ----
cat > $BASE/main_lib.c << 'EOF'
#include <stdio.h>
extern void extra_func(void);
void main_func(void) {
    printf("[libmain] main_func calling extra...\n");
    extra_func();
}
EOF

# ---- prog source ----
cat > $BASE/prog.c << 'EOF'
#include <stdio.h>
extern void main_func(void);
int main(void) {
    main_func();
    return 0;
}
EOF

echo "=== Build libextra.so ==="
gcc -shared -fPIC -o $BASE/lib/extra/libextra.so $BASE/extra_utils.c

echo "=== Build libmain.so with \$ORIGIN/extra rpath ==="
gcc -g -c -fPIC $BASE/main_lib.c -o $BASE/main_lib.o
gcc -g -shared \
    -Wl,-rpath,'$ORIGIN/extra' \
    -o $BASE/lib/libmain.so $BASE/main_lib.o \
    -L$BASE/lib/extra -lextra
# $ORIGIN here = /tmp/origin_lib_demo/lib (lib dir of libmain.so)
# So rpath expands to /tmp/origin_lib_demo/lib/extra at runtime

echo ""
echo "rpath in libmain.so:"
objdump -p $BASE/lib/libmain.so | grep RPATH

echo ""
echo "=== Build prog ==="
gcc -g -Wall \
    -Wl,-rpath,'$ORIGIN/../lib' \
    -o $BASE/bin/prog $BASE/prog.c \
    -L$BASE/lib -lmain

echo ""
echo "rpath in prog:"
objdump -p $BASE/bin/prog | grep RPATH

echo ""
echo "=== ldd: complete dependency chain ==="
ldd $BASE/bin/prog

echo ""
echo "=== Run ==="
$BASE/bin/prog

Coding Example 3: Common $ORIGIN Patterns Reference
/*
 * Common $ORIGIN rpath patterns for different directory structures
 *
 * Pattern 1: Binary and libs in the same directory
 * ------------------------------------------------
 * app/
 *   prog          <-- rpath: $ORIGIN
 *   libfoo.so.1
 */
// gcc -o app/prog main.c -Wl,-rpath,'$ORIGIN' -L./app -lfoo

/*
 * Pattern 2: Binary in bin/, libs in lib/ (sibling dirs)
 * -------------------------------------------------------
 * app/
 *   bin/
 *     prog        <-- rpath: $ORIGIN/../lib
 *   lib/
 *     libfoo.so.1
 */
// gcc -o bin/prog main.c -Wl,-rpath,'$ORIGIN/../lib' -L./lib -lfoo

/*
 * Pattern 3: Multiple lib directories
 * ------------------------------------
 * app/
 *   bin/
 *     prog        <-- rpath: $ORIGIN/../lib:$ORIGIN/../lib64
 *   lib/
 *     libfoo.so
 *   lib64/
 *     libbar.so
 */
// gcc -o bin/prog main.c \
//     -Wl,-rpath,'$ORIGIN/../lib:$ORIGIN/../lib64' \
//     -L./lib -L./lib64 -lfoo -lbar

/*
 * Pattern 4: Using ${ORIGIN} syntax (useful in Makefiles)
 * ---------------------------------------------------------
 */
// RPATH_FLAG = -Wl,-rpath,'$${ORIGIN}/../lib'  (in Makefile)
// Or use \$\${ORIGIN} depending on Makefile quoting rules

/*
 * Pattern 5: Combining $ORIGIN with absolute path
 * -------------------------------------------------
 * Try private lib first, fall back to system lib
 */
// gcc -o bin/prog main.c \
//     -Wl,-rpath,'$ORIGIN/../lib:/usr/local/lib' \
//     -L./lib -lfoo

/*
 * Verification: read the literal rpath from binary
 */
// objdump -p bin/prog | grep RPATH
// Expected: RPATH  $ORIGIN/../lib
// (NOT expanded — the $ is literal in the ELF file)

Problem-Solution Comparison: Three Approaches
Approach Pros Cons Best For
Install to /usr/lib Standard, always found Requires root; pollutes system; conflicts possible System-wide packages (apt/rpm packages)
LD_LIBRARY_PATH Simple, no rebuild needed Must set every session; not inherited; security risk Development/testing
Hardcoded rpath Self-contained Breaks if user moves the app Fixed-location deployment
$ORIGIN rpath Self-contained AND relocatable Slightly complex to set up correctly Portable application distribution

Interview Questions
Q1. What does $ORIGIN mean in an rpath specification?
$ORIGIN is a special token understood by the Linux dynamic linker (ld.so). At runtime, when resolving library paths, the dynamic linker expands $ORIGIN to the directory containing the executable (or the directory containing the shared library, if $ORIGIN appears in a shared library’s rpath). This allows the rpath to be relative to the binary’s location rather than an absolute path, making the binary relocatable.
Q2. Why must you use single quotes around $ORIGIN when passing it to gcc?
The shell interprets $ORIGIN as a shell variable before passing arguments to gcc. Since $ORIGIN is not a defined shell variable, it expands to an empty string, resulting in a broken rpath. Single quotes prevent shell expansion and pass the literal string $ORIGIN/../lib to gcc, which then passes it to the linker, which stores it literally in the ELF file. Alternatively, you can escape it as \$ORIGIN.
Q3. In a program with the binary in bin/ and libraries in lib/ (siblings), what $ORIGIN rpath do you use?
$ORIGIN/../lib. This works as follows: $ORIGIN expands to the bin/ directory where the binary lives, then /.. goes up one level to the app root, then /lib enters the lib/ directory. So regardless of where the whole app is installed, the path is always correctly resolved relative to the binary.
Q4. If $ORIGIN appears in a shared library’s rpath, what directory does it expand to?
It expands to the directory containing the shared library, NOT the directory of the main executable. This is an important distinction. For example, if libmain.so is in /app/lib/ and has rpath $ORIGIN/extra, the dynamic linker searches /app/lib/extra/ for that library’s dependencies.
Q5. How do you verify that $ORIGIN is stored correctly (as a literal string) in the binary?
Use objdump -p binary | grep RPATH or readelf -d binary | grep RPATH. If $ORIGIN is stored correctly, you will see the literal string $ORIGIN/../lib (with the dollar sign) in the output. If you forgot single quotes and the shell expanded it, you would see an empty path or no RPATH entry at all.
Q6. What are the two alternative syntaxes for $ORIGIN in rpath?
Both $ORIGIN and ${ORIGIN} are equivalent and understood by the dynamic linker. The ${ORIGIN} form (with braces) can be useful to disambiguate boundaries, for example in ${ORIGIN}/../lib versus $ORIGIN/../lib — both mean the same thing here, but braces become necessary when the name is immediately followed by alphanumeric characters.
Q7. What problem does $ORIGIN solve that a hardcoded rpath does not?
A hardcoded rpath like /opt/myapp/lib only works when the application is installed exactly at /opt/myapp/. If a user installs it at /home/bob/tools/myapp/, the hardcoded path is wrong and the libraries won’t be found. $ORIGIN solves this by making the path relative to wherever the binary actually is at runtime, so the binary works from any installation directory without requiring the user to set LD_LIBRARY_PATH or run an installation script.

Ch. 41 Complete — Keep Going!

← DT_RPATH vs DT_RUNPATH ↑ Back to Series Start

Leave a Reply

Your email address will not be published. Required fields are marked *