Building Portable Applications

Use RPM package collections to build self-contained, portable application bundles that run across different Linux distributions without depending on system libraries.

← Back to Main Page

The Portable Application Challenge

Modern applications often need to run across multiple Linux distributions, each with different versions of system libraries. Package collections provide a solution by bundling variable dependencies with your application.

What is a Portable Application?

A portable application is one that can run on multiple Linux distributions without modification. It achieves this by:

  • Bundling dependencies that vary between distributions (e.g., specific Python versions, custom libraries)
  • Using rpath to ensure the application finds its bundled libraries at runtime
  • Relying only on stable, widely-available system libraries (glibc, basic system libs)
  • Installing to a self-contained directory structure (typically /opt)

When to Use Package Collections

System Packages

Best for infrastructure

Standard RPM packages installed to /usr are ideal when:

  • Building for a specific distribution version
  • Integration with system package manager is desired
  • Dependencies are available in distribution repos
  • You control the target environment

Limitation: Different distributions may have incompatible library versions, making cross-distribution deployment difficult.

Package Collections

Best for applications

Self-contained collections in /opt are ideal when:

  • Application needs to run on multiple distributions
  • You need specific library versions regardless of system
  • Application and dependencies should be isolated
  • Users may not have package manager access

Benefit: Works across any distribution with compatible glibc, giving you maximum portability.

Example: Building a Portable Python Application

Let's build a Python application that needs Python 3.12 and several libraries, deployable across Fedora, RHEL, Ubuntu, and Debian.

Scenario

Your application requires:

  • Python 3.12 (may not be available on older distributions)
  • NumPy, Pandas, and other scientific libraries
  • Custom application code

Target distributions: Fedora 39+, RHEL 9, Ubuntu 22.04+, Debian 12+

Package Collection Configuration

Using rpm-build-assist with the package-collection type:

# build-assist.yaml
base: fedora-43-x86_64

build:
  - type: dist-git
    url: https://src.fedoraproject.org/rpms/
    packages:
      - python3.12:release-3.12
      - python3.12-pip:release-24.0
      - python3.12-numpy:release-1.26
      - python3.12-pandas:release-2.1

  - type: git
    url: https://github.com/myorg/myapp.git
    packages:
      - myapp:main

install:
  type: package-collection
  collection: myapp
  prefix: /opt/myapp
  packages:
    - python3.12
    - python3.12-pip
    - python3.12-numpy
    - python3.12-pandas
    - myapp

What This Produces

The package-collection build creates a self-contained directory structure:

/opt/myapp/
├── bin/
│   ├── python3.12          # Python interpreter
│   ├── pip3.12             # Package installer
│   └── myapp               # Your application
├── lib/
│   ├── libpython3.12.so    # Python runtime library
│   └── python3.12/         # Python standard library
│       └── site-packages/  # Installed packages
│           ├── numpy/
│           ├── pandas/
│           └── myapp/
└── share/
    └── myapp/              # Application data

Key feature: All executables in /opt/myapp/bin have rpath set to find libraries in /opt/myapp/lib automatically.

How Package Collections Work

The Role of rpath

rpath (runtime path) is an ELF binary attribute that tells the dynamic linker where to find shared libraries. When building a package collection, rpm-build-assist automatically:

  • Sets rpath on all executables to point to the collection's lib directory
  • Ensures libraries can find other libraries within the collection
  • Allows the collection to be self-contained and relocatable

Example rpath in /opt/myapp/bin/python3.12:

$ readelf -d /opt/myapp/bin/python3.12 | grep RPATH
 0x000000000000000f (RPATH)    Library rpath: [/opt/myapp/lib]

Dependency Resolution

The package collection includes:

  • Bundled dependencies: Libraries that vary between distributions (Python, NumPy, custom libs)
  • System dependencies: Stable libraries present on all target systems (glibc, libm, libpthread)

The RPM spec files control this split:

# In python3.12.spec
BuildRequires: gcc, make, openssl-devel
Requires: glibc >= 2.34
# Does NOT require specific system Python

# Collection build includes python3.12 and its libs
# Runtime only requires compatible glibc (available everywhere)

Distribution and Installation

The resulting package collection can be distributed as:

  • Tarball: Simple tar.gz of /opt/myapp for manual extraction
  • RPM: An RPM that installs the collection (works on RPM-based distros)
  • DEB: Convert to .deb using alien or build scripts (for Debian/Ubuntu)
  • Container base: Use as the base for container images

Users can run your application simply:

# After extracting to /opt/myapp
$ /opt/myapp/bin/myapp

# Or add to PATH
$ export PATH="/opt/myapp/bin:$PATH"
$ myapp

Building Complex Applications

Package collections excel when you need to bundle multiple interdependent components.

Example: Web Application with Custom Runtime

Build a complete web application stack with specific versions:

# build-assist.yaml
base: fedora-43-x86_64

build:
  - type: dist-git
    url: https://src.fedoraproject.org/rpms/
    packages:
      - nodejs:release-20
      - postgresql:release-16
      - nginx:release-1.25

  - type: git
    url: https://github.com/myorg/
    packages:
      - myapp-frontend:main
      - myapp-backend:main
      - myapp-database:main

install:
  type: package-collection
  collection: myapp-stack
  prefix: /opt/myapp-stack
  packages:
    - nodejs
    - postgresql
    - nginx
    - myapp-frontend
    - myapp-backend
    - myapp-database

This creates a complete, portable application stack at /opt/myapp-stack with:

  • Specific versions of Node.js, PostgreSQL, and nginx
  • Your application components
  • All dependencies properly linked via rpath
  • Configuration files in /opt/myapp-stack/etc

Benefits of the Package Collection Approach

Comparison with Other Approaches

Static Linking

Approach: Compile all dependencies into the executable.

Advantages:

  • Single binary distribution
  • No runtime dependencies

Disadvantages:

  • Very large binaries
  • Can't update libraries independently
  • License complications (GPL, etc.)
  • Doesn't work for interpreted languages

AppImage / Flatpak / Snap

Approach: Distribution-specific packaging formats.

Advantages:

  • Desktop integration
  • Sandboxing features
  • Built-in update mechanisms

Disadvantages:

  • Focused on desktop apps, not servers
  • Additional runtime requirements
  • May require specific kernel features

Containers (Docker, Podman)

Approach: Bundle application with entire userspace.

Advantages:

  • Complete isolation
  • Includes entire OS environment
  • Industry standard for deployment

Disadvantages:

  • Requires container runtime
  • Larger distribution size
  • Additional complexity
  • May require root/privileges

Package Collections (RPM)

Approach: Bundle variable dependencies with rpath.

Advantages:

  • Native binaries, no runtime needed
  • Smaller than containers
  • Works as regular processes
  • No special privileges required
  • Reusable package components

Disadvantages:

  • Requires compatible glibc
  • Less isolation than containers

Best Practices

What to Bundle

Include in your package collection:

  • Language runtimes (Python, Node.js, Ruby) if you need specific versions
  • Application-specific libraries not available in all target distributions
  • Libraries that frequently have incompatible versions across distros
  • Your application code and its direct dependencies

What NOT to Bundle

Rely on system packages for:

  • glibc and core system libraries (present everywhere)
  • Common libraries with stable ABIs (zlib, OpenSSL if using system crypto)
  • Kernel interfaces and system services
  • Libraries that require system integration (PAM, systemd)

Why: Bundling too much increases size and can cause compatibility issues with system-level features.

Testing for Portability

Before distributing your package collection:

  • Test on minimum supported distribution versions (e.g., oldest Ubuntu LTS)
  • Verify with ldd that bundled executables find their libraries
  • Check for unintended system dependencies with rpm -qpR
  • Test extraction and running as non-root user
  • Verify it works without modifications to system PATH or LD_LIBRARY_PATH