From f434db24cd4a223e7447a71b38dd3727a4418cff Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Mon, 20 Oct 2025 17:39:24 +0200 Subject: [PATCH 1/3] 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`. --- plugins/python-build/bin/python-build | 194 +++++++++++++++++++++++- plugins/python-build/test/build.bats | 205 +++++++++++++++++++++++++- 2 files changed, 397 insertions(+), 2 deletions(-) diff --git a/plugins/python-build/bin/python-build b/plugins/python-build/bin/python-build index 8e3f5a0f..3cce8d20 100755 --- a/plugins/python-build/bin/python-build +++ b/plugins/python-build/bin/python-build @@ -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 diff --git a/plugins/python-build/test/build.bats b/plugins/python-build/test/build.bats index 7d787eab..581af781 100644 --- a/plugins/python-build/test/build.bats +++ b/plugins/python-build/test/build.bats @@ -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 < Date: Thu, 23 Oct 2025 15:51:37 +0200 Subject: [PATCH 2/3] Factor out function for configuring tcltk flags --- plugins/python-build/bin/python-build | 81 +++++++++++---------------- 1 file changed, 32 insertions(+), 49 deletions(-) diff --git a/plugins/python-build/bin/python-build b/plugins/python-build/bin/python-build index 3cce8d20..8a0f5ee3 100755 --- a/plugins/python-build/bin/python-build +++ b/plugins/python-build/bin/python-build @@ -2032,6 +2032,35 @@ use_pixi_zlib() { fi } +configure_tcltk_flags() { + local prefix="$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"')" + # Since 2.7.6, 3.3.3, 3.4.0 (Issue #1584): --with-tcltk-includes + --with-tcltk-libs Configure options + # Since 3.11.0 (bpo-45847): `pkg-config` call, TCLTK_CFLAGS + TCLTK_LIBS override + 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 +} + use_homebrew_tcltk() { can_use_homebrew || return 1 local tcltk_formula @@ -2042,33 +2071,9 @@ use_homebrew_tcltk() { local tcltk_prefix="$(brew --prefix "${tcltk_formula}" 2>/dev/null || true)" if [ -d "$tcltk_prefix" ]; then echo "python-build: use ${tcltk_formula} from homebrew" - # In Homebrew Tcl/Tk 8.6.13, headers have been moved to the 'tcl-tk' subdir. - local tcltk_includes="$(sh -c 'cd '"$tcltk_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 '. '"$tcltk_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 '"$tcltk_prefix"'/lib; . ./tclConfig.sh; . ./tkConfig.sh; echo "$TCL_LIB_SPEC $TK_LIB_SPEC"')" - # Since 2.7.6, 3.3.3, 3.4.0 (Issue #1584): --with-tcltk-includes + --with-tcltk-libs Configure options - # Since 3.11.0 (bpo-45847): `pkg-config` call, TCLTK_CFLAGS + TCLTK_LIBS override - 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 - #set in either case as a failsafe + configure_tcltk_flags "$tcltk_prefix" + # set PKG_CONFIG_PATH as a failsafe export PKG_CONFIG_PATH="${tcltk_prefix}/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}" - lock_in homebrew return 0 fi @@ -2085,29 +2090,7 @@ use_pixi_tcltk() { 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 - + configure_tcltk_flags "$prefix" lock_in pixi return 0 fi From c26bc2455748806a416ee054827be1754cf35d50 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Thu, 23 Oct 2025 15:59:23 +0200 Subject: [PATCH 3/3] Update README --- plugins/python-build/README.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/plugins/python-build/README.md b/plugins/python-build/README.md index 22dc2420..6066e727 100644 --- a/plugins/python-build/README.md +++ b/plugins/python-build/README.md @@ -144,10 +144,19 @@ MacPorts Homebrew is used to find dependency packages if `port` is found on `PAT Set `PYTHON_BUILD_USE_MACPORTS` or `PYTHON_BUILD_SKIP_MACPORTS` to override this default. -###### Interaction with Homebrew +##### Pixi -If both Homebrew and MacPorts are installed and allowed to be used, Homebrew takes preference. -There first ecosystem where any of the required dependency packages is found is used. +Pixi is used to find dependency packages if `pixi` is found on `PATH` in both MacOS and Linux. + +Set `PYTHON_BUILD_USE_PIXI` or `PYTHON_BUILD_SKIP_PIXI` to override this default. + +By default, python-build looks for dependencies in a Pixi global environment named `python-deps`. +You can override this by setting `PYTHON_BUILD_PIXI_ENV` to a different environment name. + +##### Interaction between Homebrew, MacPorts and pixi + +If more than once package ecosystems are installed, Homebrew takes preference, then MacPorts, then pixi. +The first ecosystem where any of the required dependency packages is found is used. ##### Portage @@ -178,6 +187,9 @@ You can set certain environment variables to control the build process. * `PYTHON_BUILD_TCLTK_FORMULA`, override the Homebrew Tcl/Tk formula to use. * `PYTHON_BUILD_SKIP_MACPORTS`, if set, will not search for libraries installed by MacPorts when it would normally will. * `PYTHON_BUILD_USE_MACPORTS`, if set, will search for libraries installed by MacPorts when it would normally not. +* `PYTHON_BUILD_SKIP_PIXI`, if set, will not search for libraries installed by pixi when it would normally will. +* `PYTHON_BUILD_USE_PIXI`, if set, will search for libraries installed by pixi when it would normally not. +* `PYTHON_BUILD_PIXI_ENV`, override the Pixi global environment to use (defaults to `python-deps`). * `PYTHON_BUILD_ROOT` overrides the default location from where build definitions in `share/python-build/` are looked up. * `PYTHON_BUILD_DEFINITIONS` can be a list of colon-separated paths that get