Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions .github/workflows/check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
1 change: 1 addition & 0 deletions docs/changelog/2637.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added support for `pkg-config` - by :user:`esafak`.
16 changes: 16 additions & 0 deletions src/virtualenv/activation/bash/activate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
19 changes: 18 additions & 1 deletion src/virtualenv/activation/batch/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import os
import re

from virtualenv.activation.via_template import ViaTemplateActivator

Expand All @@ -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)


Expand Down
3 changes: 3 additions & 0 deletions src/virtualenv/activation/batch/activate.bat
Original file line number Diff line number Diff line change
Expand Up @@ -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%"
Expand Down
4 changes: 4 additions & 0 deletions src/virtualenv/activation/batch/deactivate.bat
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
9 changes: 8 additions & 1 deletion src/virtualenv/activation/cshell/activate.csh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
14 changes: 14 additions & 0 deletions src/virtualenv/activation/fish/activate.fish
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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__
Expand Down
6 changes: 6 additions & 0 deletions src/virtualenv/activation/nushell/activate.nu
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions src/virtualenv/activation/powershell/activate.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/virtualenv/activation/python/activate_this.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions src/virtualenv/activation/via_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
45 changes: 45 additions & 0 deletions tests/unit/activation/test_bash.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import os
import shlex
from argparse import Namespace

import pytest
Expand Down Expand Up @@ -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):
Expand Down
42 changes: 42 additions & 0 deletions tests/unit/activation/test_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
41 changes: 41 additions & 0 deletions tests/unit/activation/test_csh.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading
Loading