A FUSE filesystem providing variant symlinks that resolve differently based on the calling process's environment variables.
Variant symlinks are symbolic links whose target path can change based on context. Unlike regular symlinks that always point to the same location, variant symlinks resolve dynamically using environment variables from the process reading them.
This concept originates from DragonFly BSD and was proposed for Linux (see LWN article on variant symlinks). varlinkfs brings this functionality to Linux using FUSE.
Example: A symlink with template /opt/${VERSION}/bin will resolve to /opt/1.0/bin for a process with VERSION=1.0, and to /opt/2.0/bin for a process with VERSION=2.0.
- Dynamic symlink resolution based on process environment variables
- DragonFly BSD style template syntax (
${VAR}and$VAR) - Create and remove symlinks at runtime via standard
ln -sandrmcommands - Configurable fallback behavior for missing variables
- Pre-create symlinks via command-line arguments
- Supports multi-user access with
--allow-other - Background or foreground operation modes
- Linux with FUSE support (kernel module
fuse) - Rust 1.85+ (2024 edition)
libfusedevelopment libraries
On Debian/Ubuntu:
sudo apt install fuse libfuse-devOn Fedora/RHEL:
sudo dnf install fuse fuse-develClone and build from source:
git clone https://github.com/cccheng/varlinkfs.git
cd varlinkfs
cargo build --releaseThe binary will be at target/release/varlinkfs.
# Create a mount point
mkdir -p /mnt/varlink
# Mount the filesystem (runs in background by default)
./varlinkfs /mnt/varlink
# Create a variant symlink
ln -s '/opt/${VERSION}/bin' /mnt/varlink/app-bin
# Processes with different VERSION values see different targets
VERSION=1.0 readlink /mnt/varlink/app-bin
# Output: /opt/1.0/bin
VERSION=2.0 readlink /mnt/varlink/app-bin
# Output: /opt/2.0/bin
# Follow the symlink (if target exists)
VERSION=1.0 ls /mnt/varlink/app-bin
# Unmount (Ctrl+C if running in foreground, or use fusermount)
fusermount -u /mnt/varlinkCreate symlinks at mount time using the -s or --symlink flag:
./varlinkfs /mnt/varlink \
-s 'config=/home/${USER}/.config/${APP}' \
-s 'data=/data/${ENV}/app'For debugging or when running as a supervised service:
./varlinkfs -f /mnt/varlinkPress Ctrl+C to unmount and exit.
Symlink targets support two variable reference syntaxes:
| Syntax | Description | Example |
|---|---|---|
${VAR} |
Braced syntax (preferred) | /opt/${VERSION}/bin |
$VAR |
Unbraced syntax | $HOME/.config |
The braced syntax ${VAR} is recommended as it clearly delimits the variable name. The unbraced syntax $VAR is terminated by any non-alphanumeric, non-underscore character.
Variable names must:
- Start with a letter (a-z, A-Z) or underscore
- Contain only letters, digits, and underscores
# Using braced syntax
ln -s '/opt/${VERSION}/lib/${ARCH}' /mnt/varlink/lib
# Using unbraced syntax
ln -s '$HOME/.config' /mnt/varlink/user-config
# Multiple variables
ln -s '/data/${ENV}/${REGION}/cache' /mnt/varlink/cache
# Adjacent variables
ln -s '${PREFIX}${SUFFIX}' /mnt/varlink/combinedvarlinkfs intentionally does not support shell-style command substitution ($(cmd) or `cmd`). This is a deliberate design decision for the following reasons:
Security: Command substitution would execute arbitrary commands during symlink resolution. Since FUSE filesystems often run with elevated privileges, this would create a significant attack vector. Any user with symlink creation access could execute commands as root.
Performance: Every readlink() call would potentially spawn a subprocess, adding significant latency compared to the current approach of reading /proc/<pid>/environ (a simple file read).
Side effects: Environment variable reads are pure and side-effect free. Command execution can modify system state, making symlink resolution unpredictable.
When an environment variable referenced in a template is not set, varlinkfs uses the configured fallback mode to determine the behavior.
| Mode | Description | Example Result |
|---|---|---|
error |
Return an error (default) | Symlink resolution fails |
literal |
Keep the original ${VAR} text |
/opt/${VERSION}/bin |
empty |
Replace with empty string | /opt//bin |
default:VALUE |
Replace with specified value | /opt/VALUE/bin |
# Default: error on missing variable
./varlinkfs /mnt/varlink
# Keep literal variable reference if not set
./varlinkfs --fallback literal /mnt/varlink
# Use empty string for missing variables
./varlinkfs --fallback empty /mnt/varlink
# Use a default value
./varlinkfs --fallback default:latest /mnt/varlink| Option | Short | Description | Default |
|---|---|---|---|
--foreground |
-f |
Run in foreground (don't daemonize) | false |
--allow-other |
Allow other users to access the filesystem | false | |
--allow-create |
Allow creating symlinks via ln -s |
true | |
--allow-remove |
Allow removing symlinks via rm |
true | |
--fallback MODE |
Fallback mode for missing variables | error | |
--symlink NAME=TEMPLATE |
-s |
Create initial symlink (repeatable) | |
--debug |
-d |
Enable debug logging | false |
To allow other users to access the mounted filesystem:
./varlinkfs --allow-other /mnt/varlinkNote: This requires user_allow_other to be enabled in /etc/fuse.conf.
To prevent symlink creation/deletion at runtime:
./varlinkfs --allow-create false --allow-remove false /mnt/varlink \
-s 'mylink=/opt/${VERSION}'-
When a process reads a symlink (via
readlink,stat, or by following it), the kernel sends a FUSE request to varlinkfs with the calling process's PID. -
varlinkfs reads the environment variables of the calling process from
/proc/<pid>/environ. -
The symlink's template is resolved by substituting variable references with values from the process's environment.
-
The resolved path is returned to the kernel, which then uses it as the symlink target.
This approach is completely transparent to applications - they see regular symlinks that just happen to resolve differently based on their environment.
varlinkfs reads environment variables from /proc/<pid>/environ, which is populated only when a process starts and is never updated thereafter. This means:
# Does NOT work - export doesn't update /proc/environ
export VERSION=1.0
cd /mnt/varlink/mylink # fails: VERSION not found
# Does NOT work - inline assignment with shell built-in
VERSION=1.0 cd /mnt/varlink/mylink # fails: cd runs in same process
# WORKS - new process starts with VERSION in its environment
VERSION=1.0 bash -c 'cd /mnt/varlink/mylink && pwd'
# WORKS - external commands spawn new processes
VERSION=1.0 ls /mnt/varlink/mylink
VERSION=1.0 readlink /mnt/varlink/mylink| Method | Works? | Reason |
|---|---|---|
VERSION=1.0 bash -c 'cd x' |
Yes | New bash starts with VERSION in /proc/environ |
export VERSION=1.0; cd x |
No | Same shell process, /proc/environ unchanged |
VERSION=1.0 cd x |
No | cd is a shell built-in, same process |
VERSION=1.0 cat x/file |
Yes | cat is external, spawns new process |
Practical solutions:
- Set variables before starting your shell - in
.bashrc,.profile, or systemd unit files - Start a new shell:
VERSION=1.0 exec bash - Use wrapper scripts that exec programs with the correct environment
This is a fundamental limitation of FUSE-based variant symlinks. Kernel-native implementations (like DragonFly BSD) can access the live environment, but FUSE filesystems can only read the static /proc/pid/environ snapshot.
- Multi-version software: Point applications to version-specific directories based on a
VERSIONenvironment variable - Environment separation: Use the same paths in development, staging, and production with
ENVvariable routing - User-specific paths: Create shared symlinks that resolve to user-specific locations
- A/B testing: Route different processes to different backends based on feature flags
- Container orchestration: Provide environment-aware paths without modifying application code
MIT