From 6cce9af813524a9ef96d6aeb429cd4797c05b78a Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Mon, 31 Aug 2026 13:51:36 +0530 Subject: [PATCH 1/4] FEAT: conda recipe for mssql-python (self-contained repackage of the signed wheel) Repackages the prebuilt, ESRP-signed mssql-python wheel into a conda package offline (no compile), vendoring the ODBC Driver 18 payload inside it (the v1.11.0 self-contained model -- no separate mssql-python-odbc conda package). - meta.yaml: python >=3.10 floor (host + run); repackage-safe conda-build flags (error_overlinking / error_overdepending off, binary_relocation off, skip pyc on the osx/win cross legs); Linux run deps openssl >=3,<4 / krb5 / libtool for driver reachability (#563); win vc14_runtime. - build.sh: Linux/macOS repackage; patchelf $ORIGIN climb (#563); the cross osx-arm64 leg extracts the universal2 wheel without Python, filtered on CONDA_PY so it can never grab another interpreter's cpXY wheel. - bld.bat: Windows repackage; the cross win-arm64 leg extracts without Python and strips the x64 mssql_py_core (no arm64 build yet -> bulk copy raises a clean "not available" error); guarded wheel globs; exit /b on failure. - .gitattributes: *.sh eol=lf, *.bat eol=crlf. --- conda/.gitattributes | 8 +++ conda/mssql-python/bld.bat | 57 +++++++++++++++++ conda/mssql-python/build.sh | 118 ++++++++++++++++++++++++++++++++++ conda/mssql-python/meta.yaml | 119 +++++++++++++++++++++++++++++++++++ 4 files changed, 302 insertions(+) create mode 100644 conda/.gitattributes create mode 100644 conda/mssql-python/bld.bat create mode 100644 conda/mssql-python/build.sh create mode 100644 conda/mssql-python/meta.yaml diff --git a/conda/.gitattributes b/conda/.gitattributes new file mode 100644 index 00000000..859639c5 --- /dev/null +++ b/conda/.gitattributes @@ -0,0 +1,8 @@ +# Keep shell build scripts LF so conda-build works on Linux/macOS agents, +# regardless of the checkout host's core.autocrlf setting. +*.sh text eol=lf + +# Keep .bat scripts CRLF for Windows conda-build agents, regardless of the checkout +# host's core.autocrlf. cmd.exe tolerates LF here (no goto/labels), but pinning CRLF +# removes any doubt on odd checkouts. +*.bat text eol=crlf diff --git a/conda/mssql-python/bld.bat b/conda/mssql-python/bld.bat new file mode 100644 index 00000000..e9643f47 --- /dev/null +++ b/conda/mssql-python/bld.bat @@ -0,0 +1,57 @@ +@echo on +REM Repackage the prebuilt, ESRP-signed wheel into a conda package (offline), and +REM vendor the ODBC Driver 18 payload INTO it (v1.11.0 model: libs ship inside). +REM PKG_NAME / PKG_VERSION are exported by conda-build; WHEELS_DIR + MSSQL_ODBC_VERSION +REM by the pipeline. +setlocal enabledelayedexpansion + +REM Site-packages of the (possibly cross-targeted) host env, plus the underscore form +REM of the package name used in wheel filenames (mssql-python -> mssql_python). +set "SP=%PREFIX%\Lib\site-packages" +if not exist "%SP%" mkdir "%SP%" +set "PKG_UNDERSCORE=%PKG_NAME:-=_%" + +REM Install the prebuilt code wheel. On a NATIVE leg the host Python runs and pip +REM installs it (pip auto-selects the wheel matching this host). On a CROSS win-arm64 +REM build the win-arm64 host Python CANNOT execute on the x64 agent (Windows has no +REM reverse emulation), so extract the wheel (a zip) WITHOUT Python -- the same +REM approach build.sh uses for the osx-arm64 cross-build. conda-build exports CONDA_PY +REM (e.g. 314), which selects the matching cp tag of the win_arm64 slice. +"%PYTHON%" -c "import sys" >nul 2>&1 +if errorlevel 1 ( + echo Host Python "%PYTHON%" not executable here ^(non-emulated cross-build^); extracting cp%CONDA_PY% win_arm64 code wheel without Python. + set "CODE_WHL=" + for %%W in ("%WHEELS_DIR%\!PKG_UNDERSCORE!-%PKG_VERSION%-cp%CONDA_PY%-*-win_arm64.whl") do if exist "%%~fW" set "CODE_WHL=%%~fW" + if not defined CODE_WHL ( + echo ERROR: no %PKG_NAME%==%PKG_VERSION% cp%CONDA_PY% win_arm64 wheel in "%WHEELS_DIR%" + exit /b 1 + ) + echo Extracting "!CODE_WHL!" into "%SP%" + tar -xf "!CODE_WHL!" -C "%SP%" + if errorlevel 1 exit /b 1 + REM No arm64 Windows build of mssql_py_core exists yet, so the win-arm64 code wheel + REM bundles the x64 (win_amd64) one. An x64 .pyd cannot load on arm64 -- it would + REM crash bulk copy AND fails the PE-arch assert -- so REMOVE it from the win-arm64 + REM package. Bulk copy then raises a clean "not available" error (the import is lazy); + REM the rest of the DBAPI is unaffected (ddbc_bindings is native arm64). Restore once + REM an arm64 mssql_py_core ships in the feed. + echo Removing x64 mssql_py_core from the win-arm64 package; bulk copy is unavailable on win-arm64 until an arm64 build ships. + if exist "%SP%\mssql_py_core" rmdir /s /q "%SP%\mssql_py_core" + if exist "%SP%\mssql_py_core.libs" rmdir /s /q "%SP%\mssql_py_core.libs" +) else ( + "%PYTHON%" -m pip install --no-deps --no-index --find-links "%WHEELS_DIR%" %PKG_NAME%==%PKG_VERSION% -vv + if errorlevel 1 exit /b 1 +) + +REM Extract the python-agnostic py3-none-win odbc wheel into the SAME site-packages +REM so mssql_python_odbc\libs\ sits beside mssql_python\ (the loader finds the driver +REM there). WHEELS_DIR is staged per-target, so a single matching odbc wheel is present. +set "ODBC_WHL=" +for %%W in ("%WHEELS_DIR%\mssql_python_odbc-%MSSQL_ODBC_VERSION%-py3-none-win_*.whl") do if exist "%%~fW" set "ODBC_WHL=%%~fW" +if not defined ODBC_WHL ( + echo ERROR: no mssql_python_odbc==%MSSQL_ODBC_VERSION% py3-none-win wheel in "%WHEELS_DIR%" + exit /b 1 +) +echo Extracting "!ODBC_WHL!" into "%SP%" +tar -xf "!ODBC_WHL!" -C "%SP%" +if errorlevel 1 exit /b 1 diff --git a/conda/mssql-python/build.sh b/conda/mssql-python/build.sh new file mode 100644 index 00000000..6385dc26 --- /dev/null +++ b/conda/mssql-python/build.sh @@ -0,0 +1,118 @@ +#!/bin/bash +# Repackage the prebuilt, ESRP-signed wheel into a conda package (offline). +# PKG_NAME / PKG_VERSION are exported by conda-build; WHEELS_DIR by the pipeline/harness. +set -euo pipefail +# Cross-arch (emulated) build: when repackaging the aarch64 wheel on an x86_64 host, +# $PYTHON is the target-arch interpreter and runs under qemu-user. Point qemu at the +# aarch64 glibc loader/libs (installed via libc6-arm64-cross) so it can find +# /lib/ld-linux-aarch64.so.1 instead of aborting with "Could not open". The dir only +# exists on the emulated aarch64 leg; setting the var elsewhere is a harmless no-op. +[ -d /usr/aarch64-linux-gnu ] && export QEMU_LD_PREFIX="${QEMU_LD_PREFIX:-/usr/aarch64-linux-gnu}" + +# This package is SELF-CONTAINED (v1.11.0 model): the ODBC Driver 18 payload ships +# INSIDE it, so there is NO separate mssql-python-odbc conda package. We land BOTH +# the code wheel AND the python-agnostic py3-none- odbc wheel in the SAME +# site-packages, so mssql_python_odbc/libs/ sits beside mssql_python/ and the C++ +# loader resolves the driver there. WHEELS_DIR is staged per-target by the pipeline, +# so exactly one matching odbc wheel is present. +odbc_ver="${MSSQL_ODBC_VERSION:?MSSQL_ODBC_VERSION not set}" + +# The normal path installs with the host-env Python -- native builds, and the +# QEMU-emulated linux-aarch64 leg where the aarch64 Python runs under binfmt. pip +# resolves the correct site-packages for BOTH wheels, so no unzip is needed there. +# The osx-arm64 conda package is CROSS-built on an Intel macOS agent (no reverse +# Rosetta): the arm64 host Python CANNOT execute and pip would abort, so extract +# both wheels (zips) WITHOUT Python -- the same approach the Windows bld.bat uses +# with `tar`. macOS ships `unzip`. The arm64 slice comes from the universal2 wheel; +# conda-build still stamps osx-arm64. +if "$PYTHON" -c "import sys" >/dev/null 2>&1; then + "$PYTHON" -m pip install --no-deps --no-index --find-links "$WHEELS_DIR" "$PKG_NAME==$PKG_VERSION" -vv + "$PYTHON" -m pip install --no-deps --no-index --find-links "$WHEELS_DIR" "mssql-python-odbc==$odbc_ver" -vv +else + echo "Host Python '$PYTHON' is not executable on this agent (non-emulated cross-build);" + echo "extracting both wheels into \$SP_DIR without running Python." + mkdir -p "$SP_DIR" + pkg_underscore="${PKG_NAME//-/_}" + # macOS universal2 wheels are cpXY-specific (they carry a compiled ddbc_bindings), + # so filter on the target CONDA_PY -- as bld.bat does for the win_arm64 slice -- to + # never silently grab another interpreter's wheel if WHEELS_DIR ever stages more + # than one. The pipeline stages exactly one today; this is defense-in-depth. + code_whl="" + for w in "$WHEELS_DIR/${pkg_underscore}-${PKG_VERSION}-cp${CONDA_PY}-"*.whl; do + [ -e "$w" ] && { code_whl="$w"; break; } + done + [ -n "$code_whl" ] || { echo "ERROR: no ${PKG_NAME}==${PKG_VERSION} cp${CONDA_PY} wheel in '$WHEELS_DIR'" >&2; exit 1; } + odbc_whl="" + for w in "$WHEELS_DIR"/mssql_python_odbc-"$odbc_ver"-py3-none-*.whl; do + [ -e "$w" ] && { odbc_whl="$w"; break; } + done + [ -n "$odbc_whl" ] || { echo "ERROR: no mssql_python_odbc==$odbc_ver py3-none wheel in '$WHEELS_DIR'" >&2; exit 1; } + echo "Extracting '$code_whl' -> '$SP_DIR'" + unzip -oq "$code_whl" -d "$SP_DIR" + echo "Extracting '$odbc_whl' -> '$SP_DIR'" + unzip -oq "$odbc_whl" -d "$SP_DIR" +fi + +# --------------------------------------------------------------------------- +# Linux driver reachability (#563) -- the core fix. +# --------------------------------------------------------------------------- +# Declaring krb5/openssl/libltdl as conda deps drops one consistent copy of each +# into $PREFIX/lib, but that is INERT on its own: the vendored ODBC binaries ship +# with a bare DT_RUNPATH=$ORIGIN (no climb), so on a minimal conda base the loader +# never looks in $PREFIX/lib -- it falls through to SYSTEM krb5 (the #563 mixing +# crash) and cannot find libltdl.so.7 at all. Reachability, not declaration, is the +# lever: stamp a PURELY RELATIVE $ORIGIN climb (the ELF twin of the macOS +# @loader_path flow) onto libmsodbcsql* and libodbcinst.so.2 so they resolve THIS +# env's own $PREFIX/lib, location-independently. +# +# SIGNATURE SAFETY: +# The Linux ODBC .so are NOT ESRP code-signed -- the mssql-python-odbc pipeline only +# MALWARE-SCANS them (there is no CodeSign task). Only Windows .dll (Authenticode) +# and macOS .dylib (codesign) are code-signed, and this recipe never patches those +# (the climb is Linux-only). So stamping the relative $ORIGIN climb here breaks no +# signature. If the binaries already carry the EXACT climb (e.g. a future odbc-side +# pre-bake before signing), the patch is a byte-for-byte no-op. +# +# The canonical RUNPATH is exactly "$ORIGIN:$ORIGIN/" -- the patch below emits +# that literal form, and the static audit (eng/scripts/audit_bundled_binaries.py) +# requires the same exact climb entry. +# +# Linux-only by construction: the glob matches nothing in a macOS payload +# (libs/macos/...), so this whole block is a natural no-op on the osx legs. +prefix_lib="$PREFIX/lib" +shopt -s nullglob +have_linux_payload=0 +[ -d "$SP_DIR/mssql_python_odbc/libs/linux" ] && have_linux_payload=1 +drivers_seen=0 +for libdir in "$SP_DIR"/mssql_python_odbc/libs/linux/*/*/lib; do + # Compute the EXACT expected climb from THIS driver dir up to $PREFIX/lib (derived + # from the real install layout, never a hard-coded ../ count). + climb="$("$PYTHON" -c 'import os,sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))' "$prefix_lib" "$libdir")" + want="\$ORIGIN:\$ORIGIN/$climb" + for so in "$libdir"/libmsodbcsql-*.so.* "$libdir"/libodbcinst.so.2; do + [ -e "$so" ] || continue + drivers_seen=$((drivers_seen + 1)) + got="$(patchelf --print-rpath "$so" 2>/dev/null || true)" + if [ "$got" = "$want" ]; then + echo "RPATH-OK (already baked) $(basename "$so") -> $got" + continue + fi + # Stamp the exact $ORIGIN climb (safe -- these Linux .so are not code-signed). + patchelf --set-rpath "$want" "$so" + got="$(patchelf --print-rpath "$so")" + # H2: compare EXACTLY to the intended value, not just "no absolute entry". + if [ "$got" != "$want" ]; then + echo "ERROR: patch did not yield the exact expected RUNPATH ('$got' != '$want')." >&2 + exit 1 + fi + echo "RPATH-PATCHED $(basename "$so") -> $got" + done +done +shopt -u nullglob +# H2: a Linux payload with NO driver found is a bypass hole -- a bare `conda build` +# skipping the orchestrator audit would then ship un-asserted drivers. Fail loudly. +if [ "$have_linux_payload" = "1" ] && [ "$drivers_seen" = "0" ]; then + echo "ERROR: Linux ODBC payload present but no libmsodbcsql*/libodbcinst.so.2 found to assert the #563 climb." >&2 + exit 1 +fi +[ "$drivers_seen" -gt 0 ] && echo "LINUX_RPATH_CLIMB_OK" || true diff --git a/conda/mssql-python/meta.yaml b/conda/mssql-python/meta.yaml new file mode 100644 index 00000000..fbcebd40 --- /dev/null +++ b/conda/mssql-python/meta.yaml @@ -0,0 +1,119 @@ +{% set version = environ.get('MSSQL_PYTHON_VERSION', '1.14.0') %} + +package: + name: mssql-python + version: "{{ version }}" + +build: + number: 0 + # Like the companion, this recipe REPACKAGES a prebuilt wheel (the compiled + # ddbc_bindings extension + bundled runtime); it compiles nothing. conda-build's + # overlinking/overdepending checks target from-source builds and mis-fire on + # vendored binaries (e.g. ddbc_bindings linking the driver that lives in the + # separate companion package), so downgrade both from errors to warnings -- CI + # enables them as errors by default. + error_overlinking: false + error_overdepending: false + # Like the companion, this recipe vendors PREBUILT, signed binaries (the compiled + # ddbc_bindings extension + bundled VC++ runtime); conda-build must neither rewrite + # nor scan them: + # - binary_relocation: rewriting RPATH / install-name in a signed binary corrupts + # the signature. + # - detect_binary_files_with_prefix: the build-prefix scan over the signed native + # binaries is meaningless for a pure repackage and is the packaging step that + # fails right after "Fixing permissions" on these recipes. + binary_relocation: false + detect_binary_files_with_prefix: false + # conda-build's .pyc byte-compilation runs the TARGET Python. That target cannot + # execute on the build agent for two CROSS legs: osx-arm64 (built on the Intel/x86 + # macOS agent) and win-arm64 (cross-built on the x64 agent -- Windows has no reverse + # emulation) -- so skip pyc on osx AND win (Python regenerates .pyc at import). The + # native osx-64/win-64 legs are unaffected in practice (pip already wrote .pyc). + # Linux legs are native or QEMU-emulated, so they keep compiling. + skip_compile_pyc: + - "**/*.py" # [osx or win] + # WHEELS_DIR is exported by the pipeline (or a local harness) and passed into the + # isolated conda-build environment so bld.bat / build.sh can install the prebuilt, + # ESRP-signed wheel from --find-links, fully offline. This mirrors CI: no PyPI. + script_env: + - WHEELS_DIR + # MSSQL_ODBC_VERSION lets build.sh / bld.bat locate the matching mssql-python-odbc + # wheel to vendor INTO this package (the driver payload now ships inside). + - MSSQL_ODBC_VERSION + +requirements: + # Linux reachability fix (#563): patchelf stamps a relative $ORIGIN RPATH climb + # onto the vendored ODBC binaries in build.sh so the DECLARED conda + # krb5/openssl/libltdl in $PREFIX/lib are actually REACHABLE. Declaration alone is + # inert -- the driver ships with a bare DT_RUNPATH=$ORIGIN (no climb) and would + # fall through to SYSTEM krb5/libltdl (the exact #563 mixing bug). Build-time only, + # Linux only (macOS uses @loader_path, Windows has no RPATH). + build: + - patchelf # [linux] + host: + - python >=3.10 + - pip + run: + - python >=3.10 + # No version floor: on the `microsoft` channel azure-identity / azure-core / msal + # ship as CalVer (e.g. 2026.06.01), so a semver floor like `>=1.12.0` is a + # misleading no-op there (every published build already satisfies it). + - azure-identity + # --- ODBC Driver 18 payload deps (folded in from the former companion) -------- + # The proprietary driver libs now ship INSIDE this package (the v1.11.0 model: + # libs bundled in the wheel), so there is NO separate `mssql-python-odbc` conda + # package and its declared, security-serviced deps live here instead. + # + # OpenSSL: the driver dlopen's libssl/libcrypto for TLS (Encrypt=yes). Because it + # is dlopen'd (not an ELF NEEDED) conda-build's overlinking can't see it, so it + # must be declared. Linux-only, pinned >=3,<4 (Driver 18 supports the OpenSSL + # 1.1/3.0 ABI only; conda-forge has begun shipping openssl 4). macOS is excluded + # (the signed dylib dlopen's OpenSSL from a hardcoded Homebrew path -- users + # `brew install openssl`); Windows uses SChannel. + - openssl >=3,<4 # [linux] + # Kerberos: libmsodbcsql NEEDs libkrb5.so.3 + libgssapi_krb5.so.2 on Linux. macOS + # uses Kerberos.framework and Windows uses SSPI, so krb5 is Linux-only. + - krb5 # [linux] + # libltdl: libodbcinst.so.2 (the unixODBC driver manager the ODBC driver loads + # through) NEEDs libltdl.so.7, which is NOT bundled on Linux. Declare libtool + # (which provides libltdl.so.7); the $ORIGIN RPATH climb in build.sh makes the + # driver resolve THIS env's copy instead of failing "libltdl.so.7 not found". + # Linux-only (macOS bundles libltdl.7.dylib; Windows has no ltdl). + - libtool # [linux] + # Windows VC++ runtime: msodbcsql18.dll imports VCRUNTIME140.dll, but the vendored + # vcredist ships only msvcp140.dll. Declare the security-serviced conda runtime. + - vc14_runtime # [win] + # conda drops the wheel's platform tag (manylinux_2_28 / macosx_15_0), so + # re-assert that floor as a virtual-package run constraint. The bundled wheels + # already required these, so this is never stricter than what shipped. + - __glibc >=2.28 # [linux] + - __osx >=15.0 # [osx] + +test: + imports: + - mssql_python + +about: + home: https://github.com/microsoft/mssql-python + # This package ships BOTH the MIT-licensed mssql-python code AND the proprietary + # Microsoft ODBC Driver 18 payload (+ the bundled VC++ runtime on Windows), so the + # license is the MIT code license AND the Microsoft proprietary EULA. + license: MIT AND LicenseRef-Microsoft-Proprietary + license_file: + - ../../LICENSE + # The proprietary EULAs live canonically with the ODBC payload; reference them + # there instead of committing duplicate copies in the recipe. conda-build resolves + # license_file relative to the recipe dir at package time and embeds them in + # info/licenses/. + - ../../mssql_python_odbc/licenses/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt + - ../../mssql_python_odbc/licenses/MICROSOFT_VISUAL_STUDIO_LICENSE.txt + summary: Microsoft driver for Python to interact with SQL Server and Azure SQL. + description: | + mssql-python is a DB API 2.0 (PEP 249) compliant driver for SQL Server, + Azure SQL, and Azure Synapse. This conda package is self-contained: the + proprietary Microsoft ODBC Driver 18 payload ships inside it (the same model as + the v1.11.0 wheel), so no separate driver package is required. + +extra: + recipe-maintainers: + - jahnvithakkar From 89544fda4ca72fc4c8f224e9e3812cb6fecf5272 Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Mon, 31 Aug 2026 14:17:02 +0530 Subject: [PATCH 2/4] Trim recipe comments to reviewer-focused essentials; add two hardening tweaks - meta.yaml: pin azure-identity >=1.12.0 in run: -- conda does NOT inherit the wheel's install_requires, so a solve against conda-forge could otherwise pick <1.12.0. - build.sh: count libmsodbcsql (driver) and libodbcinst (driver manager) separately so a Linux payload missing EITHER fails loudly (a lone libodbcinst would ship no SQL driver). - Condense the verbose block comments in meta.yaml / build.sh / bld.bat to concise, meaningful lines while keeping the essential "why". --- conda/mssql-python/bld.bat | 31 ++++-------- conda/mssql-python/build.sh | 96 ++++++++++++++---------------------- conda/mssql-python/meta.yaml | 92 ++++++++++------------------------ 3 files changed, 74 insertions(+), 145 deletions(-) diff --git a/conda/mssql-python/bld.bat b/conda/mssql-python/bld.bat index e9643f47..51b6acea 100644 --- a/conda/mssql-python/bld.bat +++ b/conda/mssql-python/bld.bat @@ -1,22 +1,15 @@ @echo on -REM Repackage the prebuilt, ESRP-signed wheel into a conda package (offline), and -REM vendor the ODBC Driver 18 payload INTO it (v1.11.0 model: libs ship inside). -REM PKG_NAME / PKG_VERSION are exported by conda-build; WHEELS_DIR + MSSQL_ODBC_VERSION -REM by the pipeline. +REM Repackage the prebuilt, signed mssql-python wheel into a conda package (offline) and +REM vendor the ODBC Driver 18 payload inside it. conda-build exports PKG_NAME / +REM PKG_VERSION / CONDA_PY; the pipeline exports WHEELS_DIR + MSSQL_ODBC_VERSION. setlocal enabledelayedexpansion -REM Site-packages of the (possibly cross-targeted) host env, plus the underscore form -REM of the package name used in wheel filenames (mssql-python -> mssql_python). set "SP=%PREFIX%\Lib\site-packages" if not exist "%SP%" mkdir "%SP%" set "PKG_UNDERSCORE=%PKG_NAME:-=_%" -REM Install the prebuilt code wheel. On a NATIVE leg the host Python runs and pip -REM installs it (pip auto-selects the wheel matching this host). On a CROSS win-arm64 -REM build the win-arm64 host Python CANNOT execute on the x64 agent (Windows has no -REM reverse emulation), so extract the wheel (a zip) WITHOUT Python -- the same -REM approach build.sh uses for the osx-arm64 cross-build. conda-build exports CONDA_PY -REM (e.g. 314), which selects the matching cp tag of the win_arm64 slice. +REM Native leg: pip installs the matching wheel. Cross win-arm64 leg: the arm64 host +REM Python can't run on the x64 agent, so extract the cp%CONDA_PY% wheel (a zip) with tar. "%PYTHON%" -c "import sys" >nul 2>&1 if errorlevel 1 ( echo Host Python "%PYTHON%" not executable here ^(non-emulated cross-build^); extracting cp%CONDA_PY% win_arm64 code wheel without Python. @@ -29,12 +22,9 @@ if errorlevel 1 ( echo Extracting "!CODE_WHL!" into "%SP%" tar -xf "!CODE_WHL!" -C "%SP%" if errorlevel 1 exit /b 1 - REM No arm64 Windows build of mssql_py_core exists yet, so the win-arm64 code wheel - REM bundles the x64 (win_amd64) one. An x64 .pyd cannot load on arm64 -- it would - REM crash bulk copy AND fails the PE-arch assert -- so REMOVE it from the win-arm64 - REM package. Bulk copy then raises a clean "not available" error (the import is lazy); - REM the rest of the DBAPI is unaffected (ddbc_bindings is native arm64). Restore once - REM an arm64 mssql_py_core ships in the feed. + REM The win-arm64 wheel bundles the x64 mssql_py_core (no arm64 build exists yet); an + REM x64 .pyd can't load on arm64 and fails the PE-arch assert, so strip it. Bulk copy + REM then raises a clean "not available" error (lazy import); the rest of the DBAPI works. echo Removing x64 mssql_py_core from the win-arm64 package; bulk copy is unavailable on win-arm64 until an arm64 build ships. if exist "%SP%\mssql_py_core" rmdir /s /q "%SP%\mssql_py_core" if exist "%SP%\mssql_py_core.libs" rmdir /s /q "%SP%\mssql_py_core.libs" @@ -43,9 +33,8 @@ if errorlevel 1 ( if errorlevel 1 exit /b 1 ) -REM Extract the python-agnostic py3-none-win odbc wheel into the SAME site-packages -REM so mssql_python_odbc\libs\ sits beside mssql_python\ (the loader finds the driver -REM there). WHEELS_DIR is staged per-target, so a single matching odbc wheel is present. +REM Extract the py3-none-win odbc wheel into the SAME site-packages so +REM mssql_python_odbc\libs\ sits beside mssql_python\ and the loader finds the driver. set "ODBC_WHL=" for %%W in ("%WHEELS_DIR%\mssql_python_odbc-%MSSQL_ODBC_VERSION%-py3-none-win_*.whl") do if exist "%%~fW" set "ODBC_WHL=%%~fW" if not defined ODBC_WHL ( diff --git a/conda/mssql-python/build.sh b/conda/mssql-python/build.sh index 6385dc26..e4f203bc 100644 --- a/conda/mssql-python/build.sh +++ b/conda/mssql-python/build.sh @@ -1,30 +1,20 @@ #!/bin/bash -# Repackage the prebuilt, ESRP-signed wheel into a conda package (offline). -# PKG_NAME / PKG_VERSION are exported by conda-build; WHEELS_DIR by the pipeline/harness. +# Repackage the prebuilt, signed mssql-python wheel into a conda package (offline) and +# vendor the ODBC Driver 18 payload inside it -- no separate mssql-python-odbc conda +# package. Both the code wheel and the py3-none- odbc wheel land in the SAME +# site-packages, so mssql_python_odbc/libs/ sits beside mssql_python/ and the C++ +# loader finds the driver. conda-build exports PKG_NAME / PKG_VERSION / CONDA_PY; +# WHEELS_DIR (one wheel per target) + MSSQL_ODBC_VERSION come from the pipeline. set -euo pipefail -# Cross-arch (emulated) build: when repackaging the aarch64 wheel on an x86_64 host, -# $PYTHON is the target-arch interpreter and runs under qemu-user. Point qemu at the -# aarch64 glibc loader/libs (installed via libc6-arm64-cross) so it can find -# /lib/ld-linux-aarch64.so.1 instead of aborting with "Could not open". The dir only -# exists on the emulated aarch64 leg; setting the var elsewhere is a harmless no-op. +# Emulated aarch64 leg: $PYTHON is the aarch64 interpreter under qemu-user; point it at +# the aarch64 glibc loader so it doesn't abort. Harmless no-op on every other leg. [ -d /usr/aarch64-linux-gnu ] && export QEMU_LD_PREFIX="${QEMU_LD_PREFIX:-/usr/aarch64-linux-gnu}" -# This package is SELF-CONTAINED (v1.11.0 model): the ODBC Driver 18 payload ships -# INSIDE it, so there is NO separate mssql-python-odbc conda package. We land BOTH -# the code wheel AND the python-agnostic py3-none- odbc wheel in the SAME -# site-packages, so mssql_python_odbc/libs/ sits beside mssql_python/ and the C++ -# loader resolves the driver there. WHEELS_DIR is staged per-target by the pipeline, -# so exactly one matching odbc wheel is present. odbc_ver="${MSSQL_ODBC_VERSION:?MSSQL_ODBC_VERSION not set}" -# The normal path installs with the host-env Python -- native builds, and the -# QEMU-emulated linux-aarch64 leg where the aarch64 Python runs under binfmt. pip -# resolves the correct site-packages for BOTH wheels, so no unzip is needed there. -# The osx-arm64 conda package is CROSS-built on an Intel macOS agent (no reverse -# Rosetta): the arm64 host Python CANNOT execute and pip would abort, so extract -# both wheels (zips) WITHOUT Python -- the same approach the Windows bld.bat uses -# with `tar`. macOS ships `unzip`. The arm64 slice comes from the universal2 wheel; -# conda-build still stamps osx-arm64. +# Native / QEMU-emulated legs: the host Python runs, so pip installs both wheels. Cross +# osx-arm64 (built on Intel): the arm64 Python can't execute, so extract both wheels +# (zips) with unzip instead -- same approach as the Windows bld.bat. if "$PYTHON" -c "import sys" >/dev/null 2>&1; then "$PYTHON" -m pip install --no-deps --no-index --find-links "$WHEELS_DIR" "$PKG_NAME==$PKG_VERSION" -vv "$PYTHON" -m pip install --no-deps --no-index --find-links "$WHEELS_DIR" "mssql-python-odbc==$odbc_ver" -vv @@ -33,10 +23,8 @@ else echo "extracting both wheels into \$SP_DIR without running Python." mkdir -p "$SP_DIR" pkg_underscore="${PKG_NAME//-/_}" - # macOS universal2 wheels are cpXY-specific (they carry a compiled ddbc_bindings), - # so filter on the target CONDA_PY -- as bld.bat does for the win_arm64 slice -- to - # never silently grab another interpreter's wheel if WHEELS_DIR ever stages more - # than one. The pipeline stages exactly one today; this is defense-in-depth. + # universal2 wheels are cpXY-specific (compiled ddbc_bindings), so filter on the + # target CONDA_PY to never grab another interpreter's wheel (mirrors bld.bat). code_whl="" for w in "$WHEELS_DIR/${pkg_underscore}-${PKG_VERSION}-cp${CONDA_PY}-"*.whl; do [ -e "$w" ] && { code_whl="$w"; break; } @@ -56,51 +44,41 @@ fi # --------------------------------------------------------------------------- # Linux driver reachability (#563) -- the core fix. # --------------------------------------------------------------------------- -# Declaring krb5/openssl/libltdl as conda deps drops one consistent copy of each -# into $PREFIX/lib, but that is INERT on its own: the vendored ODBC binaries ship -# with a bare DT_RUNPATH=$ORIGIN (no climb), so on a minimal conda base the loader -# never looks in $PREFIX/lib -- it falls through to SYSTEM krb5 (the #563 mixing -# crash) and cannot find libltdl.so.7 at all. Reachability, not declaration, is the -# lever: stamp a PURELY RELATIVE $ORIGIN climb (the ELF twin of the macOS -# @loader_path flow) onto libmsodbcsql* and libodbcinst.so.2 so they resolve THIS -# env's own $PREFIX/lib, location-independently. -# -# SIGNATURE SAFETY: -# The Linux ODBC .so are NOT ESRP code-signed -- the mssql-python-odbc pipeline only -# MALWARE-SCANS them (there is no CodeSign task). Only Windows .dll (Authenticode) -# and macOS .dylib (codesign) are code-signed, and this recipe never patches those -# (the climb is Linux-only). So stamping the relative $ORIGIN climb here breaks no -# signature. If the binaries already carry the EXACT climb (e.g. a future odbc-side -# pre-bake before signing), the patch is a byte-for-byte no-op. -# -# The canonical RUNPATH is exactly "$ORIGIN:$ORIGIN/" -- the patch below emits -# that literal form, and the static audit (eng/scripts/audit_bundled_binaries.py) -# requires the same exact climb entry. -# -# Linux-only by construction: the glob matches nothing in a macOS payload -# (libs/macos/...), so this whole block is a natural no-op on the osx legs. +# Declaring krb5/openssl/libltdl as conda deps drops them in $PREFIX/lib, but that is +# INERT: the vendored ODBC .so ship with a bare DT_RUNPATH=$ORIGIN (no climb), so the +# loader never looks in $PREFIX/lib and falls through to SYSTEM krb5 (#563 crash) or +# can't find libltdl.so.7. Fix: stamp a relative "$ORIGIN:$ORIGIN/" onto +# libmsodbcsql* + libodbcinst.so.2 so they resolve THIS env's $PREFIX/lib. Safe to +# patch -- the Linux .so are malware-scanned, not code-signed (only Windows .dll / +# macOS .dylib are, and those are never touched). Linux-only: the glob is a no-op on +# macOS. audit_bundled_binaries.py asserts the same exact climb. prefix_lib="$PREFIX/lib" shopt -s nullglob have_linux_payload=0 [ -d "$SP_DIR/mssql_python_odbc/libs/linux" ] && have_linux_payload=1 -drivers_seen=0 +# Count the driver (libmsodbcsql) and the driver manager (libodbcinst) separately so a +# payload missing EITHER fails loudly -- a lone libodbcinst would ship no SQL driver. +msodbc_seen=0 +odbcinst_seen=0 for libdir in "$SP_DIR"/mssql_python_odbc/libs/linux/*/*/lib; do - # Compute the EXACT expected climb from THIS driver dir up to $PREFIX/lib (derived - # from the real install layout, never a hard-coded ../ count). + # Exact climb from this driver dir up to $PREFIX/lib (from the real layout, never a + # hard-coded ../ count). climb="$("$PYTHON" -c 'import os,sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))' "$prefix_lib" "$libdir")" want="\$ORIGIN:\$ORIGIN/$climb" for so in "$libdir"/libmsodbcsql-*.so.* "$libdir"/libodbcinst.so.2; do [ -e "$so" ] || continue - drivers_seen=$((drivers_seen + 1)) + case "$(basename "$so")" in + libmsodbcsql-*.so.*) msodbc_seen=$((msodbc_seen + 1)) ;; + libodbcinst.so.2) odbcinst_seen=$((odbcinst_seen + 1)) ;; + esac got="$(patchelf --print-rpath "$so" 2>/dev/null || true)" if [ "$got" = "$want" ]; then echo "RPATH-OK (already baked) $(basename "$so") -> $got" continue fi - # Stamp the exact $ORIGIN climb (safe -- these Linux .so are not code-signed). patchelf --set-rpath "$want" "$so" got="$(patchelf --print-rpath "$so")" - # H2: compare EXACTLY to the intended value, not just "no absolute entry". + # Assert the EXACT intended RUNPATH, not just "no absolute entry". if [ "$got" != "$want" ]; then echo "ERROR: patch did not yield the exact expected RUNPATH ('$got' != '$want')." >&2 exit 1 @@ -109,10 +87,10 @@ for libdir in "$SP_DIR"/mssql_python_odbc/libs/linux/*/*/lib; do done done shopt -u nullglob -# H2: a Linux payload with NO driver found is a bypass hole -- a bare `conda build` -# skipping the orchestrator audit would then ship un-asserted drivers. Fail loudly. -if [ "$have_linux_payload" = "1" ] && [ "$drivers_seen" = "0" ]; then - echo "ERROR: Linux ODBC payload present but no libmsodbcsql*/libodbcinst.so.2 found to assert the #563 climb." >&2 +# A Linux payload missing the driver OR the driver manager is a bypass hole (a bare +# `conda build` skipping the orchestrator audit would ship un-asserted binaries). +if [ "$have_linux_payload" = "1" ] && { [ "$msodbc_seen" = "0" ] || [ "$odbcinst_seen" = "0" ]; }; then + echo "ERROR: Linux payload present but incomplete (libmsodbcsql=$msodbc_seen, libodbcinst.so.2=$odbcinst_seen)." >&2 exit 1 fi -[ "$drivers_seen" -gt 0 ] && echo "LINUX_RPATH_CLIMB_OK" || true +[ "$msodbc_seen" -gt 0 ] && echo "LINUX_RPATH_CLIMB_OK" || true diff --git a/conda/mssql-python/meta.yaml b/conda/mssql-python/meta.yaml index fbcebd40..3b3f25ca 100644 --- a/conda/mssql-python/meta.yaml +++ b/conda/mssql-python/meta.yaml @@ -6,86 +6,52 @@ package: build: number: 0 - # Like the companion, this recipe REPACKAGES a prebuilt wheel (the compiled - # ddbc_bindings extension + bundled runtime); it compiles nothing. conda-build's - # overlinking/overdepending checks target from-source builds and mis-fire on - # vendored binaries (e.g. ddbc_bindings linking the driver that lives in the - # separate companion package), so downgrade both from errors to warnings -- CI - # enables them as errors by default. + # This recipe REPACKAGES a prebuilt, signed wheel -- it compiles nothing -- so + # conda-build's from-source checks mis-fire on the vendored binaries. Downgrade + # overlinking/overdepending to warnings, and disable the relocation / prefix scans + # that would corrupt the signature or fail the pure-repackage packaging step. error_overlinking: false error_overdepending: false - # Like the companion, this recipe vendors PREBUILT, signed binaries (the compiled - # ddbc_bindings extension + bundled VC++ runtime); conda-build must neither rewrite - # nor scan them: - # - binary_relocation: rewriting RPATH / install-name in a signed binary corrupts - # the signature. - # - detect_binary_files_with_prefix: the build-prefix scan over the signed native - # binaries is meaningless for a pure repackage and is the packaging step that - # fails right after "Fixing permissions" on these recipes. binary_relocation: false detect_binary_files_with_prefix: false - # conda-build's .pyc byte-compilation runs the TARGET Python. That target cannot - # execute on the build agent for two CROSS legs: osx-arm64 (built on the Intel/x86 - # macOS agent) and win-arm64 (cross-built on the x64 agent -- Windows has no reverse - # emulation) -- so skip pyc on osx AND win (Python regenerates .pyc at import). The - # native osx-64/win-64 legs are unaffected in practice (pip already wrote .pyc). - # Linux legs are native or QEMU-emulated, so they keep compiling. + # .pyc byte-compilation runs the TARGET Python, which can't execute on the cross legs + # (osx-arm64 built on Intel, win-arm64 on x64). Skip pyc there; Python regenerates it. skip_compile_pyc: - "**/*.py" # [osx or win] - # WHEELS_DIR is exported by the pipeline (or a local harness) and passed into the - # isolated conda-build environment so bld.bat / build.sh can install the prebuilt, - # ESRP-signed wheel from --find-links, fully offline. This mirrors CI: no PyPI. + # Passed into the isolated conda-build env so build.sh / bld.bat can install the + # prebuilt wheel from --find-links offline (WHEELS_DIR) and locate the odbc wheel to + # vendor in (MSSQL_ODBC_VERSION). script_env: - WHEELS_DIR - # MSSQL_ODBC_VERSION lets build.sh / bld.bat locate the matching mssql-python-odbc - # wheel to vendor INTO this package (the driver payload now ships inside). - MSSQL_ODBC_VERSION requirements: - # Linux reachability fix (#563): patchelf stamps a relative $ORIGIN RPATH climb - # onto the vendored ODBC binaries in build.sh so the DECLARED conda - # krb5/openssl/libltdl in $PREFIX/lib are actually REACHABLE. Declaration alone is - # inert -- the driver ships with a bare DT_RUNPATH=$ORIGIN (no climb) and would - # fall through to SYSTEM krb5/libltdl (the exact #563 mixing bug). Build-time only, - # Linux only (macOS uses @loader_path, Windows has no RPATH). build: + # patchelf stamps the $ORIGIN RPATH climb (#563) so the declared conda + # krb5/openssl/libltdl below are actually reachable. Linux-only, build-time only. - patchelf # [linux] host: - python >=3.10 - pip run: - python >=3.10 - # No version floor: on the `microsoft` channel azure-identity / azure-core / msal - # ship as CalVer (e.g. 2026.06.01), so a semver floor like `>=1.12.0` is a - # misleading no-op there (every published build already satisfies it). - - azure-identity - # --- ODBC Driver 18 payload deps (folded in from the former companion) -------- - # The proprietary driver libs now ship INSIDE this package (the v1.11.0 model: - # libs bundled in the wheel), so there is NO separate `mssql-python-odbc` conda - # package and its declared, security-serviced deps live here instead. - # - # OpenSSL: the driver dlopen's libssl/libcrypto for TLS (Encrypt=yes). Because it - # is dlopen'd (not an ELF NEEDED) conda-build's overlinking can't see it, so it - # must be declared. Linux-only, pinned >=3,<4 (Driver 18 supports the OpenSSL - # 1.1/3.0 ABI only; conda-forge has begun shipping openssl 4). macOS is excluded - # (the signed dylib dlopen's OpenSSL from a hardcoded Homebrew path -- users - # `brew install openssl`); Windows uses SChannel. + # Mirror the wheel's floor: conda does NOT inherit the wheel's install_requires, so + # without this a solve against conda-forge could pick azure-identity <1.12.0. + - azure-identity >=1.12.0 + # ODBC Driver 18 payload deps (the driver ships inside this package, so its + # security-serviced deps are declared here). OpenSSL for TLS is dlopen'd, so + # overlinking can't see it; pinned <4 (Driver 18 supports the OpenSSL 1.1/3.0 ABI + # only). macOS uses Homebrew OpenSSL, Windows SChannel. - openssl >=3,<4 # [linux] - # Kerberos: libmsodbcsql NEEDs libkrb5.so.3 + libgssapi_krb5.so.2 on Linux. macOS - # uses Kerberos.framework and Windows uses SSPI, so krb5 is Linux-only. + # libmsodbcsql NEEDs libkrb5 / libgssapi_krb5 on Linux (macOS framework, Windows SSPI). - krb5 # [linux] - # libltdl: libodbcinst.so.2 (the unixODBC driver manager the ODBC driver loads - # through) NEEDs libltdl.so.7, which is NOT bundled on Linux. Declare libtool - # (which provides libltdl.so.7); the $ORIGIN RPATH climb in build.sh makes the - # driver resolve THIS env's copy instead of failing "libltdl.so.7 not found". - # Linux-only (macOS bundles libltdl.7.dylib; Windows has no ltdl). + # libodbcinst.so.2 NEEDs libltdl.so.7 (not bundled); libtool provides it and the + # #563 climb makes the driver resolve THIS env's copy. - libtool # [linux] - # Windows VC++ runtime: msodbcsql18.dll imports VCRUNTIME140.dll, but the vendored - # vcredist ships only msvcp140.dll. Declare the security-serviced conda runtime. + # msodbcsql18.dll imports VCRUNTIME140.dll but the vendored vcredist ships only + # msvcp140.dll; declare the serviced conda runtime. - vc14_runtime # [win] - # conda drops the wheel's platform tag (manylinux_2_28 / macosx_15_0), so - # re-assert that floor as a virtual-package run constraint. The bundled wheels - # already required these, so this is never stricter than what shipped. + # Re-assert the wheel's platform floor (conda drops the wheel tag); never stricter. - __glibc >=2.28 # [linux] - __osx >=15.0 # [osx] @@ -95,16 +61,12 @@ test: about: home: https://github.com/microsoft/mssql-python - # This package ships BOTH the MIT-licensed mssql-python code AND the proprietary - # Microsoft ODBC Driver 18 payload (+ the bundled VC++ runtime on Windows), so the - # license is the MIT code license AND the Microsoft proprietary EULA. + # Ships MIT code AND the proprietary ODBC Driver 18 payload, so the license is both. license: MIT AND LicenseRef-Microsoft-Proprietary license_file: - ../../LICENSE - # The proprietary EULAs live canonically with the ODBC payload; reference them - # there instead of committing duplicate copies in the recipe. conda-build resolves - # license_file relative to the recipe dir at package time and embeds them in - # info/licenses/. + # The proprietary EULAs live with the ODBC payload; reference them there rather than + # duplicating. conda-build embeds them in info/licenses/. - ../../mssql_python_odbc/licenses/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt - ../../mssql_python_odbc/licenses/MICROSOFT_VISUAL_STUDIO_LICENSE.txt summary: Microsoft driver for Python to interact with SQL Server and Azure SQL. From 1af01b33e1b14949a51876ec401d6b6aa051c0fa Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Mon, 31 Aug 2026 14:32:16 +0530 Subject: [PATCH 3/4] FIX: assert the extracted ddbc_bindings matches the target Python on cross legs The osx-arm64 (built on Intel) and win-arm64 (built on x64) legs extract the wheel without running the target Python, and the pipeline can't import-test them. A real Apple-Silicon test found every osx-arm64 build (py3.11-3.14) shipped the cp310 ddbc_bindings, so only 3.10 could import. The cp${CONDA_PY} wheel filter already picks the right wheel; this adds a static post-extract assert that the packaged mssql_python/ddbc_bindings.cp-* actually exists -- the python-tag twin of the existing win-arm64 PE-arch assert -- so a wrong-Python binding fails the build instead of the user's import. --- conda/mssql-python/bld.bat | 7 +++++++ conda/mssql-python/build.sh | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/conda/mssql-python/bld.bat b/conda/mssql-python/bld.bat index 51b6acea..a6519e0e 100644 --- a/conda/mssql-python/bld.bat +++ b/conda/mssql-python/bld.bat @@ -22,6 +22,13 @@ if errorlevel 1 ( echo Extracting "!CODE_WHL!" into "%SP%" tar -xf "!CODE_WHL!" -C "%SP%" if errorlevel 1 exit /b 1 + REM win-arm64 cross can't run the arm64 Python, so statically prove the extracted + REM binding is for THIS interpreter (the osx-arm64 twin bug shipped a cp310 .so in + REM every build); a wrong-Python .pyd would only fail at the user's import. + if not exist "%SP%\mssql_python\ddbc_bindings.cp%CONDA_PY%-*.pyd" ( + echo ERROR: extracted "!CODE_WHL!" has no mssql_python\ddbc_bindings.cp%CONDA_PY% pyd ^(wrong-Python binding^). + exit /b 1 + ) REM The win-arm64 wheel bundles the x64 mssql_py_core (no arm64 build exists yet); an REM x64 .pyd can't load on arm64 and fails the PE-arch assert, so strip it. Bulk copy REM then raises a clean "not available" error (lazy import); the rest of the DBAPI works. diff --git a/conda/mssql-python/build.sh b/conda/mssql-python/build.sh index e4f203bc..64d94ab4 100644 --- a/conda/mssql-python/build.sh +++ b/conda/mssql-python/build.sh @@ -37,6 +37,14 @@ else [ -n "$odbc_whl" ] || { echo "ERROR: no mssql_python_odbc==$odbc_ver py3-none wheel in '$WHEELS_DIR'" >&2; exit 1; } echo "Extracting '$code_whl' -> '$SP_DIR'" unzip -oq "$code_whl" -d "$SP_DIR" + # osx-arm64 cross can't run the arm64 Python, so statically prove the extracted + # binding is for THIS interpreter -- a cpXY ddbc_bindings for another Python (the bug + # where every osx-arm64 build shipped the cp310 .so) would only fail at the user's + # import. The python-tag twin of the win-arm64 PE-arch assert. + ls "$SP_DIR"/mssql_python/ddbc_bindings.cp${CONDA_PY}-*.so >/dev/null 2>&1 || { + echo "ERROR: '$code_whl' has no mssql_python/ddbc_bindings.cp${CONDA_PY}-*.so (wrong-Python binding)." >&2 + exit 1 + } echo "Extracting '$odbc_whl' -> '$SP_DIR'" unzip -oq "$odbc_whl" -d "$SP_DIR" fi From f24b366790f8f450cc311cb4bfbd13519663a054 Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Mon, 31 Aug 2026 17:07:40 +0530 Subject: [PATCH 4/4] FIX: revert host/run python to bare `python` (>=3.10 range broke conda-build) The `python >=3.10` floor added in review broke conda-build's per-Python variant: a version range in `host: python` overrides the `--python` matrix, so every `--python 3.10..3.14` build collapsed into ONE package pinned to python_abi cp (build string `py>=310`). Every leg (win-64/arm64, osx-64/arm64, linux-64/aarch64) then failed validation with: package mssql-python-1.14.0-py314_0 requires python_abi 3.14.* *_cp314 ... python =3.10 is not installable `host: python` must be bare so conda-build binds the per-Python variant. The >=3.10 floor is enforced implicitly -- only py310-py314 are built and each package's python_abi pins its exact minor. Verified with `conda-build --output`: --python 3.10 -> py310_0, --python 3.13 -> py313_0. azure-identity >=1.12.0 (a real run floor) is kept. --- conda/mssql-python/meta.yaml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/conda/mssql-python/meta.yaml b/conda/mssql-python/meta.yaml index 3b3f25ca..0f9630d4 100644 --- a/conda/mssql-python/meta.yaml +++ b/conda/mssql-python/meta.yaml @@ -31,12 +31,19 @@ requirements: # krb5/openssl/libltdl below are actually reachable. Linux-only, build-time only. - patchelf # [linux] host: - - python >=3.10 + # Bare `python` (NO version range): conda-build binds the per-Python variant from the + # --python matrix here, so each build emits the matching py3XX package. A range such + # as `python >=3.10` collapses ALL --python builds into ONE cp package (build + # string "py>=310") whose python_abi is pinned to 3.14, so `conda create python=3.10 + # mssql-python` can't solve. The >=3.10 floor is enforced implicitly: only py310-py314 + # are built and each package's python_abi pins its exact minor. + - python - pip run: - - python >=3.10 - # Mirror the wheel's floor: conda does NOT inherit the wheel's install_requires, so - # without this a solve against conda-forge could pick azure-identity <1.12.0. + # Bare `python` for the same reason as host (the floor is the per-build python_abi, + # NOT a range on python -- a range breaks the per-Python solve). + - python + # conda does NOT inherit the wheel's install_requires, so pin azure-identity here. - azure-identity >=1.12.0 # ODBC Driver 18 payload deps (the driver ships inside this package, so its # security-serviced deps are declared here). OpenSSL for TLS is dlopen'd, so