Add support for recognizing pixi-installed dependencies

This commit adds support for using pixi as a package manager alongside
Homebrew and MacPorts on macOS. When pixi is detected and Homebrew/MacPorts
are not available (or skipped), python-build will automatically use
dependencies from pixi's `python-deps` environment.

Key changes:
- Add `can_use_pixi()` and `pixi_env_prefix()` helper functions to detect
  and configure pixi environments
- Add `use_pixi_*()` functions for yaml, readline, ncurses, openssl, zlib,
  and tcl-tk to link against pixi-provided dependencies
- Support `PYTHON_BUILD_SKIP_PIXI` environment variable to disable pixi
- Support `PYTHON_BUILD_PIXI_ENV` to specify a custom pixi environment name
  (defaults to "python-deps")

The pixi integration follows the same pattern as Homebrew and MacPorts,
checking for package availability via `pixi global list` and using the
environment directory from `pixi info`.
This commit is contained in:
Lysandros Nikolaou 2025-10-20 17:39:24 +02:00
parent 74fea7edc6
commit f434db24cd
No known key found for this signature in database
GPG key ID: 43E92D11D08F5F29
2 changed files with 397 additions and 2 deletions

View file

@ -166,6 +166,60 @@ can_use_macports() {
PYTHON_BUILD_SKIP_MACPORTS=1; return 1
}
can_use_pixi() {
if locked_in; then
locked_in pixi && rc=$? || rc=$?; return $rc
fi
[[ -n "$PYTHON_BUILD_USE_PIXI" && -n "$PYTHON_BUILD_SKIP_PIXI" ]] && {
echo "error: mutually exclusive environment variables PYTHON_BUILD_USE_PIXI and PYTHON_BUILD_SKIP_PIXI are set" >&3
exit 1
}
[[ -n "$PYTHON_BUILD_USE_PIXI" ]] && return 0
[[ -n "$PYTHON_BUILD_SKIP_PIXI" ]] && return 1
# Return cached result if already checked
[[ -n "$PYTHON_BUILD_PIXI_ENVS_DIR" ]] && return 0
# Check if pixi exists and cache the environment directory
local envs_dir
command -v pixi &>/dev/null && \
envs_dir="$(pixi info 2>/dev/null | grep 'Environment dir:' | awk '{print $3}')" && \
[[ -n "$envs_dir" ]] && { export PYTHON_BUILD_PIXI_ENVS_DIR="$envs_dir"; return 0; }
# do not check the same stuff multiple times
PYTHON_BUILD_SKIP_PIXI=1; return 1
}
pixi_env_prefix() {
# Return cached value if available
if [[ -n "${PYTHON_BUILD_PIXI_PREFIX:-}" ]]; then
echo "$PYTHON_BUILD_PIXI_PREFIX"
return 0
fi
# Use PYTHON_BUILD_PIXI_ENV if set, otherwise default to "python-deps"
local env_name="${PYTHON_BUILD_PIXI_ENV:-python-deps}"
# Use cached envs_dir if available, otherwise get it from pixi info
local envs_dir="${PYTHON_BUILD_PIXI_ENVS_DIR:-}"
if [ -z "$envs_dir" ]; then
envs_dir="$(pixi info 2>/dev/null | grep 'Environment dir:' | awk '{print $3}')"
if [ -z "$envs_dir" ]; then
return 1
fi
export PYTHON_BUILD_PIXI_ENVS_DIR="$envs_dir"
fi
local prefix="${envs_dir}/${env_name}"
# Check if the environment exists
if [ -d "$prefix" ]; then
# Cache the prefix for subsequent calls
export PYTHON_BUILD_PIXI_PREFIX="$prefix"
echo "$prefix"
return 0
fi
return 1
}
locked_in() {
if [[ -z "$1" ]]; then
[[ -n $_PYTHON_BUILD_ECOSYSTEM_LOCKED_IN ]]
@ -889,13 +943,28 @@ build_package_standard_build() {
use_macports_zlib || true
fi
fi
if can_use_pixi; then
use_custom_tcltk || use_pixi_tcltk || true
use_pixi_readline || true
use_pixi_ncurses || true
if is_mac -ge 1014; then
# While XCode SDK is "always available",
# still need a fallback in case we are using an alternate compiler
use_xcode_sdk_zlib || use_pixi_zlib || true
else
use_pixi_zlib || true
fi
fi
if can_use_homebrew; then
use_homebrew || true
fi
if can_use_macports; then
use_macports || true
fi
if is_mac -ge 1014 && ! can_use_homebrew && ! can_use_macports; then
if can_use_pixi; then
use_pixi || true
fi
if is_mac -ge 1014 && ! can_use_homebrew && ! can_use_macports && ! can_use_pixi; then
use_xcode_sdk_zlib || true
fi
@ -1530,12 +1599,25 @@ use_macports() {
fi
}
use_pixi() {
can_use_pixi || return 1
if command -v pixi &>/dev/null; then
local prefix="$(pixi_env_prefix)" || return 1
export CPPFLAGS="-I${prefix}/include${CPPFLAGS:+ $CPPFLAGS}"
prepend_ldflags_libs "-L${prefix}/lib -Wl,-rpath,${prefix}/lib"
export PKG_CONFIG_PATH="$prefix/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"
lock_in pixi
fi
}
needs_yaml() {
if ! configured_with_package_dir "python" "yaml.h"; then
if can_use_homebrew; then
use_homebrew_yaml && return 1
elif can_use_macports; then
use_macports_yaml && return 1
elif can_use_pixi; then
use_pixi_yaml && return 1
fi
fi
}
@ -1563,6 +1645,17 @@ use_macports_yaml() {
fi
}
use_pixi_yaml() {
can_use_pixi || return 1
local env_name="${PYTHON_BUILD_PIXI_ENV:-python-deps}"
if pixi global list --environment "$env_name" 2>/dev/null | grep -q "^yaml"; then
echo "python-build: use libyaml from pixi (${env_name})"
lock_in pixi
else
return 1
fi
}
use_freebsd_pkg() {
# check if FreeBSD
if [ "FreeBSD" = "${_PYTHON_BUILD_CACHE_UNAME_S:=$(uname -s)}" ]; then
@ -1606,6 +1699,9 @@ has_broken_mac_readline() {
if can_use_macports; then
use_macports_readline && return 1
fi
if can_use_pixi; then
use_pixi_readline && return 1
fi
return 0
}
@ -1636,6 +1732,19 @@ use_macports_readline() {
fi
}
use_pixi_readline() {
can_use_pixi || return 1
if ! configured_with_package_dir "python" "readline/rlconf.h"; then
local env_name="${PYTHON_BUILD_PIXI_ENV:-python-deps}"
if pixi global list --environment "$env_name" 2>/dev/null | grep -q "^readline"; then
echo "python-build: use readline from pixi (${env_name})"
lock_in pixi
else
return 1
fi
fi
}
use_homebrew_ncurses() {
can_use_homebrew || return 1
local libdir="$(brew --prefix ncurses 2>/dev/null || true)"
@ -1659,6 +1768,17 @@ use_macports_ncurses() {
fi
}
use_pixi_ncurses() {
can_use_pixi || return 1
local env_name="${PYTHON_BUILD_PIXI_ENV:-python-deps}"
if pixi global list --environment "$env_name" 2>/dev/null | grep -q "^ncurses"; then
echo "python-build: use ncurses from pixi (${env_name})"
lock_in pixi
else
return 1
fi
}
prefer_openssl11() {
# Allow overriding the preference of OpenSSL version per definition basis (#1302, #1325, #1326)
PYTHON_BUILD_HOMEBREW_OPENSSL_FORMULA="${PYTHON_BUILD_HOMEBREW_OPENSSL_FORMULA:-openssl@1.1 openssl}"
@ -1695,6 +1815,9 @@ has_broken_mac_openssl() {
if can_use_macports; then
use_macports_openssl && return 1
fi
if can_use_pixi; then
use_pixi_openssl && return 1
fi
fi
return 0
}
@ -1742,6 +1865,26 @@ use_macports_openssl() {
return 1
}
use_pixi_openssl() {
can_use_pixi || return 1
command -v pixi >/dev/null || return 1
local env_name="${PYTHON_BUILD_PIXI_ENV:-python-deps}"
if pixi global list --environment "$env_name" 2>/dev/null | grep -q "^openssl"; then
echo "python-build: use openssl from pixi (${env_name})"
if [[ -n "${PYTHON_BUILD_CONFIGURE_WITH_OPENSSL:-}" ]]; then
# configure script of newer CPython versions support `--with-openssl`
# https://bugs.python.org/issue21541
local prefix="$(pixi_env_prefix)" || return 1
package_option python configure --with-openssl="${prefix}"
fi
lock_in pixi
return 0
fi
return 1
}
build_package_mac_openssl() {
# Install to a subdirectory since we don't want shims for bin/openssl.
OPENSSL_PREFIX_PATH="${PREFIX_PATH}/openssl"
@ -1878,6 +2021,17 @@ use_macports_zlib() {
fi
}
use_pixi_zlib() {
can_use_pixi || return 1
local env_name="${PYTHON_BUILD_PIXI_ENV:-python-deps}"
if pixi global list --environment "$env_name" 2>/dev/null | grep -q "^zlib"; then
echo "python-build: use zlib from pixi (${env_name})"
lock_in pixi
else
return 1
fi
}
use_homebrew_tcltk() {
can_use_homebrew || return 1
local tcltk_formula
@ -1922,6 +2076,44 @@ use_homebrew_tcltk() {
return 1
}
use_pixi_tcltk() {
can_use_pixi || return 1
local env_name="${PYTHON_BUILD_PIXI_ENV:-python-deps}"
# Check if tk is actually installed there
if pixi global list --environment "$env_name" 2>/dev/null | grep -q "^tk"; then
echo "python-build: use tk from pixi (${env_name})"
local prefix="$(pixi_env_prefix)" || return 1
# In Homebrew Tcl/Tk 8.6.13, headers have been moved to the 'tcl-tk' subdir.
local tcltk_includes="$(sh -c 'cd '"$prefix"'/lib; . ./tclConfig.sh; . ./tkConfig.sh; echo "$TCL_INCLUDE_SPEC $TK_INCLUDE_SPEC"')"
# Homebrew Tcl/Tk 9 is built with external libtommath. CPython's build as of 3.14.0 does not detect that and fails to link to tommath symbols
local tcltk_cflags
if sh -c '. '"$prefix"'/lib/tclConfig.sh; echo "$TCL_DEFS"' | grep -qwFe '-DTCL_WITH_EXTERNAL_TOMMATH=1'; then
tcltk_cflags="-DTCL_WITH_EXTERNAL_TOMMATH=1"
fi
# For some reason, keg-only tcl-tk@8 successfully links with Tkinter without specifying rpath, with `/opt' rpath
# so no need to translate /Cellar path to /opt path
local tcltk_libs="$(sh -c 'cd '"$prefix"'/lib; . ./tclConfig.sh; . ./tkConfig.sh; echo "$TCL_LIB_SPEC $TK_LIB_SPEC"')"
if [[ -n "$PYTHON_BUILD_TCLTK_USE_PKGCONFIG" ]]; then
# pkg-config is not present out of the box in MacOS.
# There's no way to provide a fallback only if it's is not present
# and Configure's logic of detecting if it's present is complicated.
# So we just override it always
export TCLTK_CFLAGS="$tcltk_includes${tcltk_cflags:+ $tcltk_cflags}"
export TCLTK_LIBS="$tcltk_libs"
else
package_option python configure --with-tcltk-includes="$tcltk_includes"
package_option python configure --with-tcltk-libs="$tcltk_libs"
[[ -n $tcltk_cflags ]] && export CFLAGS="${tcltk_cflags}${CFLAGS:+ $CFLAGS}"
fi
lock_in pixi
return 0
fi
return 1
}
# FIXME: this function is a workaround for #1125
# once fixed, it should be removed.
# if tcltk_ops_flag is in PYTHON_CONFIGURE_OPTS, use user provided tcltk

View file

@ -212,16 +212,19 @@ make install
OUT
}
@test "Homebrew and port are tried if both are present in PATH in MacOS" {
@test "Homebrew, port, and pixi are tried if all are present in PATH in MacOS" {
cached_tarball "Python-3.6.2"
BREW_PREFIX="$BATS_TEST_TMPDIR/homebrew-prefix"
PIXI_PREFIX="$BATS_TEST_TMPDIR/pixi-prefix"
stub uname '-s : echo Darwin'
stub sw_vers '-productVersion : echo 1010'
for i in {1..5}; do stub brew false; done
stub brew "--prefix : echo '$BREW_PREFIX'"
for i in {1..3}; do stub port false; done
stub pixi "info : echo 'Environment dir: $PIXI_PREFIX'"
for i in {1..4}; do stub pixi false; done
stub_make_install
export PYENV_DEBUG=1
@ -234,6 +237,7 @@ DEF
unstub sw_vers
unstub brew
unstub port
unstub pixi
unstub make
assert_build_log <<OUT
@ -306,6 +310,38 @@ make install
OUT
}
@test "pixi is used if Homebrew and MacPorts were not picked" {
cached_tarball "Python-3.6.2"
PIXI_ENV_DIR="$BATS_TEST_TMPDIR/pixi-envs"
mkdir -p "$PIXI_ENV_DIR/python-deps"
stub uname '-s : echo Darwin'
stub sw_vers '-productVersion : echo 1010'
stub pixi "info : echo 'Environment dir: $PIXI_ENV_DIR'"
for i in {1..4}; do stub pixi false; done
stub_make_install
export PYTHON_BUILD_SKIP_HOMEBREW=1
export PYTHON_BUILD_SKIP_MACPORTS=1
run_inline_definition <<DEF
install_package "Python-3.6.2" "http://python.org/ftp/python/3.6.2/Python-3.6.2.tar.gz"
DEF
assert_success
unstub uname
unstub sw_vers
unstub pixi
unstub make
assert_build_log <<OUT
Python-3.6.2: CFLAGS="" CPPFLAGS="-I${PIXI_ENV_DIR}/python-deps/include -I${BATS_TEST_TMPDIR}/install/include" LDFLAGS="-L${PIXI_ENV_DIR}/python-deps/lib -Wl,-rpath,${PIXI_ENV_DIR}/python-deps/lib -L${BATS_TEST_TMPDIR}/install/lib -Wl,-rpath,${BATS_TEST_TMPDIR}/install/lib" PKG_CONFIG_PATH="${PIXI_ENV_DIR}/python-deps/lib/pkgconfig"
Python-3.6.2: --prefix=$INSTALL_ROOT --enable-shared --libdir=$INSTALL_ROOT/lib
make -j 2
make install
OUT
}
@test "yaml is linked from Homebrew" {
cached_tarball "Python-3.6.2"
@ -483,6 +519,107 @@ make install
OUT
}
@test "yaml is linked from pixi" {
cached_tarball "Python-3.6.2"
PIXI_ENV_DIR="$BATS_TEST_TMPDIR/pixi-envs"
mkdir -p "$PIXI_ENV_DIR/python-deps"
stub uname '-s : echo Darwin'
stub sw_vers '-productVersion : echo 1010'
stub pixi "info : echo 'Environment dir: $PIXI_ENV_DIR'"
stub pixi "global list --environment python-deps : echo yaml"
for i in {1..4}; do stub pixi false; done
stub_make_install
export PYTHON_BUILD_SKIP_HOMEBREW=1
export PYTHON_BUILD_SKIP_MACPORTS=1
install_fixture definitions/needs-yaml
assert_success
unstub uname
unstub sw_vers
unstub pixi
unstub make
assert_build_log <<OUT
Python-3.6.2: CFLAGS="" CPPFLAGS="-I${PIXI_ENV_DIR}/python-deps/include -I${BATS_TEST_TMPDIR}/install/include" LDFLAGS="-L${PIXI_ENV_DIR}/python-deps/lib -Wl,-rpath,${PIXI_ENV_DIR}/python-deps/lib -L${BATS_TEST_TMPDIR}/install/lib -Wl,-rpath,${BATS_TEST_TMPDIR}/install/lib" PKG_CONFIG_PATH="${PIXI_ENV_DIR}/python-deps/lib/pkgconfig"
Python-3.6.2: --prefix=$INSTALL_ROOT --enable-shared --libdir=$INSTALL_ROOT/lib
make -j 2
make install
OUT
}
@test "readline is linked from pixi" {
cached_tarball "Python-3.6.2"
PIXI_ENV_DIR="$BATS_TEST_TMPDIR/pixi-envs"
mkdir -p "$PIXI_ENV_DIR/python-deps"
stub uname '-s : echo Darwin'
stub sw_vers '-productVersion : echo 1010'
stub pixi "info : echo 'Environment dir: $PIXI_ENV_DIR'"
stub pixi false
stub pixi "global list --environment python-deps : echo readline"
stub pixi false
stub pixi false
stub_make_install
export PYTHON_BUILD_SKIP_HOMEBREW=1
export PYTHON_BUILD_SKIP_MACPORTS=1
run_inline_definition <<DEF
install_package "Python-3.6.2" "http://python.org/ftp/python/3.6.2/Python-3.6.2.tar.gz"
DEF
assert_success
unstub uname
unstub sw_vers
unstub pixi
unstub make
assert_build_log <<OUT
Python-3.6.2: CFLAGS="" CPPFLAGS="-I${PIXI_ENV_DIR}/python-deps/include -I${BATS_TEST_TMPDIR}/install/include" LDFLAGS="-L${PIXI_ENV_DIR}/python-deps/lib -Wl,-rpath,${PIXI_ENV_DIR}/python-deps/lib -L${BATS_TEST_TMPDIR}/install/lib -Wl,-rpath,${BATS_TEST_TMPDIR}/install/lib" PKG_CONFIG_PATH="${PIXI_ENV_DIR}/python-deps/lib/pkgconfig"
Python-3.6.2: --prefix=$INSTALL_ROOT --enable-shared --libdir=$INSTALL_ROOT/lib
make -j 2
make install
OUT
}
@test "ncurses is linked from pixi" {
cached_tarball "Python-3.6.2"
PIXI_ENV_DIR="$BATS_TEST_TMPDIR/pixi-envs"
mkdir -p "$PIXI_ENV_DIR/python-deps"
stub uname '-s : echo Darwin'
stub sw_vers '-productVersion : echo 1010'
stub pixi "info : echo 'Environment dir: $PIXI_ENV_DIR'"
stub pixi false
stub pixi false
stub pixi "global list --environment python-deps : echo ncurses"
stub pixi false
stub_make_install
export PYTHON_BUILD_SKIP_HOMEBREW=1
export PYTHON_BUILD_SKIP_MACPORTS=1
run_inline_definition <<DEF
install_package "Python-3.6.2" "http://python.org/ftp/python/3.6.2/Python-3.6.2.tar.gz"
DEF
assert_success
unstub uname
unstub sw_vers
unstub pixi
unstub make
assert_build_log <<OUT
Python-3.6.2: CFLAGS="" CPPFLAGS="-I${PIXI_ENV_DIR}/python-deps/include -I${BATS_TEST_TMPDIR}/install/include" LDFLAGS="-L${PIXI_ENV_DIR}/python-deps/lib -Wl,-rpath,${PIXI_ENV_DIR}/python-deps/lib -L${BATS_TEST_TMPDIR}/install/lib -Wl,-rpath,${BATS_TEST_TMPDIR}/install/lib" PKG_CONFIG_PATH="${PIXI_ENV_DIR}/python-deps/lib/pkgconfig"
Python-3.6.2: --prefix=$INSTALL_ROOT --enable-shared --libdir=$INSTALL_ROOT/lib
make -j 2
make install
OUT
}
@test "openssl is linked from Ports in FreeBSD if present" {
cached_tarball "Python-3.6.2"
@ -605,6 +742,36 @@ make install
OUT
}
@test "pixi is not touched if PYTHON_BUILD_SKIP_PIXI is set" {
cached_tarball "Python-3.6.2"
stub uname '-s : echo Darwin'
stub sw_vers '-productVersion : echo 1010'
stub brew false
stub port false
stub pixi true; pixi
stub_make_install
export PYTHON_BUILD_SKIP_HOMEBREW=1
export PYTHON_BUILD_SKIP_MACPORTS=1
export PYTHON_BUILD_SKIP_PIXI=1
run_inline_definition <<DEF
install_package "Python-3.6.2" "http://python.org/ftp/python/3.6.2/Python-3.6.2.tar.gz"
DEF
assert_success
unstub uname
unstub pixi
unstub make
assert_build_log <<OUT
Python-3.6.2: CFLAGS="" CPPFLAGS="-I${BATS_TEST_TMPDIR}/install/include" LDFLAGS="-L${BATS_TEST_TMPDIR}/install/lib -Wl,-rpath,${BATS_TEST_TMPDIR}/install/lib" PKG_CONFIG_PATH=""
Python-3.6.2: --prefix=$INSTALL_ROOT --enable-shared --libdir=$INSTALL_ROOT/lib
make -j 2
make install
OUT
}
@test "MacPorts is not touched in Linux" {
cached_tarball "Python-3.6.2"
@ -1001,6 +1168,42 @@ make install
OUT
}
@test "tcl-tk is linked from pixi via --with-tcl-*" {
cached_tarball "Python-3.6.2"
PIXI_ENV_DIR="$BATS_TEST_TMPDIR/pixi-envs"
mkdir -p "$PIXI_ENV_DIR/python-deps"
stub_tcltk "$PIXI_ENV_DIR/python-deps"
stub uname '-s : echo Darwin'
stub sw_vers '-productVersion : echo 1010'
stub pixi "info : echo 'Environment dir: $PIXI_ENV_DIR'"
stub pixi "global list --environment python-deps : echo tk"
stub pixi false
stub pixi false
stub pixi false
stub_make_install
export PYTHON_BUILD_SKIP_HOMEBREW=1
export PYTHON_BUILD_SKIP_MACPORTS=1
run_inline_definition <<DEF
install_package "Python-3.6.2" "http://python.org/ftp/python/3.6.2/Python-3.6.2.tar.gz"
DEF
assert_success
unstub uname
unstub sw_vers
unstub pixi
unstub make
assert_build_log <<OUT
Python-3.6.2: CFLAGS="" CPPFLAGS="-I${PIXI_ENV_DIR}/python-deps/include -I${BATS_TEST_TMPDIR}/install/include" LDFLAGS="-L${PIXI_ENV_DIR}/python-deps/lib -Wl,-rpath,${PIXI_ENV_DIR}/python-deps/lib -L${BATS_TEST_TMPDIR}/install/lib -Wl,-rpath,${BATS_TEST_TMPDIR}/install/lib" PKG_CONFIG_PATH="${PIXI_ENV_DIR}/python-deps/lib/pkgconfig"
Python-3.6.2: --prefix=${BATS_TEST_TMPDIR}/install --enable-shared --libdir=${BATS_TEST_TMPDIR}/install/lib --with-tcltk-includes=-I${PIXI_ENV_DIR}/python-deps/include -I${PIXI_ENV_DIR}/python-deps/include --with-tcltk-libs=-L${PIXI_ENV_DIR}/python-deps/lib -ltclX.Y -L${PIXI_ENV_DIR}/python-deps/lib -ltkX.Y
make -j 2
make install
OUT
}
@test "number of CPU cores defaults to 2" {
cached_tarball "Python-3.6.2"