diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 3f943853f..923ceb8ae 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -23,24 +23,24 @@ jobs: py: - "3.13t" - "3.13" - - "3.12" - - "3.11" - - "3.10" - - "3.9" +# - "3.12" +# - "3.11" +# - "3.10" +# - "3.9" - "3.8" - pypy-3.11 - - pypy-3.10 - - pypy-3.9 - - pypy-3.8 - - graalpy-24.1 +# - pypy-3.10 +# - pypy-3.9 +# - pypy-3.8 +# - graalpy-24.1 os: - - ubuntu-24.04 - - macos-15 +# - ubuntu-24.04 +# - macos-15 - windows-2025 - include: - - { os: macos-15, py: "brew@3.11" } - - { os: macos-15, py: "brew@3.10" } - - { os: macos-15, py: "brew@3.9" } +# include: +# - { os: macos-15, py: "brew@3.11" } +# - { os: macos-15, py: "brew@3.10" } +# - { os: macos-15, py: "brew@3.9" } exclude: - { os: windows-2025, py: "graalpy-24.1" } - { os: windows-2025, py: "pypy-3.10" } diff --git a/docs/changelog/2637.feature.rst b/docs/changelog/2637.feature.rst new file mode 100644 index 000000000..d420a7e9d --- /dev/null +++ b/docs/changelog/2637.feature.rst @@ -0,0 +1 @@ +Added support for `pkg-config` - by :user:`esafak`. diff --git a/src/virtualenv/activation/bash/activate.sh b/src/virtualenv/activation/bash/activate.sh index b54358e9e..c0b7c701c 100644 --- a/src/virtualenv/activation/bash/activate.sh +++ b/src/virtualenv/activation/bash/activate.sh @@ -34,6 +34,14 @@ deactivate () { unset _OLD_VIRTUAL_TK_LIBRARY fi + if ! [ -z "${_OLD_VIRTUAL_PKG_CONFIG_PATH+_}" ]; then + PKG_CONFIG_PATH="$_OLD_VIRTUAL_PKG_CONFIG_PATH" + export PKG_CONFIG_PATH + unset _OLD_VIRTUAL_PKG_CONFIG_PATH + else + unset PKG_CONFIG_PATH + fi + # The hash command must be called to get it to forget past # commands. Without forgetting past commands the $PATH changes # we made may not be respected @@ -66,6 +74,14 @@ _OLD_VIRTUAL_PATH="$PATH" PATH="$VIRTUAL_ENV/"__BIN_NAME__":$PATH" export PATH +if ! [ -z "${PKG_CONFIG_PATH+_}" ]; then + _OLD_VIRTUAL_PKG_CONFIG_PATH="$PKG_CONFIG_PATH" + PKG_CONFIG_PATH=__PKG_CONFIG_PATH__${__PATH_SEP__}$PKG_CONFIG_PATH +else + PKG_CONFIG_PATH=__PKG_CONFIG_PATH__ +fi +export PKG_CONFIG_PATH + if [ "x"__VIRTUAL_PROMPT__ != x ] ; then VIRTUAL_ENV_PROMPT=__VIRTUAL_PROMPT__ else diff --git a/src/virtualenv/activation/batch/__init__.py b/src/virtualenv/activation/batch/__init__.py index 3d74ba835..f668bcc38 100644 --- a/src/virtualenv/activation/batch/__init__.py +++ b/src/virtualenv/activation/batch/__init__.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +import re from virtualenv.activation.via_template import ViaTemplateActivator @@ -21,7 +22,23 @@ def quote(string): def instantiate_template(self, replacements, template, creator): # ensure the text has all newlines as \r\n - required by batch - base = super().instantiate_template(replacements, template, creator) + if template == "activate.bat": + # sanitize batch-special chars from key replacements + safe_replacements = replacements.copy() + + # CRITICAL: Escape & in PATH assignments (batch-safe) + safe_replacements["__VIRTUAL_ENV__"] = ( + replacements["__VIRTUAL_ENV__"].replace("&", "^&") + ) + + # Sanitize prompt (remove batch command separators) + safe_replacements["__VIRTUAL_PROMPT__"] = re.sub( + r"[&<>|^]", "", replacements["__VIRTUAL_PROMPT__"] + ) + + base = super().instantiate_template(safe_replacements, template, creator) + else: + base = super().instantiate_template(replacements, template, creator) return base.replace(os.linesep, "\n").replace("\n", os.linesep) diff --git a/src/virtualenv/activation/batch/activate.bat b/src/virtualenv/activation/batch/activate.bat index 62f393c80..8997a192e 100644 --- a/src/virtualenv/activation/batch/activate.bat +++ b/src/virtualenv/activation/batch/activate.bat @@ -39,6 +39,9 @@ @if defined TK_LIBRARY @set "_OLD_VIRTUAL_TK_LIBRARY=%TK_LIBRARY%" @if NOT "__TK_LIBRARY__"=="" @set "TK_LIBRARY=__TK_LIBRARY__" +@if defined PKG_CONFIG_PATH @set "_OLD_VIRTUAL_PKG_CONFIG_PATH=%PKG_CONFIG_PATH%" +@if defined _OLD_VIRTUAL_PKG_CONFIG_PATH (@set PKG_CONFIG_PATH=__PKG_CONFIG_PATH__;%_OLD_VIRTUAL_PKG_CONFIG_PATH%) else (@set PKG_CONFIG_PATH=__PKG_CONFIG_PATH__) + @REM if defined _OLD_VIRTUAL_PATH ( @if not defined _OLD_VIRTUAL_PATH @goto ENDIFVPATH1 @set "PATH=%_OLD_VIRTUAL_PATH%" diff --git a/src/virtualenv/activation/batch/deactivate.bat b/src/virtualenv/activation/batch/deactivate.bat index 7a12d47ed..89d4c29e5 100644 --- a/src/virtualenv/activation/batch/deactivate.bat +++ b/src/virtualenv/activation/batch/deactivate.bat @@ -20,6 +20,10 @@ @if not defined _OLD_VIRTUAL_TK_LIBRARY @set TK_LIBRARY= @set _OLD_VIRTUAL_TK_LIBRARY= +@if defined _OLD_VIRTUAL_PKG_CONFIG_PATH @set "PKG_CONFIG_PATH=%_OLD_VIRTUAL_PKG_CONFIG_PATH%" +@if not defined _OLD_VIRTUAL_PKG_CONFIG_PATH @set PKG_CONFIG_PATH= +@set _OLD_VIRTUAL_PKG_CONFIG_PATH= + @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 5c02616d7..479c42e6f 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_TCL_LIBRARY != 0 && setenv TCL_LIBRARY "$_OLD_VIRTUAL_TCL_LIBRARY:q" && unset _OLD_VIRTUAL_TCL_LIBRARY || unsetenv TCL_LIBRARY; test $?_OLD_VIRTUAL_TK_LIBRARY != 0 && setenv TK_LIBRARY "$_OLD_VIRTUAL_TK_LIBRARY:q" && unset _OLD_VIRTUAL_TK_LIBRARY || unsetenv TK_LIBRARY; 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; rehash; test $?_OLD_VIRTUAL_TCL_LIBRARY != 0 && setenv TCL_LIBRARY "$_OLD_VIRTUAL_TCL_LIBRARY:q" && unset _OLD_VIRTUAL_TCL_LIBRARY || unsetenv TCL_LIBRARY; test $?_OLD_VIRTUAL_TK_LIBRARY != 0 && setenv TK_LIBRARY "$_OLD_VIRTUAL_TK_LIBRARY:q" && unset _OLD_VIRTUAL_TK_LIBRARY || unsetenv TK_LIBRARY; test $?_OLD_VIRTUAL_PKG_CONFIG_PATH != 0 && setenv PKG_CONFIG_PATH "$_OLD_VIRTUAL_PKG_CONFIG_PATH:q" && unset _OLD_VIRTUAL_PKG_CONFIG_PATH || unsetenv PKG_CONFIG_PATH; 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 @@ -29,6 +29,13 @@ if (__TK_LIBRARY__ != "") then setenv TK_LIBRARY __TK_LIBRARY__ endif +if ($?PKG_CONFIG_PATH) then + set _OLD_VIRTUAL_PKG_CONFIG_PATH="$PKG_CONFIG_PATH:q" + setenv PKG_CONFIG_PATH __PKG_CONFIG_PATH__:$PKG_CONFIG_PATH:q +else + setenv PKG_CONFIG_PATH __PKG_CONFIG_PATH__ +endif + if (__VIRTUAL_PROMPT__ != "") then setenv VIRTUAL_ENV_PROMPT __VIRTUAL_PROMPT__ else diff --git a/src/virtualenv/activation/fish/activate.fish b/src/virtualenv/activation/fish/activate.fish index c9d174997..b6bb2bdec 100644 --- a/src/virtualenv/activation/fish/activate.fish +++ b/src/virtualenv/activation/fish/activate.fish @@ -43,6 +43,13 @@ function deactivate -d 'Exit virtualenv mode and return to the normal environmen end 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 + else + set -e PKG_CONFIG_PATH + end + if test -n "$_OLD_VIRTUAL_PYTHONHOME" set -gx PYTHONHOME "$_OLD_VIRTUAL_PYTHONHOME" set -e _OLD_VIRTUAL_PYTHONHOME @@ -98,6 +105,13 @@ if test -n __TK_LIBRARY__ set -gx TK_LIBRARY '__TK_LIBRARY__' end +if set -q PKG_CONFIG_PATH + set -gx _OLD_VIRTUAL_PKG_CONFIG_PATH $PKG_CONFIG_PATH + set -gx PKG_CONFIG_PATH __PKG_CONFIG_PATH__ $PKG_CONFIG_PATH +else + set -gx PKG_CONFIG_PATH __PKG_CONFIG_PATH__ +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 b48fdd03f..6cd93b07d 100644 --- a/src/virtualenv/activation/nushell/activate.nu +++ b/src/virtualenv/activation/nushell/activate.nu @@ -64,6 +64,12 @@ export-env { if (has-env 'TK_LIBRARY') { let $new_env = $new_env | insert TK_LIBRARY __TK_LIBRARY__ } + let $new_env = if (has-env 'PKG_CONFIG_PATH') { + let pkg_config_path = ($env.PKG_CONFIG_PATH | prepend __PKG_CONFIG_PATH__) + $new_env | insert PKG_CONFIG_PATH $pkg_config_path + } else { + $new_env | insert PKG_CONFIG_PATH [__PKG_CONFIG_PATH__] + } let old_prompt_command = if (has-env 'PROMPT_COMMAND') { $env.PROMPT_COMMAND } else { '' } let new_env = if (is-env-true 'VIRTUAL_ENV_DISABLE_PROMPT') { $new_env diff --git a/src/virtualenv/activation/powershell/activate.ps1 b/src/virtualenv/activation/powershell/activate.ps1 index 9f95e4370..cc7c509ae 100644 --- a/src/virtualenv/activation/powershell/activate.ps1 +++ b/src/virtualenv/activation/powershell/activate.ps1 @@ -25,6 +25,16 @@ 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 { + if (Test-Path env:PKG_CONFIG_PATH) { + Remove-Item env:PKG_CONFIG_PATH -ErrorAction SilentlyContinue + } + } + if (Test-Path function:_old_virtual_prompt) { $function:prompt = $function:_old_virtual_prompt Remove-Item function:\_old_virtual_prompt @@ -76,6 +86,14 @@ if (__TK_LIBRARY__ -ne "") { $env:TK_LIBRARY = __TK_LIBRARY__ } +if (Test-Path env:PKG_CONFIG_PATH) { + New-Variable -Scope global -Name _OLD_VIRTUAL_PKG_CONFIG_PATH -Value $env:PKG_CONFIG_PATH + $env:PKG_CONFIG_PATH = __PKG_CONFIG_PATH__ + __PATH_SEP__ + $env:PKG_CONFIG_PATH +} +else { + $env:PKG_CONFIG_PATH = __PKG_CONFIG_PATH__ +} + New-Variable -Scope global -Name _OLD_VIRTUAL_PATH -Value $env:PATH $env:PATH = "$env:VIRTUAL_ENV/" + __BIN_NAME__ + __PATH_SEP__ + $env:PATH diff --git a/src/virtualenv/activation/python/activate_this.py b/src/virtualenv/activation/python/activate_this.py index 9cc816fab..6b55d3683 100644 --- a/src/virtualenv/activation/python/activate_this.py +++ b/src/virtualenv/activation/python/activate_this.py @@ -24,6 +24,14 @@ # prepend bin to PATH (this file is inside the bin directory) os.environ["PATH"] = os.pathsep.join([bin_dir, *os.environ.get("PATH", "").split(os.pathsep)]) + +# set PKG_CONFIG_PATH +pkg_config_path = os.path.join(base, "lib", "pkgconfig") +if "PKG_CONFIG_PATH" in os.environ: + os.environ["PKG_CONFIG_PATH"] = os.pathsep.join([pkg_config_path, os.environ["PKG_CONFIG_PATH"]]) +else: + os.environ["PKG_CONFIG_PATH"] = pkg_config_path + os.environ["VIRTUAL_ENV"] = base # virtual env is right above bin directory os.environ["VIRTUAL_ENV_PROMPT"] = __VIRTUAL_PROMPT__ or os.path.basename(base) diff --git a/src/virtualenv/activation/via_template.py b/src/virtualenv/activation/via_template.py index 85f932605..2bc89cb97 100644 --- a/src/virtualenv/activation/via_template.py +++ b/src/virtualenv/activation/via_template.py @@ -49,6 +49,7 @@ def replacements(self, creator, dest_folder): # noqa: ARG002 "__PATH_SEP__": os.pathsep, "__TCL_LIBRARY__": getattr(creator.interpreter, "tcl_lib", None) or "", "__TK_LIBRARY__": getattr(creator.interpreter, "tk_lib", None) or "", + "__PKG_CONFIG_PATH__": str(creator.dest / "lib" / "pkgconfig"), } def _generate(self, replacements, templates, to_folder, creator): diff --git a/tests/unit/activation/test_bash.py b/tests/unit/activation/test_bash.py index 0a9c588cf..1bf55608f 100644 --- a/tests/unit/activation/test_bash.py +++ b/tests/unit/activation/test_bash.py @@ -1,5 +1,7 @@ from __future__ import annotations +import os +import shlex from argparse import Namespace import pytest @@ -63,6 +65,49 @@ def __init__(self, dest): assert "export TCL_LIBRARY" in content +@pytest.mark.skipif(IS_WIN, reason="Github Actions ships with WSL bash") +def test_bash_pkg_config_generation(tmp_path): + # GIVEN + class MockCreator: + def __init__(self, dest): + self.dest = dest + self.bin_dir = dest / "bin" + self.bin_dir.mkdir() + self.pyenv_cfg = {} + self.env_name = "my-env" + + class MockInterpreter: + pass + + self.interpreter = MockInterpreter() + self.interpreter.tcl_lib = None + self.interpreter.tk_lib = None + + creator = MockCreator(tmp_path) + options = Namespace(prompt=None) + activator = BashActivator(options) + + # WHEN + activator.generate(creator) + content = (creator.bin_dir / "activate").read_text(encoding="utf-8") + + # THEN + pkg_config_path = str(tmp_path / "lib" / "pkgconfig") + content = content.replace(shlex.quote(pkg_config_path), "__PKG_CONFIG_PATH__") + content = content.replace(shlex.quote(os.pathsep), "__PATH_SEP__") + + assert 'if ! [ -z "${PKG_CONFIG_PATH+_}" ]; then' in content + assert '_OLD_VIRTUAL_PKG_CONFIG_PATH="$PKG_CONFIG_PATH"' in content + assert "PKG_CONFIG_PATH=__PKG_CONFIG_PATH__${__PATH_SEP__}$PKG_CONFIG_PATH" in content + assert "else" in content + assert "PKG_CONFIG_PATH=__PKG_CONFIG_PATH__" in content + assert "export PKG_CONFIG_PATH" in content + assert 'if ! [ -z "${_OLD_VIRTUAL_PKG_CONFIG_PATH+_}" ]; then' in content + assert 'PKG_CONFIG_PATH="$_OLD_VIRTUAL_PKG_CONFIG_PATH"' in content + assert "unset _OLD_VIRTUAL_PKG_CONFIG_PATH" in content + assert "unset PKG_CONFIG_PATH" in content + + @pytest.mark.skipif(IS_WIN, reason="Github Actions ships with WSL bash") @pytest.mark.parametrize("hashing_enabled", [True, False]) def test_bash(raise_on_non_source_class, hashing_enabled, activation_tester): diff --git a/tests/unit/activation/test_batch.py b/tests/unit/activation/test_batch.py index db2595860..617865e63 100644 --- a/tests/unit/activation/test_batch.py +++ b/tests/unit/activation/test_batch.py @@ -52,6 +52,48 @@ def __init__(self, dest): assert '@if NOT ""=="" @set "TK_LIBRARY="' in activate_content +def test_batch_pkg_config_generation(tmp_path): + # GIVEN + class MockInterpreter: + os = "nt" + + interpreter = MockInterpreter() + interpreter.tcl_lib = None + interpreter.tk_lib = None + + class MockCreator: + def __init__(self, dest): + self.dest = dest + self.bin_dir = dest / "bin" + self.bin_dir.mkdir() + self.interpreter = interpreter + self.pyenv_cfg = {} + self.env_name = "my-env" + + creator = MockCreator(tmp_path) + options = Namespace(prompt=None) + activator = BatchActivator(options) + + # WHEN + activator.generate(creator) + activate_content = (creator.bin_dir / "activate.bat").read_text(encoding="utf-8") + deactivate_content = (creator.bin_dir / "deactivate.bat").read_text(encoding="utf-8") + + # THEN + pkg_config_path = str(tmp_path / "lib" / "pkgconfig") + activate_content = activate_content.replace(BatchActivator.quote(pkg_config_path), "__PKG_CONFIG_PATH__") + + assert '@if defined PKG_CONFIG_PATH @set "_OLD_VIRTUAL_PKG_CONFIG_PATH=%PKG_CONFIG_PATH%"' in activate_content + assert "(@set PKG_CONFIG_PATH=__PKG_CONFIG_PATH__;%_OLD_VIRTUAL_PKG_CONFIG_PATH%)" in activate_content + assert "else (@set PKG_CONFIG_PATH=__PKG_CONFIG_PATH__)" in activate_content + assert ( + '@if defined _OLD_VIRTUAL_PKG_CONFIG_PATH @set "PKG_CONFIG_PATH=%_OLD_VIRTUAL_PKG_CONFIG_PATH%"' + in deactivate_content + ) + assert "@if not defined _OLD_VIRTUAL_PKG_CONFIG_PATH @set PKG_CONFIG_PATH=" in deactivate_content + assert "@set _OLD_VIRTUAL_PKG_CONFIG_PATH=" in deactivate_content + + @pytest.mark.usefixtures("activation_python") def test_batch(activation_tester_class, activation_tester, tmp_path): version_script = tmp_path / "version.bat" diff --git a/tests/unit/activation/test_csh.py b/tests/unit/activation/test_csh.py index 5cea684ec..af43b4857 100644 --- a/tests/unit/activation/test_csh.py +++ b/tests/unit/activation/test_csh.py @@ -53,6 +53,47 @@ def __init__(self, dest): assert "setenv TCL_LIBRARY ''" in content +def test_cshell_pkg_config_generation(tmp_path): + # GIVEN + class MockInterpreter: + pass + + interpreter = MockInterpreter() + interpreter.tcl_lib = None + interpreter.tk_lib = None + + class MockCreator: + def __init__(self, dest): + self.dest = dest + self.bin_dir = dest / "bin" + self.bin_dir.mkdir() + self.interpreter = interpreter + self.pyenv_cfg = {} + self.env_name = "my-env" + + creator = MockCreator(tmp_path) + options = Namespace(prompt=None) + activator = CShellActivator(options) + + # WHEN + activator.generate(creator) + content = (creator.bin_dir / "activate.csh").read_text(encoding="utf-8") + + # THEN + pkg_config_path = str(tmp_path / "lib" / "pkgconfig") + content = content.replace(CShellActivator.quote(pkg_config_path), "__PKG_CONFIG_PATH__") + + assert "if ($?PKG_CONFIG_PATH) then" in content + assert 'set _OLD_VIRTUAL_PKG_CONFIG_PATH="$PKG_CONFIG_PATH:q"' in content + assert "setenv PKG_CONFIG_PATH __PKG_CONFIG_PATH__:$PKG_CONFIG_PATH:q" in content + assert "else" in content + assert "setenv PKG_CONFIG_PATH __PKG_CONFIG_PATH__" in content + assert "test $?_OLD_VIRTUAL_PKG_CONFIG_PATH != 0" in content + assert 'setenv PKG_CONFIG_PATH "$_OLD_VIRTUAL_PKG_CONFIG_PATH:q"' in content + assert "unset _OLD_VIRTUAL_PKG_CONFIG_PATH" in content + assert "unsetenv PKG_CONFIG_PATH" in content + + def test_csh(activation_tester_class, activation_tester): exe = f"tcsh{'.exe' if sys.platform == 'win32' else ''}" if which(exe): diff --git a/tests/unit/activation/test_fish.py b/tests/unit/activation/test_fish.py index c15a8f513..c1010d2b4 100644 --- a/tests/unit/activation/test_fish.py +++ b/tests/unit/activation/test_fish.py @@ -52,6 +52,47 @@ def __init__(self, dest): assert "if test -n ''\n if set -q TK_LIBRARY;" in content +def test_fish_pkg_config_generation(tmp_path): + # GIVEN + class MockInterpreter: + pass + + interpreter = MockInterpreter() + interpreter.tcl_lib = None + interpreter.tk_lib = None + + class MockCreator: + def __init__(self, dest): + self.dest = dest + self.bin_dir = dest / "bin" + self.bin_dir.mkdir() + self.interpreter = interpreter + self.pyenv_cfg = {} + self.env_name = "my-env" + + creator = MockCreator(tmp_path) + options = Namespace(prompt=None) + activator = FishActivator(options) + + # WHEN + activator.generate(creator) + content = (creator.bin_dir / "activate.fish").read_text(encoding="utf-8") + + # THEN + pkg_config_path = str(tmp_path / "lib" / "pkgconfig") + content = content.replace(FishActivator.quote(pkg_config_path), "__PKG_CONFIG_PATH__") + + assert "if set -q PKG_CONFIG_PATH" in content + assert "set -gx _OLD_VIRTUAL_PKG_CONFIG_PATH $PKG_CONFIG_PATH" in content + assert "set -gx PKG_CONFIG_PATH __PKG_CONFIG_PATH__ $PKG_CONFIG_PATH" in content + assert "else" in content + assert "set -gx PKG_CONFIG_PATH __PKG_CONFIG_PATH__" in content + assert 'if test -n "$_OLD_VIRTUAL_PKG_CONFIG_PATH"' in content + assert 'set -gx PKG_CONFIG_PATH "$_OLD_VIRTUAL_PKG_CONFIG_PATH"' in content + assert "set -e _OLD_VIRTUAL_PKG_CONFIG_PATH" in content + assert "set -e PKG_CONFIG_PATH" in content + + @pytest.mark.skipif(IS_WIN, reason="we have not setup fish in CI yet") def test_fish(activation_tester_class, activation_tester, monkeypatch, tmp_path): monkeypatch.setenv("HOME", str(tmp_path)) diff --git a/tests/unit/activation/test_nushell.py b/tests/unit/activation/test_nushell.py index 08c5cb1a1..21dd16cc6 100644 --- a/tests/unit/activation/test_nushell.py +++ b/tests/unit/activation/test_nushell.py @@ -43,6 +43,40 @@ def __init__(self, dest): assert expected_tk in content +def test_nushell_pkg_config_generation(tmp_path): + # GIVEN + class MockInterpreter: + pass + + interpreter = MockInterpreter() + interpreter.tcl_lib = None + interpreter.tk_lib = None + + class MockCreator: + def __init__(self, dest): + self.dest = dest + self.bin_dir = dest / "bin" + self.bin_dir.mkdir() + self.interpreter = interpreter + self.pyenv_cfg = {} + self.env_name = "my-env" + + creator = MockCreator(tmp_path) + options = Namespace(prompt=None) + activator = NushellActivator(options) + + # WHEN + activator.generate(creator) + content = (creator.bin_dir / "activate.nu").read_text(encoding="utf-8") + + # THEN + assert "let $new_env = if (has-env 'PKG_CONFIG_PATH') {" in content + assert "let pkg_config_path = ($env.PKG_CONFIG_PATH | prepend __PKG_CONFIG_PATH__)" in content + assert "$new_env | insert PKG_CONFIG_PATH $pkg_config_path" in content + assert "else {" in content + assert "$new_env | insert PKG_CONFIG_PATH [__PKG_CONFIG_PATH__]" in content + + def test_nushell(activation_tester_class, activation_tester): class Nushell(activation_tester_class): def __init__(self, session) -> None: diff --git a/tests/unit/activation/test_powershell.py b/tests/unit/activation/test_powershell.py index 2a48956cf..995834494 100644 --- a/tests/unit/activation/test_powershell.py +++ b/tests/unit/activation/test_powershell.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os import sys from argparse import Namespace @@ -54,6 +55,48 @@ def __init__(self, dest): assert "$env:TCL_LIBRARY = ''" in content +def test_powershell_pkg_config_generation(tmp_path): + # GIVEN + class MockInterpreter: + os = "nt" + + interpreter = MockInterpreter() + interpreter.tcl_lib = None + interpreter.tk_lib = None + + class MockCreator: + def __init__(self, dest): + self.dest = dest + self.bin_dir = dest / "bin" + self.bin_dir.mkdir() + self.interpreter = interpreter + self.pyenv_cfg = {} + self.env_name = "my-env" + + creator = MockCreator(tmp_path) + options = Namespace(prompt=None) + activator = PowerShellActivator(options) + + # WHEN + activator.generate(creator) + content = (creator.bin_dir / "activate.ps1").read_text(encoding="utf-8-sig") + + # THEN + pkg_config_path = str(tmp_path / "lib" / "pkgconfig") + content = content.replace(PowerShellActivator.quote(pkg_config_path), "__PKG_CONFIG_PATH__") + content = content.replace(PowerShellActivator.quote(os.pathsep), "__PATH_SEP__") + + assert "if (Test-Path env:PKG_CONFIG_PATH)" in content + assert "New-Variable -Scope global -Name _OLD_VIRTUAL_PKG_CONFIG_PATH -Value $env:PKG_CONFIG_PATH" in content + assert "$env:PKG_CONFIG_PATH = __PKG_CONFIG_PATH__ + __PATH_SEP__ + $env:PKG_CONFIG_PATH" in content + assert "else" in content + assert "$env:PKG_CONFIG_PATH = __PKG_CONFIG_PATH__" in content + assert "if (Test-Path variable:_OLD_VIRTUAL_PKG_CONFIG_PATH)" in content + assert "$env:PKG_CONFIG_PATH = $variable:_OLD_VIRTUAL_PKG_CONFIG_PATH" in content + assert 'Remove-Variable "_OLD_VIRTUAL_PKG_CONFIG_PATH" -Scope global' in content + assert "Remove-Item env:PKG_CONFIG_PATH -ErrorAction SilentlyContinue" in content + + @pytest.mark.slow def test_powershell(activation_tester_class, activation_tester, monkeypatch): monkeypatch.setenv("TERM", "xterm") diff --git a/tests/unit/activation/test_python_activator.py b/tests/unit/activation/test_python_activator.py index 24a3561c5..13fb3f545 100644 --- a/tests/unit/activation/test_python_activator.py +++ b/tests/unit/activation/test_python_activator.py @@ -5,6 +5,8 @@ from ast import literal_eval from textwrap import dedent +import pytest + from virtualenv.activation import PythonActivator from virtualenv.info import IS_WIN @@ -59,7 +61,7 @@ def print_r(value): """ return dedent(raw).splitlines() - def assert_output(self, out, raw, tmp_path): # noqa: ARG002 + def assert_output(self, out, _raw, _tmp_path): 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 @@ -90,3 +92,65 @@ def non_source_activate(self, activate_script): return [*self._invoke_script, "-c", f"exec(open({act!r}).read())"] activation_tester(Python) + + +@pytest.mark.parametrize("pkg_config_path", [None, "/data/foo"]) +def test_python_pkg_config(raise_on_non_source_class, activation_tester, pkg_config_path, monkeypatch): + if pkg_config_path: + monkeypatch.setenv("PKG_CONFIG_PATH", pkg_config_path) + + class Python(raise_on_non_source_class): + def __init__(self, session) -> None: + super().__init__( + PythonActivator, + session, + sys.executable, + activate_script="activate_this.py", + extension="py", + non_source_fail_message="You must use import runpy; runpy.run_path(this_file)", + ) + self.unix_line_ending = not IS_WIN + + def env(self, tmp_path): + env = super().env(tmp_path) + if pkg_config_path is None and "PKG_CONFIG_PATH" in env: + del env["PKG_CONFIG_PATH"] + return env + + @staticmethod + def _get_test_lines(activate_script): + raw = f""" + import os + import sys + import runpy + + def print_r(value): + print(repr(value)) + + print_r(os.environ.get("PKG_CONFIG_PATH")) + file_at = {str(activate_script)!r} + runpy.run_path(file_at) + print_r(os.environ.get("PKG_CONFIG_PATH")) + """ + return dedent(raw).splitlines() + + def assert_output(self, out, _raw, _tmp_path): + out = [literal_eval(i) for i in out] + + # Before activation + assert out[0] == pkg_config_path + + # After activation + venv_pkg_config = str(self._creator.dest / "lib" / "pkgconfig") + if pkg_config_path is None: + expected = venv_pkg_config + else: + expected = os.pathsep.join([venv_pkg_config, pkg_config_path]) + + assert out[1] == expected + + def non_source_activate(self, activate_script): + act = str(activate_script) + return [*self._invoke_script, "-c", f"exec(open({act!r}).read())"] + + activation_tester(Python)