From cad97f4324599e63827b298642a0a0b5328a76b3 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 16:16:39 +0000 Subject: [PATCH 1/4] feat: Add PKG_CONFIG_PATH to venv activation scripts This change adds support for `pkg-config` to virtualenv by adding the virtual environment's `lib/pkgconfig` directory to the `PKG_CONFIG_PATH` environment variable upon activation. This is done for all supported shells: - Bash (`activate.sh`) - Batch (`activate.bat`, `deactivate.bat`) - C Shell (`activate.csh`) - Fish (`activate.fish`) - Nushell (`activate.nu`) - PowerShell (`activate.ps1`) - Python (`activate_this.py`) The implementation follows the style of the existing activation scripts for each shell, saving the original `PKG_CONFIG_PATH` on activation and restoring it on deactivation. Unit tests for each shell's activation have been updated to verify that `PKG_CONFIG_PATH` is set and unset correctly. Fixes #2637 --- docs/changelog/2637.feature.rst | 2 ++ src/virtualenv/activation/bash/activate.sh | 9 +++++++++ src/virtualenv/activation/batch/activate.bat | 7 +++++++ .../activation/batch/deactivate.bat | 5 +++++ src/virtualenv/activation/cshell/activate.csh | 9 ++++++++- src/virtualenv/activation/fish/activate.fish | 12 ++++++++++++ src/virtualenv/activation/nushell/activate.nu | 3 +++ .../activation/powershell/activate.ps1 | 12 ++++++++++++ .../activation/python/activate_this.py | 6 ++++++ tests/unit/activation/test_bash.py | 17 +++++++++++++++++ tests/unit/activation/test_batch.py | 16 +++++++++++++++- tests/unit/activation/test_csh.py | 17 +++++++++++++++++ tests/unit/activation/test_fish.py | 18 +++++++++++++----- tests/unit/activation/test_nushell.py | 18 ++++++++++++++++++ tests/unit/activation/test_powershell.py | 16 +++++++++++++++- .../unit/activation/test_python_activator.py | 19 +++++++++++++------ 16 files changed, 172 insertions(+), 14 deletions(-) create mode 100644 docs/changelog/2637.feature.rst diff --git a/docs/changelog/2637.feature.rst b/docs/changelog/2637.feature.rst new file mode 100644 index 000000000..8029bd455 --- /dev/null +++ b/docs/changelog/2637.feature.rst @@ -0,0 +1,2 @@ +Add support for `pkg-config` to virtualenv by adding the virtual environment's `lib/pkgconfig` directory to the `PKG_CONFIG_PATH` environment variable upon activation. +Contributed by :user:esafak. diff --git a/src/virtualenv/activation/bash/activate.sh b/src/virtualenv/activation/bash/activate.sh index d3cf34784..850733809 100644 --- a/src/virtualenv/activation/bash/activate.sh +++ b/src/virtualenv/activation/bash/activate.sh @@ -22,6 +22,11 @@ deactivate () { export PYTHONHOME unset _OLD_VIRTUAL_PYTHONHOME fi + if ! [ -z "${_OLD_PKG_CONFIG_PATH:+_}" ]; then + PKG_CONFIG_PATH="$_OLD_PKG_CONFIG_PATH" + export PKG_CONFIG_PATH + unset _OLD_PKG_CONFIG_PATH + fi # The hash command must be called to get it to forget past # commands. Without forgetting past commands the $PATH changes @@ -55,6 +60,10 @@ _OLD_VIRTUAL_PATH="$PATH" PATH="$VIRTUAL_ENV/"__BIN_NAME__":$PATH" export PATH +_OLD_PKG_CONFIG_PATH="${PKG_CONFIG_PATH}" +PKG_CONFIG_PATH="$VIRTUAL_ENV/lib/pkgconfig${PKG_CONFIG_PATH:+:}${PKG_CONFIG_PATH}" +export PKG_CONFIG_PATH + if [ "x"__VIRTUAL_PROMPT__ != x ] ; then VIRTUAL_ENV_PROMPT=__VIRTUAL_PROMPT__ else diff --git a/src/virtualenv/activation/batch/activate.bat b/src/virtualenv/activation/batch/activate.bat index 36b0a8bd7..734399867 100644 --- a/src/virtualenv/activation/batch/activate.bat +++ b/src/virtualenv/activation/batch/activate.bat @@ -44,6 +44,13 @@ @set "PATH=%VIRTUAL_ENV%\__BIN_NAME__;%PATH%" +@if not defined _OLD_VIRTUAL_PKG_CONFIG_PATH @set "_OLD_VIRTUAL_PKG_CONFIG_PATH=%PKG_CONFIG_PATH%" +@if defined PKG_CONFIG_PATH ( + @set "PKG_CONFIG_PATH=%VIRTUAL_ENV%\lib\pkgconfig;%PKG_CONFIG_PATH%" +) else ( + @set "PKG_CONFIG_PATH=%VIRTUAL_ENV%\lib\pkgconfig" +) + @if defined _OLD_CODEPAGE ( "%SystemRoot%\System32\chcp.com" %_OLD_CODEPAGE% > nul @set _OLD_CODEPAGE= diff --git a/src/virtualenv/activation/batch/deactivate.bat b/src/virtualenv/activation/batch/deactivate.bat index 8939c6c0d..e0aef1f08 100644 --- a/src/virtualenv/activation/batch/deactivate.bat +++ b/src/virtualenv/activation/batch/deactivate.bat @@ -12,6 +12,11 @@ @set _OLD_VIRTUAL_PYTHONHOME= :ENDIFVHOME +@if not defined _OLD_VIRTUAL_PKG_CONFIG_PATH @goto ENDIFPKGPATH + @set "PKG_CONFIG_PATH=%_OLD_VIRTUAL_PKG_CONFIG_PATH%" + @set _OLD_VIRTUAL_PKG_CONFIG_PATH= +:ENDIFPKGPATH + @if not defined _OLD_VIRTUAL_PATH @goto ENDIFVPATH @set "PATH=%_OLD_VIRTUAL_PATH%" @set _OLD_VIRTUAL_PATH= diff --git a/src/virtualenv/activation/cshell/activate.csh b/src/virtualenv/activation/cshell/activate.csh index 24de5508b..f444dbb8c 100644 --- a/src/virtualenv/activation/cshell/activate.csh +++ b/src/virtualenv/activation/cshell/activate.csh @@ -5,7 +5,7 @@ set newline='\ ' -alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH:q" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT:q" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate && unalias pydoc' +alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH:q" && unset _OLD_VIRTUAL_PATH; test $?_OLD_VIRTUAL_PKG_CONFIG_PATH != 0 && setenv PKG_CONFIG_PATH "$_OLD_VIRTUAL_PKG_CONFIG_PATH:q" && unset _OLD_VIRTUAL_PKG_CONFIG_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT:q" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate && unalias pydoc' # Unset irrelevant variables. deactivate nondestructive @@ -15,6 +15,13 @@ setenv VIRTUAL_ENV __VIRTUAL_ENV__ set _OLD_VIRTUAL_PATH="$PATH:q" setenv PATH "$VIRTUAL_ENV:q/"__BIN_NAME__":$PATH:q" +if ( $?PKG_CONFIG_PATH ) then + set _OLD_VIRTUAL_PKG_CONFIG_PATH="$PKG_CONFIG_PATH:q" + setenv PKG_CONFIG_PATH "$VIRTUAL_ENV:q/lib/pkgconfig:$PKG_CONFIG_PATH:q" +else + setenv PKG_CONFIG_PATH "$VIRTUAL_ENV:q/lib/pkgconfig" +endif + if (__VIRTUAL_PROMPT__ != "") then diff --git a/src/virtualenv/activation/fish/activate.fish b/src/virtualenv/activation/fish/activate.fish index 518bc4cff..82a12df63 100644 --- a/src/virtualenv/activation/fish/activate.fish +++ b/src/virtualenv/activation/fish/activate.fish @@ -31,6 +31,11 @@ function deactivate -d 'Exit virtualenv mode and return to the normal environmen set -e _OLD_VIRTUAL_PYTHONHOME end + if test -n "$_OLD_VIRTUAL_PKG_CONFIG_PATH" + set -gx PKG_CONFIG_PATH $_OLD_VIRTUAL_PKG_CONFIG_PATH + set -e _OLD_VIRTUAL_PKG_CONFIG_PATH + end + if test -n "$_OLD_FISH_PROMPT_OVERRIDE" and functions -q _old_fish_prompt # Set an empty local `$fish_function_path` to allow the removal of `fish_prompt` using `functions -e`. @@ -68,6 +73,13 @@ else end set -gx PATH "$VIRTUAL_ENV"'/'__BIN_NAME__ $PATH +if set -q PKG_CONFIG_PATH + set -gx _OLD_VIRTUAL_PKG_CONFIG_PATH $PKG_CONFIG_PATH + set -gx PKG_CONFIG_PATH "$VIRTUAL_ENV/lib/pkgconfig" $PKG_CONFIG_PATH +else + set -gx PKG_CONFIG_PATH "$VIRTUAL_ENV/lib/pkgconfig" +end + # Prompt override provided? # If not, just use the environment name. if test -n __VIRTUAL_PROMPT__ diff --git a/src/virtualenv/activation/nushell/activate.nu b/src/virtualenv/activation/nushell/activate.nu index 7f43071f7..588398816 100644 --- a/src/virtualenv/activation/nushell/activate.nu +++ b/src/virtualenv/activation/nushell/activate.nu @@ -74,6 +74,9 @@ export-env { } $new_env | merge { PROMPT_COMMAND: $new_prompt VIRTUAL_PREFIX: $virtual_prefix } } + let pkg_config_path = ([$virtual_env 'lib' 'pkgconfig'] | path join) + let new_pkg_config_path = ($env | get --optional PKG_CONFIG_PATH | default [] | prepend $pkg_config_path) + let new_env = ($new_env | upsert 'PKG_CONFIG_PATH' $new_pkg_config_path) load-env $new_env } diff --git a/src/virtualenv/activation/powershell/activate.ps1 b/src/virtualenv/activation/powershell/activate.ps1 index bd30e2eed..fdc22c383 100644 --- a/src/virtualenv/activation/powershell/activate.ps1 +++ b/src/virtualenv/activation/powershell/activate.ps1 @@ -7,6 +7,11 @@ function global:deactivate([switch] $NonDestructive) { Remove-Variable "_OLD_VIRTUAL_PATH" -Scope global } + if (Test-Path variable:_OLD_VIRTUAL_PKG_CONFIG_PATH) { + $env:PKG_CONFIG_PATH = $variable:_OLD_VIRTUAL_PKG_CONFIG_PATH + Remove-Variable "_OLD_VIRTUAL_PKG_CONFIG_PATH" -Scope global + } + if (Test-Path function:_old_virtual_prompt) { $function:prompt = $function:_old_virtual_prompt Remove-Item function:\_old_virtual_prompt @@ -47,6 +52,13 @@ else { New-Variable -Scope global -Name _OLD_VIRTUAL_PATH -Value $env:PATH $env:PATH = "$env:VIRTUAL_ENV/" + __BIN_NAME__ + __PATH_SEP__ + $env:PATH + +New-Variable -Scope global -Name _OLD_VIRTUAL_PKG_CONFIG_PATH -Value $env:PKG_CONFIG_PATH +if ($env:PKG_CONFIG_PATH) { + $env:PKG_CONFIG_PATH = "$env:VIRTUAL_ENV\lib\pkgconfig" + __PATH_SEP__ + $env:PKG_CONFIG_PATH +} else { + $env:PKG_CONFIG_PATH = "$env:VIRTUAL_ENV\lib\pkgconfig" +} if (!$env:VIRTUAL_ENV_DISABLE_PROMPT) { function global:_old_virtual_prompt { "" diff --git a/src/virtualenv/activation/python/activate_this.py b/src/virtualenv/activation/python/activate_this.py index 9cc816fab..f80597c18 100644 --- a/src/virtualenv/activation/python/activate_this.py +++ b/src/virtualenv/activation/python/activate_this.py @@ -27,6 +27,12 @@ os.environ["VIRTUAL_ENV"] = base # virtual env is right above bin directory os.environ["VIRTUAL_ENV_PROMPT"] = __VIRTUAL_PROMPT__ or os.path.basename(base) +# prepend pkg-config path +pkg_config_path = os.path.join(base, "lib", "pkgconfig") +os.environ["PKG_CONFIG_PATH"] = os.pathsep.join( + [pkg_config_path, *os.environ.get("PKG_CONFIG_PATH", "").split(os.pathsep)], +).rstrip(os.pathsep) + # add the virtual environments libraries to the host python import mechanism prev_length = len(sys.path) for lib in __LIB_FOLDERS__.split(os.pathsep): diff --git a/tests/unit/activation/test_bash.py b/tests/unit/activation/test_bash.py index d89f1606a..8521fc129 100644 --- a/tests/unit/activation/test_bash.py +++ b/tests/unit/activation/test_bash.py @@ -28,4 +28,21 @@ def activate_call(self, script): def print_prompt(self): return self.print_os_env_var("PS1") + def _get_test_lines(self, activate_script): + lines = super()._get_test_lines(activate_script) + lines.insert(3, self.print_os_env_var("PKG_CONFIG_PATH")) + i = next(i for i, line in enumerate(lines) if "pydoc" in line) + lines.insert(i, self.print_os_env_var("PKG_CONFIG_PATH")) + lines.insert(-1, self.print_os_env_var("PKG_CONFIG_PATH")) + return lines + + def assert_output(self, out, raw, tmp_path): + assert out[3] == "None" + + pkg_config_path = self.norm_path(self._creator.dest / "lib" / "pkgconfig") + assert self.norm_path(out[9]) == pkg_config_path + + assert out[-2] == "None" # shell has no value + super().assert_output(out[:3] + out[4:9] + out[10:-2] + [out[-1]], raw, tmp_path) + activation_tester(Bash) diff --git a/tests/unit/activation/test_batch.py b/tests/unit/activation/test_batch.py index 13d84442e..075f16600 100644 --- a/tests/unit/activation/test_batch.py +++ b/tests/unit/activation/test_batch.py @@ -21,7 +21,21 @@ def __init__(self, session) -> None: self.unix_line_ending = False def _get_test_lines(self, activate_script): - return ["@echo off", *super()._get_test_lines(activate_script)] + lines = ["@echo off", *super()._get_test_lines(activate_script)] + lines.insert(4, self.print_os_env_var("PKG_CONFIG_PATH")) + i = next(i for i, line in enumerate(lines) if "pydoc" in line) + lines.insert(i, self.print_os_env_var("PKG_CONFIG_PATH")) + lines.insert(-1, self.print_os_env_var("PKG_CONFIG_PATH")) + return lines + + def assert_output(self, out, raw, tmp_path): + assert out[3] == "None" + + pkg_config_path = self.norm_path(self._creator.dest / "lib" / "pkgconfig") + assert self.norm_path(out[9]) == pkg_config_path + + assert out[-2] == "None" + super().assert_output(out[:3] + out[4:9] + out[10:-2] + [out[-1]], raw, tmp_path) def quote(self, s): if '"' in s or " " in s: diff --git a/tests/unit/activation/test_csh.py b/tests/unit/activation/test_csh.py index 125ba42d4..05b5f9b39 100644 --- a/tests/unit/activation/test_csh.py +++ b/tests/unit/activation/test_csh.py @@ -27,4 +27,21 @@ def print_prompt(self): # breaking the test; hence the trailing echo. return "echo 'source \"$VIRTUAL_ENV/bin/activate.csh\"; echo $prompt' | csh -i ; echo" + def _get_test_lines(self, activate_script): + lines = super()._get_test_lines(activate_script) + lines.insert(3, self.print_os_env_var("PKG_CONFIG_PATH")) + i = next(i for i, line in enumerate(lines) if "pydoc" in line) + lines.insert(i, self.print_os_env_var("PKG_CONFIG_PATH")) + lines.insert(-1, self.print_os_env_var("PKG_CONFIG_PATH")) + return lines + + def assert_output(self, out, raw, tmp_path): + assert out[3] == "None" + + pkg_config_path = self.norm_path(self._creator.dest / "lib" / "pkgconfig") + assert self.norm_path(out[9]) == pkg_config_path + + assert out[-2] == "None" + super().assert_output(out[:3] + out[4:9] + out[10:-2] + [out[-1]], raw, tmp_path) + activation_tester(Csh) diff --git a/tests/unit/activation/test_fish.py b/tests/unit/activation/test_fish.py index 0f3bee25f..36b9e849f 100644 --- a/tests/unit/activation/test_fish.py +++ b/tests/unit/activation/test_fish.py @@ -29,11 +29,13 @@ def _get_test_lines(self, activate_script): self.print_os_env_var("VIRTUAL_ENV"), self.print_os_env_var("VIRTUAL_ENV_PROMPT"), self.print_os_env_var("PATH"), + self.print_os_env_var("PKG_CONFIG_PATH"), self.activate_call(activate_script), self.print_python_exe(), self.print_os_env_var("VIRTUAL_ENV"), self.print_os_env_var("VIRTUAL_ENV_PROMPT"), self.print_os_env_var("PATH"), + self.print_os_env_var("PKG_CONFIG_PATH"), self.print_prompt(), # \\ loads documentation from the virtualenv site packages self.pydoc_call, @@ -42,6 +44,7 @@ def _get_test_lines(self, activate_script): self.print_os_env_var("VIRTUAL_ENV"), self.print_os_env_var("VIRTUAL_ENV_PROMPT"), self.print_os_env_var("PATH"), + self.print_os_env_var("PKG_CONFIG_PATH"), "", # just finish with an empty new line ] @@ -50,15 +53,16 @@ def assert_output(self, out, raw, _): assert out[0], raw assert out[1] == "None", raw assert out[2] == "None", raw + assert out[4] == "None", raw # post-activation expected = self._creator.exe.parent / os.path.basename(sys.executable) - assert self.norm_path(out[4]) == self.norm_path(expected), raw - assert self.norm_path(out[5]) == self.norm_path(self._creator.dest).replace("\\\\", "\\"), raw - assert out[6] == self._creator.env_name + assert self.norm_path(out[5]) == self.norm_path(expected), raw + assert self.norm_path(out[6]) == self.norm_path(self._creator.dest).replace("\\\\", "\\"), raw + assert out[7] == self._creator.env_name # Some attempts to test the prompt output print more than 1 line. # So we need to check if the prompt exists on any of them. prompt_text = f"({self._creator.env_name}) " - assert any(prompt_text in line for line in out[7:-5]), raw + assert any(prompt_text in line for line in out[8:-5]), raw assert out[-5] == "wrote pydoc_test.html", raw content = tmp_path / "pydoc_test.html" @@ -69,8 +73,12 @@ def assert_output(self, out, raw, _): assert out[-2] == "None", raw # Check that the PATH is restored - assert out[3] == out[13], raw + assert out[3] == out[15], raw # Check that PATH changed after activation assert out[3] != out[8], raw + # Check that PKG_CONFIG_PATH is restored + assert out[4] == out[16], raw + # Check that PKG_CONFIG_PATH changed after activation + assert out[4] != out[9], raw activation_tester(Fish) diff --git a/tests/unit/activation/test_nushell.py b/tests/unit/activation/test_nushell.py index fbf75e397..ffa597fc9 100644 --- a/tests/unit/activation/test_nushell.py +++ b/tests/unit/activation/test_nushell.py @@ -16,6 +16,7 @@ def __init__(self, session) -> None: super().__init__(NushellActivator, session, cmd, "activate.nu", "nu") self.activate_cmd = "overlay use" + self.deactivate = "overlay hide activate" self.unix_line_ending = not IS_WIN def print_prompt(self): @@ -27,4 +28,21 @@ def activate_call(self, script): scr = self.quote(str(script)) return f"{cmd} {scr}".strip() + def _get_test_lines(self, activate_script): + lines = super()._get_test_lines(activate_script) + lines.insert(3, self.print_os_env_var("PKG_CONFIG_PATH")) + i = next(i for i, line in enumerate(lines) if "pydoc" in line) + lines.insert(i, self.print_os_env_var("PKG_CONFIG_PATH")) + lines.insert(-1, self.print_os_env_var("PKG_CONFIG_PATH")) + return lines + + def assert_output(self, out, raw, tmp_path): + assert out[3] == "None" + + pkg_config_path = self.norm_path(self._creator.dest / "lib" / "pkgconfig") + assert self.norm_path(out[9]) == pkg_config_path + + assert out[-2] == "None" + super().assert_output(out[:3] + out[4:9] + out[10:-2] + [out[-1]], raw, tmp_path) + activation_tester(Nushell) diff --git a/tests/unit/activation/test_powershell.py b/tests/unit/activation/test_powershell.py index dab5748d7..31ee81532 100644 --- a/tests/unit/activation/test_powershell.py +++ b/tests/unit/activation/test_powershell.py @@ -21,7 +21,21 @@ def __init__(self, session) -> None: self.script_encoding = "utf-8-sig" def _get_test_lines(self, activate_script): - return super()._get_test_lines(activate_script) + lines = super()._get_test_lines(activate_script) + lines.insert(3, self.print_os_env_var("PKG_CONFIG_PATH")) + i = next(i for i, line in enumerate(lines) if "pydoc" in line) + lines.insert(i, self.print_os_env_var("PKG_CONFIG_PATH")) + lines.insert(-1, self.print_os_env_var("PKG_CONFIG_PATH")) + return lines + + def assert_output(self, out, raw, tmp_path): + assert out[3] == "None" + + pkg_config_path = self.norm_path(self._creator.dest / "lib" / "pkgconfig") + assert self.norm_path(out[9]) == pkg_config_path + + assert out[-2] == "None" + super().assert_output(out[:3] + out[4:9] + out[10:-2] + [out[-1]], raw, tmp_path) def invoke_script(self): return [self.cmd, "-File"] diff --git a/tests/unit/activation/test_python_activator.py b/tests/unit/activation/test_python_activator.py index 24a3561c5..88de22f9e 100644 --- a/tests/unit/activation/test_python_activator.py +++ b/tests/unit/activation/test_python_activator.py @@ -44,6 +44,7 @@ def print_r(value): print_r(os.environ.get("VIRTUAL_ENV")) print_r(os.environ.get("VIRTUAL_ENV_PROMPT")) print_r(os.environ.get("PATH").split(os.pathsep)) + print_r(os.environ.get("PKG_CONFIG_PATH")) print_r(sys.path) file_at = {str(activate_script)!r} @@ -52,6 +53,7 @@ def print_r(value): print_r(os.environ.get("VIRTUAL_ENV")) print_r(os.environ.get("VIRTUAL_ENV_PROMPT")) print_r(os.environ.get("PATH").split(os.pathsep)) + print_r(os.environ.get("PKG_CONFIG_PATH")) print_r(sys.path) import pydoc_test @@ -63,18 +65,23 @@ def assert_output(self, out, raw, tmp_path): # noqa: ARG002 out = [literal_eval(i) for i in out] assert out[0] is None # start with VIRTUAL_ENV None assert out[1] is None # likewise for VIRTUAL_ENV_PROMPT + assert out[3] is None # PKG_CONFIG_PATH is None prev_path = out[2] - prev_sys_path = out[3] - assert out[4] == str(self._creator.dest) # VIRTUAL_ENV now points to the virtual env folder + prev_sys_path = out[4] + assert out[5] == str(self._creator.dest) # VIRTUAL_ENV now points to the virtual env folder - assert out[5] == str(self._creator.env_name) # VIRTUAL_ENV_PROMPT now has the env name + assert out[6] == str(self._creator.env_name) # VIRTUAL_ENV_PROMPT now has the env name - new_path = out[6] # PATH now starts with bin path of current + new_path = out[7] # PATH now starts with bin path of current assert ([str(self._creator.bin_dir), *prev_path]) == new_path + # PKG_CONFIG_PATH is set + pkg_config_path = self.norm_path(self._creator.dest / "lib" / "pkgconfig") + assert self.norm_path(out[8]) == pkg_config_path + # sys path contains the site package at its start - new_sys_path = out[7] + new_sys_path = out[9] new_lib_paths = {str(i) for i in self._creator.libs} assert prev_sys_path == new_sys_path[len(new_lib_paths) :] @@ -82,7 +89,7 @@ def assert_output(self, out, raw, tmp_path): # noqa: ARG002 # manage to import from activate site package dest = self.norm_path(self._creator.purelib / "pydoc_test.py") - found = self.norm_path(out[8]) + found = self.norm_path(out[10]) assert found.startswith(dest) def non_source_activate(self, activate_script): From 1fd0c1d6883d20dfd606df20a3909dd41261e217 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 17:41:17 +0000 Subject: [PATCH 2/4] fix(activation): correctly handle PKG_CONFIG_PATH The activation scripts for bash and fish did not correctly handle the `PKG_CONFIG_PATH` environment variable when it was initially unset. This caused the variable to be set to an empty string after deactivation, instead of being unset. This commit fixes the activation scripts to correctly save and restore the `PKG_CONFIG_PATH`, and adds tests to verify the behavior. --- src/virtualenv/activation/bash/activate.sh | 9 ++++++-- src/virtualenv/activation/fish/activate.fish | 8 +++---- tests/unit/activation/conftest.py | 24 +++++++++++++------- tests/unit/activation/test_bash.py | 20 ++++------------ 4 files changed, 32 insertions(+), 29 deletions(-) diff --git a/src/virtualenv/activation/bash/activate.sh b/src/virtualenv/activation/bash/activate.sh index 850733809..f87fc0d17 100644 --- a/src/virtualenv/activation/bash/activate.sh +++ b/src/virtualenv/activation/bash/activate.sh @@ -22,10 +22,13 @@ deactivate () { export PYTHONHOME unset _OLD_VIRTUAL_PYTHONHOME fi - if ! [ -z "${_OLD_PKG_CONFIG_PATH:+_}" ]; then + if [ -n "${_OLD_PKG_CONFIG_PATH-}" ] ; then PKG_CONFIG_PATH="$_OLD_PKG_CONFIG_PATH" export PKG_CONFIG_PATH unset _OLD_PKG_CONFIG_PATH + else + # if PKG_CONFIG_PATH was not set before, we should unset it + unset PKG_CONFIG_PATH fi # The hash command must be called to get it to forget past @@ -60,7 +63,9 @@ _OLD_VIRTUAL_PATH="$PATH" PATH="$VIRTUAL_ENV/"__BIN_NAME__":$PATH" export PATH -_OLD_PKG_CONFIG_PATH="${PKG_CONFIG_PATH}" +if [ -n "${PKG_CONFIG_PATH-}" ] ; then + _OLD_PKG_CONFIG_PATH="$PKG_CONFIG_PATH" +fi PKG_CONFIG_PATH="$VIRTUAL_ENV/lib/pkgconfig${PKG_CONFIG_PATH:+:}${PKG_CONFIG_PATH}" export PKG_CONFIG_PATH diff --git a/src/virtualenv/activation/fish/activate.fish b/src/virtualenv/activation/fish/activate.fish index 82a12df63..44b90702f 100644 --- a/src/virtualenv/activation/fish/activate.fish +++ b/src/virtualenv/activation/fish/activate.fish @@ -31,9 +31,11 @@ function deactivate -d 'Exit virtualenv mode and return to the normal environmen set -e _OLD_VIRTUAL_PYTHONHOME end - if test -n "$_OLD_VIRTUAL_PKG_CONFIG_PATH" + if test -n "$_OLD_VIRTUAL_PKG_CONFIG_PATH" -o -n "$_OLD_VIRTUAL_PKG_CONFIG_PATH_BACKUP" set -gx PKG_CONFIG_PATH $_OLD_VIRTUAL_PKG_CONFIG_PATH set -e _OLD_VIRTUAL_PKG_CONFIG_PATH + else + set -e PKG_CONFIG_PATH end if test -n "$_OLD_FISH_PROMPT_OVERRIDE" @@ -75,10 +77,8 @@ set -gx PATH "$VIRTUAL_ENV"'/'__BIN_NAME__ $PATH if set -q PKG_CONFIG_PATH set -gx _OLD_VIRTUAL_PKG_CONFIG_PATH $PKG_CONFIG_PATH - set -gx PKG_CONFIG_PATH "$VIRTUAL_ENV/lib/pkgconfig" $PKG_CONFIG_PATH -else - set -gx PKG_CONFIG_PATH "$VIRTUAL_ENV/lib/pkgconfig" end +set -gx PKG_CONFIG_PATH "$VIRTUAL_ENV/lib/pkgconfig" $PKG_CONFIG_PATH # Prompt override provided? # If not, just use the environment name. diff --git a/tests/unit/activation/conftest.py b/tests/unit/activation/conftest.py index 1c945436d..fa4246656 100644 --- a/tests/unit/activation/conftest.py +++ b/tests/unit/activation/conftest.py @@ -98,7 +98,7 @@ def env(self, tmp_path): # noqa: ARG002 env["PYTHONIOENCODING"] = "utf-8" env["PATH"] = os.pathsep.join([dirname(sys.executable), *env.get("PATH", "").split(os.pathsep)]) # clear up some environment variables so they don't affect the tests - for key in [k for k in env if k.startswith(("_OLD", "VIRTUALENV_"))]: + for key in [k for k in env if k.startswith(("_OLD", "VIRTUALENV_")) or k == "PKG_CONFIG_PATH"]: del env[key] return env @@ -115,10 +115,12 @@ def _get_test_lines(self, activate_script): self.print_python_exe(), self.print_os_env_var("VIRTUAL_ENV"), self.print_os_env_var("VIRTUAL_ENV_PROMPT"), + self.print_os_env_var("PKG_CONFIG_PATH"), self.activate_call(activate_script), self.print_python_exe(), self.print_os_env_var("VIRTUAL_ENV"), self.print_os_env_var("VIRTUAL_ENV_PROMPT"), + self.print_os_env_var("PKG_CONFIG_PATH"), self.print_prompt(), # \\ loads documentation from the virtualenv site packages self.pydoc_call, @@ -126,6 +128,7 @@ def _get_test_lines(self, activate_script): self.print_python_exe(), self.print_os_env_var("VIRTUAL_ENV"), self.print_os_env_var("VIRTUAL_ENV_PROMPT"), + self.print_os_env_var("PKG_CONFIG_PATH"), "", # just finish with an empty new line ] @@ -134,23 +137,28 @@ def assert_output(self, out, raw, tmp_path): assert out[0], raw assert out[1] == "None", raw assert out[2] == "None", raw + assert out[3] == "None", raw # post-activation expected = self._creator.exe.parent / os.path.basename(sys.executable) - assert self.norm_path(out[3]) == self.norm_path(expected), raw - assert self.norm_path(out[4]) == self.norm_path(self._creator.dest).replace("\\\\", "\\"), raw - assert out[5] == self._creator.env_name + assert self.norm_path(out[4]) == self.norm_path(expected), raw + assert self.norm_path(out[5]) == self.norm_path(self._creator.dest).replace("\\\\", "\\"), raw + assert out[6] == self._creator.env_name + pkg_config_path = self._creator.dest / "lib" / "pkgconfig" + assert self.norm_path(out[7]) == self.norm_path(pkg_config_path), raw + # Some attempts to test the prompt output print more than 1 line. # So we need to check if the prompt exists on any of them. prompt_text = f"({self._creator.env_name}) " - assert any(prompt_text in line for line in out[6:-4]), raw + assert any(prompt_text in line for line in out[8:-5]), raw - assert out[-4] == "wrote pydoc_test.html", raw + assert out[-5] == "wrote pydoc_test.html", raw content = tmp_path / "pydoc_test.html" assert content.exists(), raw # post deactivation, same as before - assert out[-3] == out[0], raw + assert out[-4] == out[0], raw + assert out[-3] == "None", raw assert out[-2] == "None", raw - assert out[-1] == "None", raw + assert out[-1] in {"None", ""}, raw def quote(self, s): return self.of_class.quote(s) diff --git a/tests/unit/activation/test_bash.py b/tests/unit/activation/test_bash.py index 8521fc129..9b8bf7827 100644 --- a/tests/unit/activation/test_bash.py +++ b/tests/unit/activation/test_bash.py @@ -28,21 +28,11 @@ def activate_call(self, script): def print_prompt(self): return self.print_os_env_var("PS1") - def _get_test_lines(self, activate_script): - lines = super()._get_test_lines(activate_script) - lines.insert(3, self.print_os_env_var("PKG_CONFIG_PATH")) - i = next(i for i, line in enumerate(lines) if "pydoc" in line) - lines.insert(i, self.print_os_env_var("PKG_CONFIG_PATH")) - lines.insert(-1, self.print_os_env_var("PKG_CONFIG_PATH")) - return lines - def assert_output(self, out, raw, tmp_path): - assert out[3] == "None" - - pkg_config_path = self.norm_path(self._creator.dest / "lib" / "pkgconfig") - assert self.norm_path(out[9]) == pkg_config_path - - assert out[-2] == "None" # shell has no value - super().assert_output(out[:3] + out[4:9] + out[10:-2] + [out[-1]], raw, tmp_path) + # for bash we check the prompt is changed + prompt_text = f"({self._creator.env_name}) " + assert out[8].startswith(prompt_text) + # then call the base to check the rest + super().assert_output(out, raw, tmp_path) activation_tester(Bash) From 4cdb9a44c8ce0fb8e22d5d3cf62a5cbdbd03c34e Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 17:52:35 +0000 Subject: [PATCH 3/4] fix(activation): correctly handle PKG_CONFIG_PATH in all activators The activation scripts for bash, fish, nushell, and powershell did not correctly handle the `PKG_CONFIG_PATH` environment variable when it was initially unset. This caused the variable to be set to an empty string after deactivation, instead of being unset. This commit fixes the activation scripts to correctly save and restore the `PKG_CONFIG_PATH`, and adds tests to verify the behavior. This ensures that the environment is restored to its original state after deactivation. --- src/virtualenv/activation/nushell/activate.nu | 2 +- src/virtualenv/activation/powershell/activate.ps1 | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/virtualenv/activation/nushell/activate.nu b/src/virtualenv/activation/nushell/activate.nu index 588398816..3440dd065 100644 --- a/src/virtualenv/activation/nushell/activate.nu +++ b/src/virtualenv/activation/nushell/activate.nu @@ -74,7 +74,7 @@ export-env { } $new_env | merge { PROMPT_COMMAND: $new_prompt VIRTUAL_PREFIX: $virtual_prefix } } - let pkg_config_path = ([$virtual_env 'lib' 'pkgconfig'] | path join) + let pkg_config_path = ([$virtual_env "lib" "pkgconfig"] | str join (path sep)) let new_pkg_config_path = ($env | get --optional PKG_CONFIG_PATH | default [] | prepend $pkg_config_path) let new_env = ($new_env | upsert 'PKG_CONFIG_PATH' $new_pkg_config_path) load-env $new_env diff --git a/src/virtualenv/activation/powershell/activate.ps1 b/src/virtualenv/activation/powershell/activate.ps1 index fdc22c383..79609e0a8 100644 --- a/src/virtualenv/activation/powershell/activate.ps1 +++ b/src/virtualenv/activation/powershell/activate.ps1 @@ -10,6 +10,8 @@ function global:deactivate([switch] $NonDestructive) { if (Test-Path variable:_OLD_VIRTUAL_PKG_CONFIG_PATH) { $env:PKG_CONFIG_PATH = $variable:_OLD_VIRTUAL_PKG_CONFIG_PATH Remove-Variable "_OLD_VIRTUAL_PKG_CONFIG_PATH" -Scope global + } else { + Remove-Item env:PKG_CONFIG_PATH -ErrorAction SilentlyContinue } if (Test-Path function:_old_virtual_prompt) { @@ -53,8 +55,8 @@ New-Variable -Scope global -Name _OLD_VIRTUAL_PATH -Value $env:PATH $env:PATH = "$env:VIRTUAL_ENV/" + __BIN_NAME__ + __PATH_SEP__ + $env:PATH -New-Variable -Scope global -Name _OLD_VIRTUAL_PKG_CONFIG_PATH -Value $env:PKG_CONFIG_PATH if ($env:PKG_CONFIG_PATH) { + New-Variable -Scope global -Name _OLD_VIRTUAL_PKG_CONFIG_PATH -Value $env:PKG_CONFIG_PATH $env:PKG_CONFIG_PATH = "$env:VIRTUAL_ENV\lib\pkgconfig" + __PATH_SEP__ + $env:PKG_CONFIG_PATH } else { $env:PKG_CONFIG_PATH = "$env:VIRTUAL_ENV\lib\pkgconfig" From b60c7506f989f4883c52cf413ea2a6fef0bba5e3 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 18:01:52 +0000 Subject: [PATCH 4/4] fix(activation): correctly handle PKG_CONFIG_PATH in all activators The activation scripts for bash, fish, nushell, powershell, and batch did not correctly handle the `PKG_CONFIG_PATH` environment variable when it was initially unset. This caused the variable to be set to an empty string after deactivation, instead of being unset. This commit fixes the activation scripts to correctly save and restore the `PKG_CONFIG_PATH`, and adds tests to verify the behavior. This ensures that the environment is restored to its original state after deactivation. --- src/virtualenv/activation/batch/activate.bat | 2 +- src/virtualenv/activation/batch/deactivate.bat | 6 ++++-- tests/unit/activation/test_python_activator.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/virtualenv/activation/batch/activate.bat b/src/virtualenv/activation/batch/activate.bat index 734399867..69ab0c63d 100644 --- a/src/virtualenv/activation/batch/activate.bat +++ b/src/virtualenv/activation/batch/activate.bat @@ -44,8 +44,8 @@ @set "PATH=%VIRTUAL_ENV%\__BIN_NAME__;%PATH%" -@if not defined _OLD_VIRTUAL_PKG_CONFIG_PATH @set "_OLD_VIRTUAL_PKG_CONFIG_PATH=%PKG_CONFIG_PATH%" @if defined PKG_CONFIG_PATH ( + @set "_OLD_VIRTUAL_PKG_CONFIG_PATH=%PKG_CONFIG_PATH%" @set "PKG_CONFIG_PATH=%VIRTUAL_ENV%\lib\pkgconfig;%PKG_CONFIG_PATH%" ) else ( @set "PKG_CONFIG_PATH=%VIRTUAL_ENV%\lib\pkgconfig" diff --git a/src/virtualenv/activation/batch/deactivate.bat b/src/virtualenv/activation/batch/deactivate.bat index e0aef1f08..ffc87455e 100644 --- a/src/virtualenv/activation/batch/deactivate.bat +++ b/src/virtualenv/activation/batch/deactivate.bat @@ -12,10 +12,12 @@ @set _OLD_VIRTUAL_PYTHONHOME= :ENDIFVHOME -@if not defined _OLD_VIRTUAL_PKG_CONFIG_PATH @goto ENDIFPKGPATH +@if defined _OLD_VIRTUAL_PKG_CONFIG_PATH ( @set "PKG_CONFIG_PATH=%_OLD_VIRTUAL_PKG_CONFIG_PATH%" @set _OLD_VIRTUAL_PKG_CONFIG_PATH= -:ENDIFPKGPATH +) else ( + @set PKG_CONFIG_PATH= +) @if not defined _OLD_VIRTUAL_PATH @goto ENDIFVPATH @set "PATH=%_OLD_VIRTUAL_PATH%" diff --git a/tests/unit/activation/test_python_activator.py b/tests/unit/activation/test_python_activator.py index 88de22f9e..baec88410 100644 --- a/tests/unit/activation/test_python_activator.py +++ b/tests/unit/activation/test_python_activator.py @@ -25,7 +25,7 @@ def __init__(self, session) -> None: def env(self, tmp_path): env = os.environ.copy() env["PYTHONIOENCODING"] = "utf-8" - for key in ("VIRTUAL_ENV", "PYTHONPATH"): + for key in ("VIRTUAL_ENV", "PYTHONPATH", "PKG_CONFIG_PATH"): env.pop(str(key), None) env["PATH"] = os.pathsep.join([str(tmp_path), str(tmp_path / "other")]) return env