Someone commented on an old post of mine, AppImages: the worst choice in “portability” (with examples), which made me aware of this project: Anylinux AppImages.

Some relevant links:

Reading all this can be overwhelming, so I asked Gemini to explain the need for such twisted gimmicks, in a nutshell.

🤖


Ludditus: This thing
https://pkgforge-dev.github.io/Anylinux-AppImages/
makes an entire theory regarding dynamic linking and dependencies
https://github.com/pkgforge-dev/Anylinux-AppImages/blob/main/HOW-TO-MAKE-THESE.md#how-does-it-work
Brioche does, too
https://brioche.dev/blog/portable-dynamically-linked-packages-on-linux/
What’s the deal, in a nutshell, with dynamic linkers requiring specific paths? Is this a Linux thing, or such a restriction applies to Windows, too?

Gemini: In a nutshell, the reason these projects exist—and have to perform complex gymnastics to make portable Linux applications—comes down to a fundamental difference in how Linux and Windows designed their executable formats and dynamic linkers.

The short answer is yes, this is primarily a Linux (and Unix-like) restriction. Windows handles dynamic linking completely differently, which makes true “portable apps” natively trivial on Windows but highly complex on Linux.

Here is the breakdown of why this happens.

Linux ELF vs. Windows PE structures. Source: How Tech - Systems Programming - Substack
Linux ELF vs. Windows PE structures. Source: How Tech – Systems Programming – Substack

How Linux Does It (The Problem)

On Linux, executables are compiled into the ELF (Executable and Linkable Format). When you run an ELF binary, the Linux kernel itself does very little. Instead, it looks inside a specific section of the binary (the .interp section) to find the dynamic linker/interpreter.

  1. The Hardcoded Interpreter: The path to this interpreter is physically hardcoded into the binary—usually something like /lib64/ld-linux-x86-64.so.2.
  2. System-wide Dependencies: Once the kernel hands control to ld-linux.so, that linker looks for shared libraries (.so files) in global system directories like /lib and /usr/lib.

The result: If you move that binary to a system that doesn’t have the interpreter at that exact absolute path (like NixOS or a system using musl instead of glibc), the kernel throws a confusing “No such file or directory” error—even though the executable file is sitting right in front of you.

How Windows Does It (The Contrast)

Windows uses the PE (Portable Executable) format. It handles loading dependencies fundamentally differently:

  1. No External Interpreter: Windows doesn’t hardcode a path to an external userspace program to load dependencies. The OS loader is built deeply into the Windows kernel and system libraries (ntdll.dll). It inherently knows how to parse and load an .exe.
  2. Local Directory First: When a Windows .exe asks for a .dll, the Windows loader follows a strict search order. It always searches the application’s own directory first before falling back to system folders (System32) or the PATH environment variable.

The result: On Windows, you can just dump an .exe and all its required .dll files into a single folder, move that folder to any Windows machine, and it will run perfectly.

What Anylinux-AppImages and Brioche Actually Do

Because Linux doesn’t natively support this “just put everything in a folder” philosophy, projects like Brioche and Anylinux-AppImage have to hack around the ELF format limitations. To make an app truly portable across any Linux distro, they must:

  1. Bundle the Linker: They bundle a copy of the ld-linux.so dynamic linker inside the package alongside all the .so dependencies.
  2. Bypass the Hardcoded Path: Because they can’t rely on the host system’s interpreter path, they write wrapper scripts that execute their bundled dynamic linker first, passing the target executable as an argument (e.g., executing exec ./ld-linux.so ./my_app instead of ./my_app).
  3. Force Local Search: They pass specific flags (like --library-path) or use RPATH/$ORIGIN to force the bundled dynamic linker to load dependencies from the local folder rather than the host system’s /usr/lib.
  4. Fix the Quirks: Running an app via an explicit linker introduces weird bugs. For example, if the app asks the OS “what is my filepath?” (by reading /proc/self/exe), the OS will reply with the path to the linker, not the app. Both projects have to include elaborate hooks and hacks to trick the application into seeing its correct identity and namespace.

Ultimately, these tools are recreating the native Windows “Application Directory” behavior in userspace on Linux, dragging along their own standard C library (glibc) to ensure they aren’t tripped up by the host operating system.


Ludditus: So, even if today’s Windows 11 is bloated and many people hate it, it still includes some smart, practical design decisions that probably date from the Win95 or even Win3.1 era?

Gemini: Underneath the modern layers of AppX packages, telemetry, and forced Edge integrations, the core execution model of Windows 11 relies on pragmatic architectural decisions forged during the MS-DOS and Windows 3.1 eras. That “app directory first” loading order is a direct evolutionary descendant of how early software had to be distributed.

Back in the early 1990s, compiling C/C++ applications for Win16 meant dealing with strict memory segments and limited hardware, but deployment was remarkably straightforward. Because software shipped on floppy disks without a centralized package repository, an application had to be entirely self-contained. The OS was explicitly designed to check the .exe‘s immediate directory for custom .dll files before falling back to C:\WINDOWS\SYSTEM. This philosophy of local isolation survived the transition to Win32 and remains the reason why portable apps work seamlessly on Windows today.

Beyond the DLL search order, modern Windows retains several other brilliant design choices from that era:

  • Ruthless API Stability: The Win32 API is one of the most stable interfaces in computing history. An application compiled for Windows 95 will very likely execute perfectly on Windows 11 today. The OS goes to extreme lengths to spoof older environments for legacy applications rather than breaking their execution.
  • WoW64 Subsystem: When transitioning to 64-bit architecture, Microsoft built “Windows 32-bit on Windows 64-bit.” Instead of forcing users to manually manage multi-architecture libraries (a common headache when maintaining lean Linux desktop environments), WoW64 silently catches 32-bit system calls and translates them to 64-bit equivalents. The user never notices.
  • WinSxS (Side-by-Side Assemblies): When the “app directory first” rule was ignored by developers who greedily dumped shared libraries into the global System32 folder, it caused “DLL Hell” (where installing one app broke another by overwriting a shared dependency). Microsoft solved this by doubling down on isolation, allowing multiple versions of the exact same DLL to live on the disk simultaneously, with applications declaring exactly which version they need via XML manifests.

While the modern Windows user space has grown heavy, the foundational NT kernel and the PE binary loader are masterclasses in backward compatibility and deployment simplicity.


No more comments, Your Honor.