diff --git a/.github/packaging/specs-python.wxs.in b/.github/packaging/specs-python.wxs.in
index 1cbf1c1..8d3fabc 100644
--- a/.github/packaging/specs-python.wxs.in
+++ b/.github/packaging/specs-python.wxs.in
@@ -13,17 +13,27 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
+
+
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..df64744 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -83,18 +83,34 @@ 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}')")
+ 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.12 -c "import sys; print(sys.platlibdir)")
SRCDIR=rpmbuild/SOURCES/specs-${SPECS_VERSION#v}
- mkdir -p ${SRCDIR}/python/lib/python${PYVER}
- cp -r /usr/lib/python${PYVER}/* ${SRCDIR}/python/lib/python${PYVER}/
+ 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: |
+ 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 +167,51 @@ 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: 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: |
@@ -320,9 +375,76 @@ 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
+ - name: Harvest Python runtime files (generate WiX fragment)
+ shell: pwsh
+ run: |
+ # Enumerate every file under msi-stage\ except specs.exe and emit a
+ # 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) {
+ # 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.Add(' ')
+ $lines.Add(' ')
+ $lines.Add('')
+ $lines | Set-Content -Encoding UTF8 msi-stage.wxs
+
- name: Create WiX source
shell: bash
run: |
@@ -333,7 +455,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
@@ -430,17 +552,29 @@ 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}
- cp -r /usr/lib/python${PYVER}/* deb-root/usr/lib/specs/python/lib/python${PYVER}/
+ 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.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 || \
+ 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/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/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 7bab222..5843d3a 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,31 @@ 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, 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"
+ # 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.
+ # 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:
diff --git a/specs/src/utils/PythonIntf.cc b/specs/src/utils/PythonIntf.cc
index 368a239..cb1d34a 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
@@ -356,15 +357,46 @@ class PythonFunctionCollection : public ExternalFunctionCollection {
return;
}
// 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).
- PyConfig config;
- PyConfig_InitPythonConfig(&config);
- PyConfig_SetBytesString(&config, &config.home, PYTHON_STDLIB_PATH);
- Py_InitializeFromConfig(&config);
- PyConfig_Clear(&config);
+#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).
+ // 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