Skip to content

Commit

Permalink
build.hooks.make-venv: Replace symlink to Python interpreter with a b…
Browse files Browse the repository at this point in the history
…inary wrapper

The venv module creates a symlink to the Python interpreter,
but this breaks the venv if you're using a symlink pointing
to the virtualenv itself.

By replacing the Python interpreter symlink with a wrapper it can be
properly linked to in another derivation.
  • Loading branch information
adisbladis committed Jan 26, 2025
1 parent 9154d35 commit 155acd8
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 0 deletions.
18 changes: 18 additions & 0 deletions build/checks/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,24 @@ let
touch $out
'';

symlinked-venv =
let
# Create a derivation with a symlink to interpreter.
# This would break the vanilla venv module.
sym = pkgs.runCommand "symlinked-venv" { } ''
mkdir -p $out/bin
ln -s ${testVenv}/bin/python $out/bin/python
'';
in
pkgs.runCommand "symlinked-venv-test"
{
nativeBuildInputs = [ sym ];
}
''
python -c 'import packaging'
touch $out
'';

prebuilt-wheel = pythonSet.pythonPkgsHostHost.callPackage (
{
stdenv,
Expand Down
41 changes: 41 additions & 0 deletions build/hooks/make-venv/make_venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import fnmatch
import os.path
import shutil
import subprocess
import sys
from functools import lru_cache
from pathlib import Path
Expand Down Expand Up @@ -262,6 +263,45 @@ def fixup_pyvenv(python_root: Path, out_root: Path) -> None:
pyvenv_f.write(pyvenv)


def wrap_python_bin(bin: Path, target: Path):
# Replace symlinks to Python binaries in venv with wrappers
# So a symlink pointing to the venv will still work.
#
# The venv module creates a symlink to the Python interpreter,
# but this breaks the venv if you're using a symlink pointing
# to the virtualenv itself.
#
# By replacing the Python interpreter symlink with a wrapper it can be
# properly linked to in another derivation.
subprocess.run(
["cc", "-Wall", "-Werror", "-Wpedantic", "-Wno-overlength-strings", "-Os", "-x", "c", "-o", bin, "-"],
check=True,
input=(
"""
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char **argv) {
argv[0] = "%s";
return execv("%s", argv);
}
"""
% (bin, target)
).encode(),
)


def wrap_python(python_root: Path, out_root: Path):
for bin in out_root.joinpath("bin").iterdir():
st_mode = lstat(bin).st_mode
if not S_ISLNK(st_mode):
continue

target = bin.readlink()
if target.is_relative_to(python_root):
wrap_python_bin(bin, target)


def main():
args = arg_parser.parse_args(namespace=ArgsNS)

Expand Down Expand Up @@ -302,6 +342,7 @@ def main():
builder.setup_python(context)
builder.create_configuration(context)
fixup_pyvenv(python_root, out_root)
wrap_python(python_root, out_root)

skip_paths = [
# Let other hooks manage nix-support
Expand Down

0 comments on commit 155acd8

Please sign in to comment.