Merge pull request #297628 from cwp/python-env-venv

Fix venv creation in Python environments

authored by Domen Kožar and committed by GitHub fb884172 350f0a9a

+132 -72
+46
pkgs/build-support/setup-hooks/make-binary-wrapper/make-binary-wrapper.sh
··· 19 19 # (if unset or empty, defaults to EXECUTABLE) 20 20 # --inherit-argv0 : the executable inherits argv0 from the wrapper. 21 21 # (use instead of --argv0 '$0') 22 + # --resolve-argv0 : if argv0 doesn't include a / character, resolve it against PATH 22 23 # --set VAR VAL : add VAR with value VAL to the executable's environment 23 24 # --set-default VAR VAL : like --set, but only adds VAR if not already set in 24 25 # the environment ··· 87 88 makeCWrapper() { 88 89 local argv0 inherit_argv0 n params cmd main flagsBefore flagsAfter flags executable length 89 90 local uses_prefix uses_suffix uses_assert uses_assert_success uses_stdio uses_asprintf 91 + local resolve_path 90 92 executable=$(escapeStringLiteral "$1") 91 93 params=("$@") 92 94 length=${#params[*]} ··· 169 171 # Whichever comes last of --argv0 and --inherit-argv0 wins 170 172 inherit_argv0=1 171 173 ;; 174 + --resolve-argv0) 175 + # this gets processed after other argv0 flags 176 + uses_stdio=1 177 + uses_string=1 178 + resolve_argv0=1 179 + ;; 172 180 *) # Using an error macro, we will make sure the compiler gives an understandable error message 173 181 main="$main#error makeCWrapper: Unknown argument ${p}"$'\n' 174 182 ;; ··· 176 184 done 177 185 [[ -z "$flagsBefore" && -z "$flagsAfter" ]] || main="$main"${main:+$'\n'}$(addFlags "$flagsBefore" "$flagsAfter")$'\n'$'\n' 178 186 [ -z "$inherit_argv0" ] && main="${main}argv[0] = \"${argv0:-${executable}}\";"$'\n' 187 + [ -z "$resolve_argv0" ] || main="${main}argv[0] = resolve_argv0(argv[0]);"$'\n' 179 188 main="${main}return execv(\"${executable}\", argv);"$'\n' 180 189 181 190 [ -z "$uses_asprintf" ] || printf '%s\n' "#define _GNU_SOURCE /* See feature_test_macros(7) */" ··· 183 192 printf '%s\n' "#include <stdlib.h>" 184 193 [ -z "$uses_assert" ] || printf '%s\n' "#include <assert.h>" 185 194 [ -z "$uses_stdio" ] || printf '%s\n' "#include <stdio.h>" 195 + [ -z "$uses_string" ] || printf '%s\n' "#include <string.h>" 186 196 [ -z "$uses_assert_success" ] || printf '\n%s\n' "#define assert_success(e) do { if ((e) < 0) { perror(#e); abort(); } } while (0)" 187 197 [ -z "$uses_prefix" ] || printf '\n%s\n' "$(setEnvPrefixFn)" 188 198 [ -z "$uses_suffix" ] || printf '\n%s\n' "$(setEnvSuffixFn)" 199 + [ -z "$resolve_argv0" ] || printf '\n%s\n' "$(resolveArgv0Fn)" 189 200 printf '\n%s' "int main(int argc, char **argv) {" 190 201 printf '\n%s' "$(indent4 "$main")" 191 202 printf '\n%s\n' "}" ··· 334 345 } else { 335 346 assert_success(setenv(env, suffix, 1)); 336 347 } 348 + } 349 + " 350 + } 351 + 352 + resolveArgv0Fn() { 353 + printf '%s' "\ 354 + char *resolve_argv0(char *argv0) { 355 + if (strchr(argv0, '/') != NULL) { 356 + return argv0; 357 + } 358 + char *path = getenv(\"PATH\"); 359 + if (path == NULL) { 360 + return argv0; 361 + } 362 + char *path_copy = strdup(path); 363 + if (path_copy == NULL) { 364 + return argv0; 365 + } 366 + char *dir = strtok(path_copy, \":\"); 367 + while (dir != NULL) { 368 + char *candidate = malloc(strlen(dir) + strlen(argv0) + 2); 369 + if (candidate == NULL) { 370 + free(path_copy); 371 + return argv0; 372 + } 373 + sprintf(candidate, \"%s/%s\", dir, argv0); 374 + if (access(candidate, X_OK) == 0) { 375 + free(path_copy); 376 + return candidate; 377 + } 378 + free(candidate); 379 + dir = strtok(NULL, \":\"); 380 + } 381 + free(path_copy); 382 + return argv0; 337 383 } 338 384 " 339 385 }
+4
pkgs/build-support/setup-hooks/make-wrapper.sh
··· 15 15 # (if unset or empty, defaults to EXECUTABLE) 16 16 # --inherit-argv0 : the executable inherits argv0 from the wrapper. 17 17 # (use instead of --argv0 '$0') 18 + # --resolve-argv0 : if argv0 doesn't include a / character, resolve it against PATH 18 19 # --set VAR VAL : add VAR with value VAL to the executable's environment 19 20 # --set-default VAR VAL : like --set, but only adds VAR if not already set in 20 21 # the environment ··· 177 178 elif [[ "$p" == "--inherit-argv0" ]]; then 178 179 # Whichever comes last of --argv0 and --inherit-argv0 wins 179 180 argv0='$0' 181 + elif [[ "$p" == "--resolve-argv0" ]]; then 182 + # this is noop in shell wrappers, since bash will always resolve $0 183 + resolve_argv0=1 180 184 else 181 185 die "makeWrapper doesn't understand the arg $p" 182 186 fi
-2
pkgs/development/interpreters/python/cpython/2.7/default.nix
··· 318 318 inherit passthru; 319 319 320 320 postFixup = '' 321 - # Include a sitecustomize.py file. Note it causes an error when it's in postInstall with 2.7. 322 - cp ${../../sitecustomize.py} $out/${sitePackages}/sitecustomize.py 323 321 '' + lib.optionalString strip2to3 '' 324 322 rm -R $out/bin/2to3 $out/lib/python*/lib2to3 325 323 '' + lib.optionalString stripConfig ''
+1 -2
pkgs/development/interpreters/python/cpython/default.nix
··· 537 537 # Strip tests 538 538 rm -R $out/lib/python*/test $out/lib/python*/**/test{,s} 539 539 '' + optionalString includeSiteCustomize '' 540 - # Include a sitecustomize.py file 541 - cp ${../sitecustomize.py} $out/${sitePackages}/sitecustomize.py 540 + 542 541 '' + optionalString stripBytecode '' 543 542 # Determinism: deterministic bytecode 544 543 # First we delete all old bytecode.
-3
pkgs/development/interpreters/python/pypy/default.nix
··· 126 126 ln -s $out/${executable}-c/include $out/include/${libPrefix} 127 127 ln -s $out/${executable}-c/lib-python/${if isPy3k then "3" else pythonVersion} $out/lib/${libPrefix} 128 128 129 - # Include a sitecustomize.py file 130 - cp ${../sitecustomize.py} $out/${if isPy38OrNewer then sitePackages else "lib/${libPrefix}/${sitePackages}"}/sitecustomize.py 131 - 132 129 runHook postInstall 133 130 ''; 134 131
-3
pkgs/development/interpreters/python/pypy/prebuilt.nix
··· 95 95 echo "Removing bytecode" 96 96 find . -name "__pycache__" -type d -depth -delete 97 97 98 - # Include a sitecustomize.py file 99 - cp ${../sitecustomize.py} $out/${sitePackages}/sitecustomize.py 100 - 101 98 runHook postInstall 102 99 ''; 103 100
-3
pkgs/development/interpreters/python/pypy/prebuilt_2_7.nix
··· 96 96 echo "Removing bytecode" 97 97 find . -name "__pycache__" -type d -depth -delete 98 98 99 - # Include a sitecustomize.py file 100 - cp ${../sitecustomize.py} $out/${sitePackages}/sitecustomize.py 101 - 102 99 runHook postInstall 103 100 ''; 104 101
-39
pkgs/development/interpreters/python/sitecustomize.py
··· 1 - """ 2 - This is a Nix-specific module for discovering modules built with Nix. 3 - 4 - The module recursively adds paths that are on `NIX_PYTHONPATH` to `sys.path`. In 5 - order to process possible `.pth` files `site.addsitedir` is used. 6 - 7 - The paths listed in `PYTHONPATH` are added to `sys.path` afterwards, but they 8 - will be added before the entries we add here and thus take precedence. 9 - 10 - Note the `NIX_PYTHONPATH` environment variable is unset in order to prevent leakage. 11 - 12 - Similarly, this module listens to the environment variable `NIX_PYTHONEXECUTABLE` 13 - and sets `sys.executable` to its value. 14 - """ 15 - import site 16 - import sys 17 - import os 18 - import functools 19 - 20 - paths = os.environ.pop('NIX_PYTHONPATH', None) 21 - if paths: 22 - functools.reduce(lambda k, p: site.addsitedir(p, k), paths.split(':'), site._init_pathinfo()) 23 - 24 - # Check whether we are in a venv or virtualenv. 25 - # For Python 3 we check whether our `base_prefix` is different from our current `prefix`. 26 - # For Python 2 we check whether the non-standard `real_prefix` is set. 27 - # https://stackoverflow.com/questions/1871549/determine-if-python-is-running-inside-virtualenv 28 - in_venv = (sys.version_info.major == 3 and sys.prefix != sys.base_prefix) or (sys.version_info.major == 2 and hasattr(sys, "real_prefix")) 29 - 30 - if not in_venv: 31 - executable = os.environ.pop('NIX_PYTHONEXECUTABLE', None) 32 - prefix = os.environ.pop('NIX_PYTHONPREFIX', None) 33 - 34 - if 'PYTHONEXECUTABLE' not in os.environ and executable is not None: 35 - sys.executable = executable 36 - if prefix is not None: 37 - # Sysconfig does not like it when sys.prefix is set to None 38 - sys.prefix = sys.exec_prefix = prefix 39 - site.PREFIXES.insert(0, prefix)
+71 -18
pkgs/development/interpreters/python/tests.nix
··· 39 39 is_virtualenv = "False"; 40 40 }; 41 41 } // lib.optionalAttrs (!python.isPyPy) { 42 - # Use virtualenv from a Nix env. 43 - nixenv-virtualenv = rec { 44 - env = runCommand "${python.name}-virtualenv" {} '' 45 - ${pythonVirtualEnv.interpreter} -m virtualenv venv 46 - mv venv $out 42 + # Use virtualenv with symlinks from a Nix env. 43 + nixenv-virtualenv-links = rec { 44 + env = runCommand "${python.name}-virtualenv-links" {} '' 45 + ${pythonVirtualEnv.interpreter} -m virtualenv --system-site-packages --symlinks --no-seed $out 46 + ''; 47 + interpreter = "${env}/bin/${python.executable}"; 48 + is_venv = "False"; 49 + is_nixenv = "True"; 50 + is_virtualenv = "True"; 51 + }; 52 + } // lib.optionalAttrs (!python.isPyPy) { 53 + # Use virtualenv with copies from a Nix env. 54 + nixenv-virtualenv-copies = rec { 55 + env = runCommand "${python.name}-virtualenv-copies" {} '' 56 + ${pythonVirtualEnv.interpreter} -m virtualenv --system-site-packages --copies --no-seed $out 47 57 ''; 48 58 interpreter = "${env}/bin/${python.executable}"; 49 59 is_venv = "False"; ··· 59 69 is_nixenv = "True"; 60 70 is_virtualenv = "False"; 61 71 }; 62 - } // lib.optionalAttrs (python.isPy3k && (!python.isPyPy)) { 63 - # Venv built using plain Python 72 + } // lib.optionalAttrs (python.pythonAtLeast "3.8" && (!python.isPyPy)) { 73 + # Venv built using links to plain Python 74 + # Python 2 does not support venv 75 + # TODO: PyPy executable name is incorrect, it should be pypy-c or pypy-3c instead of pypy and pypy3. 76 + plain-venv-links = rec { 77 + env = runCommand "${python.name}-venv-links" {} '' 78 + ${python.interpreter} -m venv --system-site-packages --symlinks --without-pip $out 79 + ''; 80 + interpreter = "${env}/bin/${python.executable}"; 81 + is_venv = "True"; 82 + is_nixenv = "False"; 83 + is_virtualenv = "False"; 84 + }; 85 + } // lib.optionalAttrs (python.pythonAtLeast "3.8" && (!python.isPyPy)) { 86 + # Venv built using copies from plain Python 64 87 # Python 2 does not support venv 65 88 # TODO: PyPy executable name is incorrect, it should be pypy-c or pypy-3c instead of pypy and pypy3. 66 - plain-venv = rec { 67 - env = runCommand "${python.name}-venv" {} '' 68 - ${python.interpreter} -m venv $out 89 + plain-venv-copies = rec { 90 + env = runCommand "${python.name}-venv-copies" {} '' 91 + ${python.interpreter} -m venv --system-site-packages --copies --without-pip $out 69 92 ''; 70 93 interpreter = "${env}/bin/${python.executable}"; 71 94 is_venv = "True"; 72 95 is_nixenv = "False"; 73 96 is_virtualenv = "False"; 74 97 }; 75 - 98 + } // lib.optionalAttrs (python.pythonAtLeast "3.8") { 99 + # Venv built using Python Nix environment (python.buildEnv) 100 + nixenv-venv-links = rec { 101 + env = runCommand "${python.name}-venv-links" {} '' 102 + ${pythonEnv.interpreter} -m venv --system-site-packages --symlinks --without-pip $out 103 + ''; 104 + interpreter = "${env}/bin/${pythonEnv.executable}"; 105 + is_venv = "True"; 106 + is_nixenv = "True"; 107 + is_virtualenv = "False"; 108 + }; 76 109 } // lib.optionalAttrs (python.pythonAtLeast "3.8") { 77 110 # Venv built using Python Nix environment (python.buildEnv) 78 - # TODO: Cannot create venv from a nix env 79 - # Error: Command '['/nix/store/ddc8nqx73pda86ibvhzdmvdsqmwnbjf7-python3-3.7.6-venv/bin/python3.7', '-Im', 'ensurepip', '--upgrade', '--default-pip']' returned non-zero exit status 1. 80 - nixenv-venv = rec { 81 - env = runCommand "${python.name}-venv" {} '' 82 - ${pythonEnv.interpreter} -m venv $out 111 + nixenv-venv-copies = rec { 112 + env = runCommand "${python.name}-venv-copies" {} '' 113 + ${pythonEnv.interpreter} -m venv --system-site-packages --copies --without-pip $out 83 114 ''; 84 115 interpreter = "${env}/bin/${pythonEnv.executable}"; 85 116 is_venv = "True"; ··· 91 122 testfun = name: attrs: runCommand "${python.name}-tests-${name}" ({ 92 123 inherit (python) pythonVersion; 93 124 } // attrs) '' 125 + mkdir $out 126 + 127 + # set up the test files 94 128 cp -r ${./tests/test_environments} tests 95 129 chmod -R +w tests 96 130 substituteAllInPlace tests/test_python.py 97 - ${attrs.interpreter} -m unittest discover --verbose tests #/test_python.py 98 - mkdir $out 131 + 132 + # run the tests by invoking the interpreter via full path 133 + echo "absolute path: ${attrs.interpreter}" 134 + ${attrs.interpreter} -m unittest discover --verbose tests 2>&1 | tee "$out/full.txt" 135 + 136 + # run the tests by invoking the interpreter via $PATH 137 + export PATH="$(dirname ${attrs.interpreter}):$PATH" 138 + echo "PATH: $(basename ${attrs.interpreter})" 139 + "$(basename ${attrs.interpreter})" -m unittest discover --verbose tests 2>&1 | tee "$out/path.txt" 140 + 141 + # make sure we get the right path when invoking through a result link 142 + ln -s "${attrs.env}" result 143 + relative="result/bin/$(basename ${attrs.interpreter})" 144 + expected="$PWD/$relative" 145 + actual="$(./$relative -c "import sys; print(sys.executable)" | tee "$out/result.txt")" 146 + if [ "$actual" != "$expected" ]; then 147 + echo "expected $expected, got $actual" 148 + exit 1 149 + fi 150 + 151 + # if we got this far, the tests passed 99 152 touch $out/success 100 153 ''; 101 154
+1 -1
pkgs/development/interpreters/python/tests/test_environments/test_python.py
··· 38 38 39 39 @unittest.skipIf(IS_PYPY or sys.version_info.major==2, "Python 2 does not have base_prefix") 40 40 def test_base_prefix(self): 41 - if IS_VENV or IS_NIXENV or IS_VIRTUALENV: 41 + if IS_VENV or IS_VIRTUALENV: 42 42 self.assertNotEqual(sys.prefix, sys.base_prefix) 43 43 else: 44 44 self.assertEqual(sys.prefix, sys.base_prefix)
+9 -1
pkgs/development/interpreters/python/wrapper.nix
··· 35 35 fi 36 36 mkdir -p "$out/bin" 37 37 38 + rm -f $out/bin/.*-wrapped 39 + 38 40 for path in ${lib.concatStringsSep " " paths}; do 39 41 if [ -d "$path/bin" ]; then 40 42 cd "$path/bin" ··· 42 44 if [ -f "$prg" ]; then 43 45 rm -f "$out/bin/$prg" 44 46 if [ -x "$prg" ]; then 45 - makeWrapper "$path/bin/$prg" "$out/bin/$prg" --set NIX_PYTHONPREFIX "$out" --set NIX_PYTHONEXECUTABLE ${pythonExecutable} --set NIX_PYTHONPATH ${pythonPath} ${lib.optionalString (!permitUserSite) ''--set PYTHONNOUSERSITE "true"''} ${lib.concatStringsSep " " makeWrapperArgs} 47 + if [ -f ".$prg-wrapped" ]; then 48 + echo "#!${pythonExecutable}" > "$out/bin/$prg" 49 + sed -e '1d' -e '3d' ".$prg-wrapped" >> "$out/bin/$prg" 50 + chmod +x "$out/bin/$prg" 51 + else 52 + makeWrapper "$path/bin/$prg" "$out/bin/$prg" --inherit-argv0 --resolve-argv0 ${lib.optionalString (!permitUserSite) ''--set PYTHONNOUSERSITE "true"''} ${lib.concatStringsSep " " makeWrapperArgs} 53 + fi 46 54 fi 47 55 fi 48 56 done