nix: Add FHS wrappers for venv binaries (ruff, pyrefly) on NixOS (#40860)

On NixOS, dynamically linked binaries from the Python venv (installed
via uv/pip) cannot run directly because they expect standard Linux
library paths that don't exist on NixOS.

This commit adds FHS wrappers that allow these binaries to run in an
FHS-compatible environment at runtime, without patching them.

I considered using the nixpkgs versions of ruff and pyrefly directly, or
overriding their derivations to match the versions in requirements.txt.
However, decided against it because:
- Version mismatches between nixpkgs and requirements.txt caused type
checking incompatibilities (pyrefly 0.34.0 vs 0.23.1)
- Building these tools from source in nix is slow and adds significant
time to nix-shell initialization (both are rust packages that take quite
some time to build)


Testing: just improvements to the NixOS development environment, no test
needed

**Before:**
```
➜ servo (main) ✔ nix-shell
➜ servo (main) ✔ ./mach fmt
Could not start dynamically linked executable: /home/dyego/coding/random/servo/.venv/bin/ruff
NixOS cannot run dynamically linked executables intended for generic
linux environments out of the box. For more information, see:
https://nix.dev/permalink/stub-ld
➜ servo (main) ✔ ./mach test-tidy
 ➤  Checking config file (./servo-tidy.toml)...
 ➤  Checking directories for correct file extensions...
Could not start dynamically linked executable: ruff
NixOS cannot run dynamically linked executables intended for generic
linux environments out of the box. For more information, see:
https://nix.dev/permalink/stub-ld
Error running mach:

    ['test-tidy']

The error occurred in code that was called by the mach command. This is either
a bug in the called code itself or in the way that mach is calling it.
You can invoke |./mach busted| to check if this issue is already on file. If it
isn't, please use |./mach busted file| to report it. If |./mach busted| is
misbehaving, you can also inspect the dependencies of bug 1543241.

If filing a bug, please include the full output of mach, including this error
message.

The details of the failure are as follows:

json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

  File "/home/dyego/coding/random/servo/python/servo/testing_commands.py", line 322, in test_tidy
    tidy_failed = tidy.scan(not all_files, not no_progress, github_annotations)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/dyego/coding/random/servo/python/tidy/tidy.py", line 919, in scan
    for error in errors:
  File "/home/dyego/coding/random/servo/python/tidy/tidy.py", line 401, in check_ruff_lints
    for error in json.loads(e.output):
                 ^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/2g9b898aq9kmizmhmhbdip5mixrc5wrk-python3-3.11.14/lib/python3.11/json/__init__.py", line 346, in loads
    return _default_decoder.decode(s)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/2g9b898aq9kmizmhmhbdip5mixrc5wrk-python3-3.11.14/lib/python3.11/json/decoder.py", line 337, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/2g9b898aq9kmizmhmhbdip5mixrc5wrk-python3-3.11.14/lib/python3.11/json/decoder.py", line 355, in raw_decode
    raise JSONDecodeError("Expecting value", s, err.value) from None
➜ servo (main) ✔
```

(note that the `JSONDecodeError` is because we're trying to parse the
"NixOS cannot run dynamically linked executables intended..." string as
JSON)

**Now:**
```
➜ servo (fix-nix-mach) ✔ nix-shell
➜ servo (fix-nix-mach) ✔ ./mach fmt
➜ servo (fix-nix-mach) ✔ ./mach test-tidy
 ➤  Checking config file (./servo-tidy.toml)...
 ➤  Checking directories for correct file extensions...
 ➤  Checking type annotations in python files ...
 ➤  Skipping WPT lint checks, because no relevant files changed.
 ➤  Running `cargo-deny` checks...
 ➤  Checking formatting of Rust files...
 ➤  Checking formatting of python files...
 ➤  Checking formatting of toml files...

  test-tidy reported no errors.
➜ servo (fix-nix-mach) ✔
```

Signed-off-by: Dyego Aurélio <dyegoaurelio@gmail.com>
This commit is contained in:
dyegoaurelio
2025-11-25 12:20:49 -03:00
committed by GitHub
parent 795e5b4fb3
commit bc81a4f11a
3 changed files with 29 additions and 0 deletions

4
mach
View File

@@ -37,6 +37,10 @@
import os
import sys
# On NixOS, prepend nix-provided binary paths so they take precedence over .venv/bin
if 'SERVO_NIX_BIN_DIR' in os.environ:
os.environ['PATH'] = os.environ['SERVO_NIX_BIN_DIR'] + os.pathsep + os.environ.get('PATH', '')
def main(args):
topdir = os.path.abspath(os.path.dirname(sys.argv[0]))
sys.path.insert(0, os.path.join(topdir, "python"))

View File

@@ -147,6 +147,9 @@ def _activate_virtualenv(topdir: str) -> None:
script_dir = "Scripts" if _is_windows() else "bin"
runpy.run_path(os.path.join(virtualenv_path, script_dir, "activate_this.py"))
# On NixOS, prepend nix-provided binary paths so they take precedence over .venv/bin
if "SERVO_NIX_BIN_DIR" in os.environ:
os.environ["PATH"] = os.environ["SERVO_NIX_BIN_DIR"] + os.pathsep + os.environ.get("PATH", "")
install_virtual_env_requirements(topdir, marker_path)

View File

@@ -52,6 +52,19 @@ let
];
};
androidSdk = androidComposition.androidsdk;
# FHS wrappers for Python tools installed via venv (e.g ruff, pyrefly)
# These allow running dynamically linked binaries on NixOS without patching them.
# The wrappers provide an FHS environment at runtime.
mkVenvFhsWrapper =
name:
buildFHSEnv {
inherit name;
runScript = writeShellScript "${name}-fhs" ''
exec "${toString ./.}/.venv/bin/${name}" "$@"
'';
};
# Required by ./mach build --android
androidEnvironment = lib.optionalAttrs buildAndroid rec {
ANDROID_SDK_ROOT = "${androidSdk}/libexec/android-sdk";
@@ -190,6 +203,15 @@ stdenv.mkDerivation (androidEnvironment // {
# get patched in a way that makes them dependent on the Nix store.
repo_root=$(git rev-parse --show-toplevel)
export RUSTUP_HOME=$repo_root/.rustup
else
# On NixOS, export FHS wrapper paths so mach can prepend them to PATH at runtime
# This ensures the FHS-wrapped binaries take precedence over .venv/bin
export SERVO_NIX_BIN_DIR="${
lib.makeBinPath [
(mkVenvFhsWrapper "ruff")
(mkVenvFhsWrapper "pyrefly")
]
}"
fi
'';
})