From f5d5e04da0270eb38e044df99e9d12d0b94bd247 Mon Sep 17 00:00:00 2001 From: niry1 Date: Wed, 17 Jun 2026 13:06:52 +0300 Subject: [PATCH 1/9] Issue #437 - Add static Python 3.12 --- .github/packaging/specs.spec.in | 7 +++++- .github/workflows/release.yml | 25 ++++++++++++++++++-- PYTHON_LICENSE | 42 +++++++++++++++++++++++++++++++++ specs/src/setup.py | 10 ++++++++ specs/src/utils/PythonIntf.cc | 6 ++--- 5 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 PYTHON_LICENSE diff --git a/.github/packaging/specs.spec.in b/.github/packaging/specs.spec.in index 4577ea1..128ec27 100644 --- a/.github/packaging/specs.spec.in +++ b/.github/packaging/specs.spec.in @@ -6,7 +6,7 @@ License: MIT URL: https://github.com/yoavnir/specs2016 Source0: specs-%{version}.tar.gz -# Disable automatic dependency detection since Python is statically linked +# Disable automatic dependency detection since Python is bundled AutoReqProv: no %description @@ -20,12 +20,15 @@ multiple lines into single lines or vice versa. %install mkdir -p %{buildroot}/usr/local/bin mkdir -p %{buildroot}/usr/share/specs +mkdir -p %{buildroot}/usr/share/doc/specs mkdir -p %{buildroot}/usr/lib/specs mkdir -p %{buildroot}/etc/bash_completion.d install -m 755 specs %{buildroot}/usr/local/bin/specs install -m 755 specs-autocomplete %{buildroot}/usr/local/bin/specs-autocomplete install -m 644 specs.1.gz %{buildroot}/usr/share/specs/specs.1.gz install -m 644 specs-completion.bash %{buildroot}/etc/bash_completion.d/specs +install -m 644 docs/LICENSE %{buildroot}/usr/share/doc/specs/LICENSE +install -m 644 docs/PYTHON_LICENSE %{buildroot}/usr/share/doc/specs/PYTHON_LICENSE cp -r python %{buildroot}/usr/lib/specs/ %post @@ -94,4 +97,6 @@ fi /usr/local/bin/specs-autocomplete /usr/share/specs/specs.1.gz /etc/bash_completion.d/specs +/usr/share/doc/specs/LICENSE +/usr/share/doc/specs/PYTHON_LICENSE /usr/lib/specs/python diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c22e70a..da9ac75 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -83,18 +83,29 @@ jobs: cp specs.1.gz rpmbuild/SOURCES/specs-${SPECS_VERSION#v}/ cp .github/packaging/specs-completion.bash rpmbuild/SOURCES/specs-${SPECS_VERSION#v}/ - - name: Bundle Python stdlib for RPM + - name: Bundle Python stdlib and shared library for RPM run: | PYVER=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')") SRCDIR=rpmbuild/SOURCES/specs-${SPECS_VERSION#v} mkdir -p ${SRCDIR}/python/lib/python${PYVER} + mkdir -p ${SRCDIR}/python/lib cp -r /usr/lib/python${PYVER}/* ${SRCDIR}/python/lib/python${PYVER}/ + # Copy the shared libpython library + cp -L /usr/lib64/libpython${PYVER}.so.1.0 ${SRCDIR}/python/lib/ 2>/dev/null || \ + cp -L /usr/lib/x86_64-linux-gnu/libpython${PYVER}.so.1.0 ${SRCDIR}/python/lib/ 2>/dev/null || true # Remove unnecessary files to reduce package size find ${SRCDIR}/python/lib/python${PYVER} -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true find ${SRCDIR}/python/lib/python${PYVER} -type f -name "*.pyc" -delete find ${SRCDIR}/python/lib/python${PYVER} -type f -name "*.pyo" -delete rm -rf ${SRCDIR}/python/lib/python${PYVER}/site-packages rm -rf ${SRCDIR}/python/lib/python${PYVER}/dist-packages + + - name: Copy license files for RPM + run: | + SRCDIR=rpmbuild/SOURCES/specs-${SPECS_VERSION#v} + mkdir -p ${SRCDIR}/docs + cp LICENSE ${SRCDIR}/docs/ + cp PYTHON_LICENSE ${SRCDIR}/docs/ - name: Create RPM tarball run: | @@ -151,12 +162,15 @@ jobs: mkdir -p pkg-root/usr/local/bin mkdir -p pkg-root/usr/local/share/man/man1 mkdir -p pkg-root/usr/local/share/zsh/site-functions + mkdir -p pkg-root/usr/local/share/doc/specs cp specs/exe/specs pkg-root/usr/local/bin/ cp specs/exe/specs-autocomplete pkg-root/usr/local/bin/ chmod 755 pkg-root/usr/local/bin/specs chmod 755 pkg-root/usr/local/bin/specs-autocomplete cp specs.1.gz pkg-root/usr/local/share/man/man1/ cp .github/packaging/specs-completion.zsh pkg-root/usr/local/share/zsh/site-functions/_specs + cp LICENSE pkg-root/usr/local/share/doc/specs/ + cp PYTHON_LICENSE pkg-root/usr/local/share/doc/specs/ - name: Prepare pkg scripts run: | @@ -430,11 +444,18 @@ jobs: cp .github/packaging/postinst deb-root/DEBIAN/ cp .github/packaging/postrm deb-root/DEBIAN/ - - name: Bundle Python stdlib for DEB + - name: Bundle Python stdlib and shared library for DEB run: | PYVER=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')") mkdir -p deb-root/usr/lib/specs/python/lib/python${PYVER} + mkdir -p deb-root/usr/share/doc/specs cp -r /usr/lib/python${PYVER}/* deb-root/usr/lib/specs/python/lib/python${PYVER}/ + # Copy the shared libpython library + cp -L /usr/lib/x86_64-linux-gnu/libpython${PYVER}.so.1.0 deb-root/usr/lib/specs/python/lib/ 2>/dev/null || \ + cp -L /usr/lib/libpython${PYVER}.so.1.0 deb-root/usr/lib/specs/python/lib/ 2>/dev/null || true + # Copy license files + install -m 644 LICENSE deb-root/usr/share/doc/specs/LICENSE + install -m 644 PYTHON_LICENSE deb-root/usr/share/doc/specs/PYTHON_LICENSE # Remove unnecessary files to reduce package size find deb-root/usr/lib/specs/python/lib/python${PYVER} -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true find deb-root/usr/lib/specs/python/lib/python${PYVER} -type f -name "*.pyc" -delete diff --git a/PYTHON_LICENSE b/PYTHON_LICENSE new file mode 100644 index 0000000..173f298 --- /dev/null +++ b/PYTHON_LICENSE @@ -0,0 +1,42 @@ +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 + +1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and + the Individual or Organization ("Licensee") accessing and otherwise using this + software ("Python") in source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby + grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, + analyze, test, perform and/or display publicly, prepare derivative works, + distribute, and otherwise use Python alone or in any derivative + version, provided, however, that PSF's License Agreement and PSF's notice of + copyright, i.e., "Copyright © 2001-2023 Python Software Foundation; All Rights + Reserved" are retained in Python alone or in any derivative version + prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on or + incorporates Python or any part thereof, and wants to make the + derivative work available to others as provided herein, then Licensee hereby + agrees to include in any such work a brief summary of the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" basis. + PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF + EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR + WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE + USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON + FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF + MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE + THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material breach of + its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any relationship + of agency, partnership, or joint venture between PSF and Licensee. This License + Agreement does not grant permission to use PSF trademarks or trade name in a + trademark sense to endorse or promote products or services of Licensee, or any + third party. + +8. By copying, installing or otherwise using Python, Licensee agrees + to be bound by the terms and conditions of this License Agreement. diff --git a/specs/src/setup.py b/specs/src/setup.py index 7bab222..d894688 100644 --- a/specs/src/setup.py +++ b/specs/src/setup.py @@ -741,6 +741,10 @@ def python_search(arg): if CFG_python: condcomp = condcomp + " " + python_cflags + "{}PYTHON_VER_{}".format(def_prefix,python_version) \ + "{}PYTHON_FULL_VER={}".format(def_prefix,full_python_version) + + # Determine if we should bundle Python (only on GitHub CI builds) + bundle_python = (os.environ.get("SPECS_BUILD_SOURCE", "local") == "github") and CFG_python + if args.static_link and platform!="NT": # Statically link libpython so the binary works regardless of the # Python version installed on the target system. @@ -761,6 +765,12 @@ def python_search(arg): # Older libpython static archives may not be PIE-compatible, so disable PIE static_pyldflags.append("-no-pie") condlink = condlink + " " + " ".join(static_pyldflags) + elif bundle_python and platform!="NT": + # Bundle the shared libpython and stdlib, use rpath to find them + # Define the path where the bundled stdlib will be installed + condcomp = condcomp + '{}PYTHON_STDLIB_PATH=\\"/usr/lib/specs/python\\"'.format(def_prefix) + # Add rpath so the bundled libpython is found first + condlink = condlink + " -Wl,-rpath,/usr/lib/specs/python/lib " + python_ldflags else: condlink = condlink + " " + python_ldflags else: diff --git a/specs/src/utils/PythonIntf.cc b/specs/src/utils/PythonIntf.cc index 368a239..8fb77f7 100644 --- a/specs/src/utils/PythonIntf.cc +++ b/specs/src/utils/PythonIntf.cc @@ -357,9 +357,9 @@ class PythonFunctionCollection : public ExternalFunctionCollection { } // Initialize Python environment #ifdef PYTHON_STDLIB_PATH - // When Python is statically linked, use PyConfig to set the home - // directory to our bundled stdlib (Py_SetPythonHome was deprecated - // in Python 3.11). + // When Python is bundled (either statically linked or as a shared library + // with rpath), use PyConfig to set the home directory to our bundled stdlib + // (Py_SetPythonHome was deprecated in Python 3.11). PyConfig config; PyConfig_InitPythonConfig(&config); PyConfig_SetBytesString(&config, &config.home, PYTHON_STDLIB_PATH); From aceb2a4b075d788994a58782006d643aec274503 Mon Sep 17 00:00:00 2001 From: niry1 Date: Wed, 17 Jun 2026 13:34:58 +0300 Subject: [PATCH 2/9] Issue #437 - Fix linking to Python library --- specs/src/utils/PythonIntf.cc | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/specs/src/utils/PythonIntf.cc b/specs/src/utils/PythonIntf.cc index 8fb77f7..42a6f8d 100644 --- a/specs/src/utils/PythonIntf.cc +++ b/specs/src/utils/PythonIntf.cc @@ -12,6 +12,7 @@ #include #include #include +#include // Some defines for compatibility #ifdef PYTHON_VER_3 @@ -360,11 +361,17 @@ class PythonFunctionCollection : public ExternalFunctionCollection { // When Python is bundled (either statically linked or as a shared library // with rpath), use PyConfig to set the home directory to our bundled stdlib // (Py_SetPythonHome was deprecated in Python 3.11). - PyConfig config; - PyConfig_InitPythonConfig(&config); - PyConfig_SetBytesString(&config, &config.home, PYTHON_STDLIB_PATH); - Py_InitializeFromConfig(&config); - PyConfig_Clear(&config); + // Only set the home path if the bundled directory actually exists. + bool use_bundled_stdlib = std::filesystem::exists(PYTHON_STDLIB_PATH); + if (use_bundled_stdlib) { + PyConfig config; + PyConfig_InitPythonConfig(&config); + PyConfig_SetBytesString(&config, &config.home, PYTHON_STDLIB_PATH); + Py_InitializeFromConfig(&config); + PyConfig_Clear(&config); + } else { + Py_Initialize(); + } #else Py_Initialize(); #endif From 08fc0a4127f0cb0e8ceec57ff24d9572d7c54172 Mon Sep 17 00:00:00 2001 From: niry1 Date: Thu, 18 Jun 2026 10:08:40 +0300 Subject: [PATCH 3/9] Issue #437 - bundle Python with Mac OS and Windows --- .github/packaging/specs-python.wxs.in | 28 ++++++++---- .github/workflows/release.yml | 63 +++++++++++++++++++++++++++ README.md | 2 +- specs/src/setup.py | 12 +++-- specs/src/utils/PythonIntf.cc | 27 +++++++++++- 5 files changed, 119 insertions(+), 13 deletions(-) diff --git a/.github/packaging/specs-python.wxs.in b/.github/packaging/specs-python.wxs.in index 1cbf1c1..69d91e4 100644 --- a/.github/packaging/specs-python.wxs.in +++ b/.github/packaging/specs-python.wxs.in @@ -13,17 +13,29 @@ - - - - - - + + + + + + + + + + + + + + - + + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index da9ac75..3df4420 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -172,6 +172,42 @@ jobs: cp LICENSE pkg-root/usr/local/share/doc/specs/ cp PYTHON_LICENSE pkg-root/usr/local/share/doc/specs/ + - name: Bundle Python dylib and stdlib for pkg + run: | + PYVER=$(python3.12 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')") + # Resolve the bundled install prefix used by setup.py (-> PYTHON_STDLIB_PATH) + BUNDLE_PREFIX=/usr/local/lib/specs/python + LIBDIR=$(python3.12 -c "import sysconfig; print(sysconfig.get_config_var('LIBDIR'))") + STDLIB=$(python3.12 -c "import sysconfig; print(sysconfig.get_path('stdlib'))") + mkdir -p pkg-root${BUNDLE_PREFIX}/lib/python${PYVER} + # Locate and copy the shared libpython dylib (cp -L resolves any symlink + # into the framework's real Mach-O so the bundled copy is standalone) + SRC_DYLIB="${LIBDIR}/libpython${PYVER}.dylib" + if [ ! -e "${SRC_DYLIB}" ]; then + SRC_DYLIB=$(find "$(python3.12 -c "import sys; print(sys.base_prefix)")" -name "libpython${PYVER}.dylib" | head -n1) + fi + cp -L "${SRC_DYLIB}" pkg-root${BUNDLE_PREFIX}/lib/libpython${PYVER}.dylib + # Copy the standard library (includes lib-dynload extension modules) + cp -R "${STDLIB}/" pkg-root${BUNDLE_PREFIX}/lib/python${PYVER}/ + # Trim files that are not needed at runtime to reduce package size + find pkg-root${BUNDLE_PREFIX}/lib/python${PYVER} -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true + find pkg-root${BUNDLE_PREFIX}/lib/python${PYVER} -type f -name "*.pyc" -delete + rm -rf pkg-root${BUNDLE_PREFIX}/lib/python${PYVER}/site-packages + rm -rf pkg-root${BUNDLE_PREFIX}/lib/python${PYVER}/test + # Repoint the binary at the bundled dylib so it no longer needs system Python + OLDREF=$(otool -L pkg-root/usr/local/bin/specs | awk 'NR>1 && (/libpython'${PYVER}'/ || /Python\.framework/){print $1; exit}') + NEWREF=${BUNDLE_PREFIX}/lib/libpython${PYVER}.dylib + echo "Repointing libpython reference: ${OLDREF} -> ${NEWREF}" + install_name_tool -change "${OLDREF}" "${NEWREF}" pkg-root/usr/local/bin/specs + install_name_tool -id "${NEWREF}" pkg-root${BUNDLE_PREFIX}/lib/libpython${PYVER}.dylib + chmod 755 pkg-root${BUNDLE_PREFIX}/lib/libpython${PYVER}.dylib + # install_name_tool invalidates the (ad-hoc) code signature; re-sign so the + # dynamic loader will accept the binaries on Apple Silicon. + codesign --force --sign - pkg-root${BUNDLE_PREFIX}/lib/libpython${PYVER}.dylib + codesign --force --sign - pkg-root/usr/local/bin/specs + # Sanity check that the reference now points at the bundled copy + otool -L pkg-root/usr/local/bin/specs | grep -i "libpython${PYVER}" + - name: Prepare pkg scripts run: | mkdir -p pkg-scripts @@ -334,6 +370,33 @@ jobs: shell: bash run: cp specs/bin/Release/specs.exe specs-${{ steps.version.outputs.display }}-python312-windows-x64.exe + - name: Stage MSI payload (bundle Python runtime) + shell: bash + run: | + # Use forward slashes so the paths are safe to use inside bash + PREFIX=$(python -c "import sys; print(sys.base_prefix.replace(chr(92), '/'))") + PYVER=$(python -c "import sys; print(f'{sys.version_info.major}{sys.version_info.minor}')") + mkdir -p msi-stage + cp specs/bin/Release/specs.exe msi-stage/ + # The Python DLLs must sit next to specs.exe so Windows loads them from + # the application directory without any system Python installation. + cp "${PREFIX}/python${PYVER}.dll" msi-stage/ + cp "${PREFIX}/python3.dll" msi-stage/ 2>/dev/null || true + cp "${PREFIX}/vcruntime140.dll" msi-stage/ 2>/dev/null || true + cp "${PREFIX}/vcruntime140_1.dll" msi-stage/ 2>/dev/null || true + # The C extension modules (.pyd) and the pure-Python standard library. + cp -r "${PREFIX}/DLLs" msi-stage/DLLs + cp -r "${PREFIX}/Lib" msi-stage/Lib + cp LICENSE msi-stage/LICENSE.txt + cp PYTHON_LICENSE msi-stage/PYTHON_LICENSE.txt + # Trim files that are not needed at runtime to reduce package size + find msi-stage/Lib -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true + rm -rf msi-stage/Lib/site-packages + rm -rf msi-stage/Lib/test + rm -rf msi-stage/Lib/idlelib + rm -rf msi-stage/Lib/tkinter + rm -rf msi-stage/Lib/turtledemo + - name: Install WiX Toolset run: dotnet tool install --global wix --version 4.0.6 diff --git a/README.md b/README.md index 91fa2b1..7433937 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ For detailed build instructions covering Linux, Mac OS, and Windows (both `make` Known Issues ============ * Regular expression grammars other than the default `ECMAScript` don't work except on Mac OS. -* On Windows with Python support, `python312.dll` must be in the path (or Python 3.12 must be installed). +* The Python-enabled Windows MSI bundles its own Python 3.12 runtime, so no system Python is required. The standalone Python-enabled `.exe` (downloaded on its own, outside the MSI) still needs `python312.dll` on the path (or Python 3.12 installed). Contributing ============ diff --git a/specs/src/setup.py b/specs/src/setup.py index d894688..2ead8b0 100644 --- a/specs/src/setup.py +++ b/specs/src/setup.py @@ -766,11 +766,17 @@ def python_search(arg): static_pyldflags.append("-no-pie") condlink = condlink + " " + " ".join(static_pyldflags) elif bundle_python and platform!="NT": - # Bundle the shared libpython and stdlib, use rpath to find them + # Bundle the shared libpython and stdlib, and point the binary at them. + # The install prefix differs per platform: the macOS .pkg installs under + # /usr/local, while the Linux RPM/DEB packages install under /usr. + if sys.platform=="darwin": + bundle_prefix = "/usr/local/lib/specs/python" + else: + bundle_prefix = "/usr/lib/specs/python" # Define the path where the bundled stdlib will be installed - condcomp = condcomp + '{}PYTHON_STDLIB_PATH=\\"/usr/lib/specs/python\\"'.format(def_prefix) + condcomp = condcomp + '{}PYTHON_STDLIB_PATH=\\"{}\\"'.format(def_prefix, bundle_prefix) # Add rpath so the bundled libpython is found first - condlink = condlink + " -Wl,-rpath,/usr/lib/specs/python/lib " + python_ldflags + condlink = condlink + " -Wl,-rpath,{}/lib ".format(bundle_prefix) + python_ldflags else: condlink = condlink + " " + python_ldflags else: diff --git a/specs/src/utils/PythonIntf.cc b/specs/src/utils/PythonIntf.cc index 42a6f8d..cb1d34a 100644 --- a/specs/src/utils/PythonIntf.cc +++ b/specs/src/utils/PythonIntf.cc @@ -357,7 +357,32 @@ class PythonFunctionCollection : public ExternalFunctionCollection { return; } // Initialize Python environment -#ifdef PYTHON_STDLIB_PATH +#if defined(WIN64) + // On Windows the MSI bundles the stdlib next to specs.exe (in a "Lib" + // subdirectory) together with pythonXY.dll, so the program runs without + // any system Python installation. Point Python's home at the executable's + // directory when that bundled layout is present; otherwise fall back to + // the default search (e.g. a developer build using a system Python). + bool bundledStdlibInitialized = false; + { + wchar_t exePath[MAX_PATH]; + DWORD exePathLen = GetModuleFileNameW(NULL, exePath, MAX_PATH); + if (exePathLen > 0 && exePathLen < MAX_PATH) { + std::filesystem::path exeDir = std::filesystem::path(exePath).parent_path(); + if (std::filesystem::exists(exeDir / "Lib")) { + PyConfig config; + PyConfig_InitPythonConfig(&config); + PyConfig_SetString(&config, &config.home, exeDir.wstring().c_str()); + Py_InitializeFromConfig(&config); + PyConfig_Clear(&config); + bundledStdlibInitialized = true; + } + } + } + if (!bundledStdlibInitialized) { + Py_Initialize(); + } +#elif defined(PYTHON_STDLIB_PATH) // When Python is bundled (either statically linked or as a shared library // with rpath), use PyConfig to set the home directory to our bundled stdlib // (Py_SetPythonHome was deprecated in Python 3.11). From fb3ba4a318562daba595a39754c270a9f56d5c7a Mon Sep 17 00:00:00 2001 From: niry1 Date: Thu, 18 Jun 2026 10:22:26 +0300 Subject: [PATCH 4/9] Issue #437 - Fix windows-python build --- .github/packaging/specs-python.wxs.in | 6 ++---- .github/workflows/release.yml | 9 ++++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/packaging/specs-python.wxs.in b/.github/packaging/specs-python.wxs.in index 69d91e4..8d3fabc 100644 --- a/.github/packaging/specs-python.wxs.in +++ b/.github/packaging/specs-python.wxs.in @@ -28,10 +28,8 @@ - - - + system Python installation is required. This component group is harvested + by heat from the msi-stage directory. --> diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3df4420..c9356bf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -400,6 +400,13 @@ jobs: - name: Install WiX Toolset run: dotnet tool install --global wix --version 4.0.6 + - name: Harvest Python runtime files with heat + shell: bash + run: | + wix heat dir msi-stage -o msi-stage.wxs -cg PythonRuntime -dr INSTALLFOLDER \ + -t .github/packaging/heat-transform.xsl \ + -x "msi-stage\specs.exe" + - name: Create WiX source shell: bash run: | @@ -410,7 +417,7 @@ jobs: - name: Build MSI shell: bash - run: wix build -o specs-${{ steps.version.outputs.display }}-python312.msi specs-python.wxs + run: wix build -o specs-${{ steps.version.outputs.display }}-python312.msi specs-python.wxs msi-stage.wxs - name: Upload MSI artifact uses: actions/upload-artifact@v7 From 0d12e007d806a4ea2021ffe3507829463acea924 Mon Sep 17 00:00:00 2001 From: niry1 Date: Thu, 18 Jun 2026 10:32:57 +0300 Subject: [PATCH 5/9] Issue #437 - 2nd attempt - fix windows-python packaging --- .github/workflows/release.yml | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c9356bf..a1a0cc8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -400,12 +400,34 @@ jobs: - name: Install WiX Toolset run: dotnet tool install --global wix --version 4.0.6 - - name: Harvest Python runtime files with heat - shell: bash + - name: Harvest Python runtime files (generate WiX fragment) + shell: pwsh run: | - wix heat dir msi-stage -o msi-stage.wxs -cg PythonRuntime -dr INSTALLFOLDER \ - -t .github/packaging/heat-transform.xsl \ - -x "msi-stage\specs.exe" + # Enumerate every file under msi-stage\ except specs.exe and emit a + # WiX v4 fragment with one Component per file. heat.exe is a separate + # NuGet package in WiX v4 and not available via the dotnet global tool, + # so we generate the fragment ourselves. + $root = "msi-stage" + $files = Get-ChildItem -Path $root -Recurse -File | + Where-Object { $_.Name -ne "specs.exe" } + $lines = @() + $lines += '' + $lines += '' + $lines += ' ' + $lines += ' ' + foreach ($f in $files) { + # Relative path from workspace root (e.g. msi-stage\Lib\os.py) + $rel = $f.FullName.Substring((Get-Location).Path.Length + 1) + # Stable component ID: replace non-alnum chars with underscores + $id = "cmp_" + ($rel -replace '[^A-Za-z0-9]','_') + $lines += " " + $lines += " " + $lines += " " + } + $lines += ' ' + $lines += ' ' + $lines += '' + $lines | Set-Content -Encoding UTF8 msi-stage.wxs - name: Create WiX source shell: bash From c3c721c46b1077dc3cc33973dd8d63199e174caa Mon Sep 17 00:00:00 2001 From: niry1 Date: Thu, 18 Jun 2026 10:45:16 +0300 Subject: [PATCH 6/9] Issue #437 - yet another fix --- .github/workflows/release.yml | 53 +++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a1a0cc8..2e10ddf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -404,29 +404,40 @@ jobs: shell: pwsh run: | # Enumerate every file under msi-stage\ except specs.exe and emit a - # WiX v4 fragment with one Component per file. heat.exe is a separate - # NuGet package in WiX v4 and not available via the dotnet global tool, - # so we generate the fragment ourselves. - $root = "msi-stage" - $files = Get-ChildItem -Path $root -Recurse -File | - Where-Object { $_.Name -ne "specs.exe" } - $lines = @() - $lines += '' - $lines += '' - $lines += ' ' - $lines += ' ' + # WiX v4 fragment with one Component/File per file, each with an + # explicit unique Id derived from the full relative path so that files + # with the same name in different directories (e.g. __init__.py) do not + # collide. heat.exe is a separate NuGet package in WiX v4 and not + # available via the dotnet global tool, so we generate the fragment here. + $root = Resolve-Path "msi-stage" + $rootLen = $root.Path.Length + $files = Get-ChildItem -Path $root -Recurse -File | + Where-Object { $_.Name -ne "specs.exe" } + $lines = [System.Collections.Generic.List[string]]::new() + $lines.Add('') + $lines.Add('') + $lines.Add(' ') + $lines.Add(' ') foreach ($f in $files) { - # Relative path from workspace root (e.g. msi-stage\Lib\os.py) - $rel = $f.FullName.Substring((Get-Location).Path.Length + 1) - # Stable component ID: replace non-alnum chars with underscores - $id = "cmp_" + ($rel -replace '[^A-Za-z0-9]','_') - $lines += " " - $lines += " " - $lines += " " + # Path relative to the workspace root, e.g. msi-stage\Lib\os.py + $rel = $f.FullName.Substring((Get-Location).Path.Length + 1) + # Path relative to msi-stage\, e.g. Lib\os.py (used as Subdirectory) + $sub = $f.FullName.Substring($rootLen + 1) + # Unique XML identifier: replace every non-alphanumeric char with '_' + $id = "f_" + ($sub -replace '[^A-Za-z0-9]', '_') + # Subdirectory of the file relative to msi-stage (empty for top-level) + $dir = Split-Path $sub -Parent + if ($dir) { + $lines.Add(" ") + } else { + $lines.Add(" ") + } + $lines.Add(" ") + $lines.Add(" ") } - $lines += ' ' - $lines += ' ' - $lines += '' + $lines.Add(' ') + $lines.Add(' ') + $lines.Add('') $lines | Set-Content -Encoding UTF8 msi-stage.wxs - name: Create WiX source From 60a44363e07dce5f0bc4d524eea2a4fd41367475 Mon Sep 17 00:00:00 2001 From: niry1 Date: Thu, 18 Jun 2026 12:41:01 +0300 Subject: [PATCH 7/9] Issue #437 - fix Linux paths --- .github/workflows/release.yml | 52 +++++++++++++++++++++-------------- specs/src/setup.py | 6 +++- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2e10ddf..923431d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -86,19 +86,24 @@ jobs: - name: Bundle Python stdlib and shared library for RPM run: | PYVER=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')") + # Use the platlibdir reported by the bundled Python (e.g. "lib" on + # Debian/Ubuntu, "lib64" on Fedora/RHEL-based) so the bundled stdlib + # lands in the directory that libpython will actually search. + PLATLIBDIR=$(python3 -c "import sys; print(sys.platlibdir)") SRCDIR=rpmbuild/SOURCES/specs-${SPECS_VERSION#v} - mkdir -p ${SRCDIR}/python/lib/python${PYVER} - mkdir -p ${SRCDIR}/python/lib - cp -r /usr/lib/python${PYVER}/* ${SRCDIR}/python/lib/python${PYVER}/ - # Copy the shared libpython library - cp -L /usr/lib64/libpython${PYVER}.so.1.0 ${SRCDIR}/python/lib/ 2>/dev/null || \ - cp -L /usr/lib/x86_64-linux-gnu/libpython${PYVER}.so.1.0 ${SRCDIR}/python/lib/ 2>/dev/null || true + mkdir -p ${SRCDIR}/python/${PLATLIBDIR}/python${PYVER} + cp -r /usr/${PLATLIBDIR}/python${PYVER}/* ${SRCDIR}/python/${PLATLIBDIR}/python${PYVER}/ 2>/dev/null || \ + cp -r /usr/lib/python${PYVER}/* ${SRCDIR}/python/${PLATLIBDIR}/python${PYVER}/ + # Copy the shared libpython library into the same platlibdir subtree + cp -L /usr/${PLATLIBDIR}/libpython${PYVER}.so.1.0 ${SRCDIR}/python/${PLATLIBDIR}/ 2>/dev/null || \ + cp -L /usr/lib/x86_64-linux-gnu/libpython${PYVER}.so.1.0 ${SRCDIR}/python/${PLATLIBDIR}/ 2>/dev/null || \ + cp -L /usr/lib/aarch64-linux-gnu/libpython${PYVER}.so.1.0 ${SRCDIR}/python/${PLATLIBDIR}/ 2>/dev/null || true # Remove unnecessary files to reduce package size - find ${SRCDIR}/python/lib/python${PYVER} -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true - find ${SRCDIR}/python/lib/python${PYVER} -type f -name "*.pyc" -delete - find ${SRCDIR}/python/lib/python${PYVER} -type f -name "*.pyo" -delete - rm -rf ${SRCDIR}/python/lib/python${PYVER}/site-packages - rm -rf ${SRCDIR}/python/lib/python${PYVER}/dist-packages + find ${SRCDIR}/python/${PLATLIBDIR}/python${PYVER} -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true + find ${SRCDIR}/python/${PLATLIBDIR}/python${PYVER} -type f -name "*.pyc" -delete + find ${SRCDIR}/python/${PLATLIBDIR}/python${PYVER} -type f -name "*.pyo" -delete + rm -rf ${SRCDIR}/python/${PLATLIBDIR}/python${PYVER}/site-packages + rm -rf ${SRCDIR}/python/${PLATLIBDIR}/python${PYVER}/dist-packages - name: Copy license files for RPM run: | @@ -550,21 +555,26 @@ jobs: - name: Bundle Python stdlib and shared library for DEB run: | PYVER=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')") - mkdir -p deb-root/usr/lib/specs/python/lib/python${PYVER} + # Use the platlibdir reported by the bundled Python so the stdlib lands + # in the directory that libpython will actually search at runtime. + PLATLIBDIR=$(python3 -c "import sys; print(sys.platlibdir)") + mkdir -p deb-root/usr/lib/specs/python/${PLATLIBDIR}/python${PYVER} mkdir -p deb-root/usr/share/doc/specs - cp -r /usr/lib/python${PYVER}/* deb-root/usr/lib/specs/python/lib/python${PYVER}/ - # Copy the shared libpython library - cp -L /usr/lib/x86_64-linux-gnu/libpython${PYVER}.so.1.0 deb-root/usr/lib/specs/python/lib/ 2>/dev/null || \ - cp -L /usr/lib/libpython${PYVER}.so.1.0 deb-root/usr/lib/specs/python/lib/ 2>/dev/null || true + cp -r /usr/${PLATLIBDIR}/python${PYVER}/* deb-root/usr/lib/specs/python/${PLATLIBDIR}/python${PYVER}/ 2>/dev/null || \ + cp -r /usr/lib/python${PYVER}/* deb-root/usr/lib/specs/python/${PLATLIBDIR}/python${PYVER}/ + # Copy the shared libpython library into the same platlibdir subtree + cp -L /usr/lib/x86_64-linux-gnu/libpython${PYVER}.so.1.0 deb-root/usr/lib/specs/python/${PLATLIBDIR}/ 2>/dev/null || \ + cp -L /usr/lib/aarch64-linux-gnu/libpython${PYVER}.so.1.0 deb-root/usr/lib/specs/python/${PLATLIBDIR}/ 2>/dev/null || \ + cp -L /usr/lib/libpython${PYVER}.so.1.0 deb-root/usr/lib/specs/python/${PLATLIBDIR}/ 2>/dev/null || true # Copy license files install -m 644 LICENSE deb-root/usr/share/doc/specs/LICENSE install -m 644 PYTHON_LICENSE deb-root/usr/share/doc/specs/PYTHON_LICENSE # Remove unnecessary files to reduce package size - find deb-root/usr/lib/specs/python/lib/python${PYVER} -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true - find deb-root/usr/lib/specs/python/lib/python${PYVER} -type f -name "*.pyc" -delete - find deb-root/usr/lib/specs/python/lib/python${PYVER} -type f -name "*.pyo" -delete - rm -rf deb-root/usr/lib/specs/python/lib/python${PYVER}/site-packages - rm -rf deb-root/usr/lib/specs/python/lib/python${PYVER}/dist-packages + find deb-root/usr/lib/specs/python/${PLATLIBDIR}/python${PYVER} -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true + find deb-root/usr/lib/specs/python/${PLATLIBDIR}/python${PYVER} -type f -name "*.pyc" -delete + find deb-root/usr/lib/specs/python/${PLATLIBDIR}/python${PYVER} -type f -name "*.pyo" -delete + rm -rf deb-root/usr/lib/specs/python/${PLATLIBDIR}/python${PYVER}/site-packages + rm -rf deb-root/usr/lib/specs/python/${PLATLIBDIR}/python${PYVER}/dist-packages - name: Build DEB run: | diff --git a/specs/src/setup.py b/specs/src/setup.py index 2ead8b0..5f19266 100644 --- a/specs/src/setup.py +++ b/specs/src/setup.py @@ -773,10 +773,14 @@ def python_search(arg): bundle_prefix = "/usr/local/lib/specs/python" else: bundle_prefix = "/usr/lib/specs/python" + # The rpath must match the platlibdir of the Python being bundled + # (e.g. "lib" on Debian/Ubuntu, "lib64" on Fedora/RHEL) so that the + # dynamic linker finds libpython in the correct subdirectory. + platlibdir = sys.platlibdir # Define the path where the bundled stdlib will be installed condcomp = condcomp + '{}PYTHON_STDLIB_PATH=\\"{}\\"'.format(def_prefix, bundle_prefix) # Add rpath so the bundled libpython is found first - condlink = condlink + " -Wl,-rpath,{}/lib ".format(bundle_prefix) + python_ldflags + condlink = condlink + " -Wl,-rpath,{}/{} ".format(bundle_prefix, platlibdir) + python_ldflags else: condlink = condlink + " " + python_ldflags else: From 148dd448b9d03288b4e38526c5e4ab771179c577 Mon Sep 17 00:00:00 2001 From: niry1 Date: Thu, 18 Jun 2026 14:06:00 +0300 Subject: [PATCH 8/9] Issue #437 - Fix bundled python version in Linux --- .github/workflows/release.yml | 8 ++++---- specs/src/setup.py | 7 +++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 923431d..df64744 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,11 +85,11 @@ jobs: - name: Bundle Python stdlib and shared library for RPM run: | - PYVER=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')") + PYVER=$(python3.12 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')") # Use the platlibdir reported by the bundled Python (e.g. "lib" on # Debian/Ubuntu, "lib64" on Fedora/RHEL-based) so the bundled stdlib # lands in the directory that libpython will actually search. - PLATLIBDIR=$(python3 -c "import sys; print(sys.platlibdir)") + PLATLIBDIR=$(python3.12 -c "import sys; print(sys.platlibdir)") SRCDIR=rpmbuild/SOURCES/specs-${SPECS_VERSION#v} mkdir -p ${SRCDIR}/python/${PLATLIBDIR}/python${PYVER} cp -r /usr/${PLATLIBDIR}/python${PYVER}/* ${SRCDIR}/python/${PLATLIBDIR}/python${PYVER}/ 2>/dev/null || \ @@ -554,10 +554,10 @@ jobs: - name: Bundle Python stdlib and shared library for DEB run: | - PYVER=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')") + PYVER=$(python3.12 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')") # Use the platlibdir reported by the bundled Python so the stdlib lands # in the directory that libpython will actually search at runtime. - PLATLIBDIR=$(python3 -c "import sys; print(sys.platlibdir)") + PLATLIBDIR=$(python3.12 -c "import sys; print(sys.platlibdir)") mkdir -p deb-root/usr/lib/specs/python/${PLATLIBDIR}/python${PYVER} mkdir -p deb-root/usr/share/doc/specs cp -r /usr/${PLATLIBDIR}/python${PYVER}/* deb-root/usr/lib/specs/python/${PLATLIBDIR}/python${PYVER}/ 2>/dev/null || \ diff --git a/specs/src/setup.py b/specs/src/setup.py index 5f19266..476c910 100644 --- a/specs/src/setup.py +++ b/specs/src/setup.py @@ -779,8 +779,11 @@ def python_search(arg): platlibdir = sys.platlibdir # Define the path where the bundled stdlib will be installed condcomp = condcomp + '{}PYTHON_STDLIB_PATH=\\"{}\\"'.format(def_prefix, bundle_prefix) - # Add rpath so the bundled libpython is found first - condlink = condlink + " -Wl,-rpath,{}/{} ".format(bundle_prefix, platlibdir) + python_ldflags + # Add rpath so the bundled libpython is found first. + # --disable-new-dtags emits DT_RPATH instead of DT_RUNPATH; DT_RPATH is + # searched before ld.so.cache, ensuring the bundled libpython takes + # precedence over any system-installed libpython3.12 on the target host. + condlink = condlink + " -Wl,--disable-new-dtags,-rpath,{}/{} ".format(bundle_prefix, platlibdir) + python_ldflags else: condlink = condlink + " " + python_ldflags else: From 6e5ab4b8ab2ebbae5880cbacecd7798ec325f9b0 Mon Sep 17 00:00:00 2001 From: niry1 Date: Thu, 18 Jun 2026 14:26:41 +0300 Subject: [PATCH 9/9] Issue #437 - prevent --disable-new-dtags on Mac OS --- specs/src/setup.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/specs/src/setup.py b/specs/src/setup.py index 476c910..5843d3a 100644 --- a/specs/src/setup.py +++ b/specs/src/setup.py @@ -780,10 +780,16 @@ def python_search(arg): # Define the path where the bundled stdlib will be installed condcomp = condcomp + '{}PYTHON_STDLIB_PATH=\\"{}\\"'.format(def_prefix, bundle_prefix) # Add rpath so the bundled libpython is found first. - # --disable-new-dtags emits DT_RPATH instead of DT_RUNPATH; DT_RPATH is - # searched before ld.so.cache, ensuring the bundled libpython takes - # precedence over any system-installed libpython3.12 on the target host. - condlink = condlink + " -Wl,--disable-new-dtags,-rpath,{}/{} ".format(bundle_prefix, platlibdir) + python_ldflags + # On Linux, --disable-new-dtags emits DT_RPATH instead of DT_RUNPATH; + # DT_RPATH is searched before ld.so.cache, ensuring the bundled + # libpython takes precedence over any system-installed libpython3.12. + # macOS uses Apple ld which does not support --disable-new-dtags, and + # does not need it (install_name_tool rewrites the dylib reference). + if sys.platform=="darwin": + rpath_flags = "-Wl,-rpath,{}/lib".format(bundle_prefix) + else: + rpath_flags = "-Wl,--disable-new-dtags,-rpath,{}/{}".format(bundle_prefix, platlibdir) + condlink = condlink + " " + rpath_flags + " " + python_ldflags else: condlink = condlink + " " + python_ldflags else: