diff --git a/.github/exclusions.json b/.github/exclusions.json new file mode 100644 index 0000000..38b0146 --- /dev/null +++ b/.github/exclusions.json @@ -0,0 +1,46 @@ +[ + { + "python-version": "3.12", + "setuptools-version": "65.7" + }, + { + "python-version": "3.12", + "setuptools-version": "64.0" + }, + { + "python-version": "3.12", + "setuptools-version": "63.4" + }, + { + "python-version": "3.12", + "setuptools-version": "62.6" + }, + { + "python-version": "3.12", + "pip-version": "22.3" + }, + { + "python-version": "3.13-dev", + "setuptools-version": "65.7" + }, + { + "python-version": "3.13-dev", + "setuptools-version": "65.7" + }, + { + "python-version": "3.13-dev", + "setuptools-version": "64.0" + }, + { + "python-version": "3.13-dev", + "setuptools-version": "63.4" + }, + { + "python-version": "3.13-dev", + "setuptools-version": "62.6" + }, + { + "python-version": "3.13-dev", + "pip-version": "22.3" + } +] \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9311010..09c56cc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,157 +7,151 @@ on: branches: - master jobs: - build-primary: - runs-on: ${{ matrix.os }} - continue-on-error: false - strategy: - fail-fast: false - matrix: - os: - - ubuntu-latest - - macos-13 - python-version: - - '3.12' - - '3.11' - - '3.10' - pip-version: - - '24.2' - - '24.1' - - '24.0' - - '23.3' - - '22.3' - setuptools-version: - - '72.1' - - '71.1' - - '70.3' - - '69.5' - - '68.2' - - '67.8' - - '66.1' - - '65.7' - exclude: - - python-version: '3.12' - setuptools-version: '65.7' - - python-version: '3.12' - pip-version: '22.3' - env: - DEPLOY_PYTHONS: "3.12" - DEPLOY_OSES: "Linux" - DEPLOY_PIPS: "24.2" - DEPLOY_SETUPTOOLS: "72.1" - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - steps: - - uses: actions/checkout@v4 - - shell: bash - run: | - echo "PYB_EXTRA_ARGS=--no-venvs" >> $GITHUB_ENV - echo "SETUPTOOLS_VER=~=${{matrix.setuptools-version}}" >> $GITHUB_ENV - echo "PIP_VER=~=${{matrix.pip-version}}" >> $GITHUB_ENV - - shell: bash - if: | - github.event_name == 'push' && - contains(env.DEPLOY_OSES, runner.os) && - contains(env.DEPLOY_PYTHONS, matrix.python-version) && - contains(env.DEPLOY_PIPS, matrix.pip-version) && - contains(env.DEPLOY_SETUPTOOLS, matrix.setuptools-version) - run: | - echo "PYB_EXTRA_ARGS=+upload --no-venvs" >> $GITHUB_ENV - - uses: pybuilder/build@master - with: - checkout: false - with-venv: false - python-version: ${{ matrix.python-version }} - pyb-extra-args: ${{ env.PYB_EXTRA_ARGS }} - - build-secondary: - runs-on: ${{ matrix.os }} - continue-on-error: false - strategy: - fail-fast: false - matrix: - os: - - ubuntu-latest - - macos-13 - python-version: - - '3.9' - - '3.8' - pip-version: - - '24.2' - - '24.1' - - '24.0' - - '23.3' - - '22.3' - setuptools-version: - - '72.1' - - '71.1' - - '70.3' - - '69.5' - - '68.2' - - '67.8' - - '66.1' - - '65.7' - - '64.0' - - '63.4' - - '62.6' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + read-exclusions: + runs-on: ubuntu-latest + outputs: + BUILD_EXCLUSIONS: ${{ steps.read-exclusions-step.outputs.BUILD_EXCLUSIONS }} steps: - uses: actions/checkout@v4 - - shell: bash + - id: read-exclusions-step run: | - echo "PYB_EXTRA_ARGS=--no-venvs" >> $GITHUB_ENV - echo "SETUPTOOLS_VER=~=${{matrix.setuptools-version}}" >> $GITHUB_ENV - echo "PIP_VER=~=${{matrix.pip-version}}" >> $GITHUB_ENV - - uses: pybuilder/build@master - with: - checkout: false - with-venv: false - python-version: ${{ matrix.python-version }} - pyb-extra-args: ${{ env.PYB_EXTRA_ARGS }} + set -xeEu + set -o pipefail + { + echo 'BUILD_EXCLUSIONS<> "$GITHUB_OUTPUT" - build-experimental: - runs-on: ${{ matrix.os }} - continue-on-error: true - strategy: - fail-fast: false - matrix: - os: - - ubuntu-latest - - macos-13 - python-version: - - '3.13-dev' - pip-version: - - '24.2' - setuptools-version: - - '72.1' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - steps: - - uses: actions/checkout@v4 - - shell: bash - run: | - echo "PYB_EXTRA_ARGS=--no-venvs" >> $GITHUB_ENV - echo "SETUPTOOLS_VER=~=${{matrix.setuptools-version}}" >> $GITHUB_ENV - echo "PIP_VER=~=${{matrix.pip-version}}" >> $GITHUB_ENV - - uses: pybuilder/build@master - with: - checkout: false - with-venv: false - python-version: ${{ matrix.python-version }} - pyb-extra-args: ${{ env.PYB_EXTRA_ARGS }} + build-ubuntu-py312: + uses: ./.github/workflows/template.yml + with: + os: ubuntu-latest + python-version: '3.12' + deploy: ${{ github.event_name == 'push' }} + deploy-pip: '24.2' + deploy-setuptools: '75.1' + exclude: ${{ needs.read-exclusions.outputs.BUILD_EXCLUSIONS }} + secrets: inherit + needs: [ read-exclusions ] + build-macos-py312: + uses: ./.github/workflows/template.yml + with: + os: macos-13 + python-version: '3.12' + exclude: ${{ needs.read-exclusions.outputs.BUILD_EXCLUSIONS }} + secrets: inherit + needs: [ build-ubuntu-py312, read-exclusions ] + build-ubuntu-py311: + uses: ./.github/workflows/template.yml + with: + os: ubuntu-latest + python-version: '3.11' + exclude: ${{ needs.read-exclusions.outputs.BUILD_EXCLUSIONS }} + secrets: inherit + needs: [ read-exclusions ] + build-macos-py311: + uses: ./.github/workflows/template.yml + with: + os: macos-13 + python-version: '3.11' + exclude: ${{ needs.read-exclusions.outputs.BUILD_EXCLUSIONS }} + secrets: inherit + needs: [ build-ubuntu-py311, read-exclusions ] + build-ubuntu-py310: + uses: ./.github/workflows/template.yml + with: + os: ubuntu-latest + python-version: '3.10' + exclude: ${{ needs.read-exclusions.outputs.BUILD_EXCLUSIONS }} + secrets: inherit + needs: [ read-exclusions ] + build-macos-py310: + uses: ./.github/workflows/template.yml + with: + os: macos-13 + python-version: '3.10' + exclude: ${{ needs.read-exclusions.outputs.BUILD_EXCLUSIONS }} + secrets: inherit + needs: [ build-ubuntu-py310, read-exclusions ] + build-ubuntu-py39: + uses: ./.github/workflows/template.yml + with: + os: ubuntu-latest + python-version: '3.9' + exclude: ${{ needs.read-exclusions.outputs.BUILD_EXCLUSIONS }} + secrets: inherit + needs: [ read-exclusions ] + build-macos-py39: + uses: ./.github/workflows/template.yml + with: + os: macos-13 + python-version: '3.9' + exclude: ${{ needs.read-exclusions.outputs.BUILD_EXCLUSIONS }} + secrets: inherit + needs: [ build-ubuntu-py39, read-exclusions ] + build-ubuntu-py38: + uses: ./.github/workflows/template.yml + with: + os: ubuntu-latest + python-version: '3.8' + exclude: ${{ needs.read-exclusions.outputs.BUILD_EXCLUSIONS }} + secrets: inherit + needs: [ read-exclusions ] + build-macos-py38: + uses: ./.github/workflows/template.yml + with: + os: macos-13 + python-version: '3.8' + exclude: ${{ needs.read-exclusions.outputs.BUILD_EXCLUSIONS }} + secrets: inherit + needs: [ build-ubuntu-py38, read-exclusions ] + build-ubuntu-py313: + uses: ./.github/workflows/template.yml + with: + os: ubuntu-latest + python-version: '3.13-dev' + exclude: ${{ needs.read-exclusions.outputs.BUILD_EXCLUSIONS }} + secrets: inherit + needs: [ read-exclusions ] + build-macos-py313: + uses: ./.github/workflows/template.yml + with: + os: macos-13 + python-version: '3.13-dev' + exclude: ${{ needs.read-exclusions.outputs.BUILD_EXCLUSIONS }} + secrets: inherit + needs: [ build-ubuntu-py313, read-exclusions ] build-summary: if: success() || failure() runs-on: ubuntu-latest name: Build Summary needs: - - build-primary - - build-secondary - - build-experimental + - build-ubuntu-py313 + - build-macos-py313 + - build-ubuntu-py312 + - build-macos-py312 + - build-ubuntu-py311 + - build-macos-py311 + - build-ubuntu-py310 + - build-macos-py310 + - build-ubuntu-py39 + - build-macos-py39 + - build-ubuntu-py38 + - build-macos-py38 steps: - name: Check build matrix status if: | - needs.build-primary.result != 'success' || - needs.build-secondary.result != 'success' + needs.build-ubuntu-py313.result != 'success' || + needs.build-macos-py313.result != 'success' || + needs.build-ubuntu-py312.result != 'success' || + needs.build-macos-py312.result != 'success' || + needs.build-ubuntu-py311.result != 'success' || + needs.build-macos-py311.result != 'success' || + needs.build-ubuntu-py310.result != 'success' || + needs.build-macos-py310.result != 'success' || + needs.build-ubuntu-py39.result != 'success' || + needs.build-macos-py39.result != 'success' || + needs.build-ubuntu-py38.result != 'success' || + needs.build-macos-py38.result != 'success' run: exit 1 diff --git a/.github/workflows/template.yml b/.github/workflows/template.yml new file mode 100644 index 0000000..cd39082 --- /dev/null +++ b/.github/workflows/template.yml @@ -0,0 +1,87 @@ +name: wheel-axle-runtime +on: + workflow_call: + inputs: + os: + required: true + type: string + python-version: + required: true + type: string + deploy: + required: false + type: boolean + default: false + deploy-pip: + required: false + type: string + default: none + deploy-setuptools: + required: false + type: string + exclude: + required: false + type: string + secrets: + PYPI_TOKEN: + required: false + +jobs: + build: + runs-on: ${{ matrix.os }} + continue-on-error: false + strategy: + fail-fast: false + matrix: + os: + - ${{ inputs.os }} + python-version: + - ${{ inputs.python-version }} + pip-version: + - '24.2' + - '24.1' + - '24.0' + - '23.3' + - '22.3' + setuptools-version: + - '75.1' + - '74.1' + - '73.0' + - '72.2' + - '71.1' + - '70.3' + - '69.5' + - '68.2' + - '67.8' + - '66.1' + - '65.7' + - '64.0' + - '63.4' + - '62.6' + exclude: ${{ fromJSON(inputs.exclude) }} + env: + DEPLOY_PIP: ${{ inputs.deploy-pip }} + DEPLOY_SETUPTOOLS: ${{ inputs.deploy-setuptools }} + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v4 + - shell: bash + run: | + echo "PYB_EXTRA_ARGS=--no-venvs" >> $GITHUB_ENV + echo "SETUPTOOLS_VER=~=${{matrix.setuptools-version}}" >> $GITHUB_ENV + echo "PIP_VER=~=${{matrix.pip-version}}" >> $GITHUB_ENV + - shell: bash + if: | + inputs.deploy && + contains(env.DEPLOY_PIP, matrix.pip-version) && + contains(env.DEPLOY_SETUPTOOLS, matrix.setuptools-version) + run: | + echo "PYB_EXTRA_ARGS=+upload --no-venvs" >> $GITHUB_ENV + - uses: pybuilder/build@master + with: + checkout: false + with-venv: false + python-version: ${{ matrix.python-version }} + pyb-extra-args: ${{ env.PYB_EXTRA_ARGS }} diff --git a/README.md b/README.md index 1874dbf..67e28bd 100644 --- a/README.md +++ b/README.md @@ -50,14 +50,19 @@ Once the distribution-specific `.pth` is executed by the Python interpreter, the 5. Now that in-process and inter-process race conditions are excluded the post-install work can begin. 6. Registered `installers` are run in sequence. Installers *should be* idempotent. The following installers are currently implemented: - 1. *Symlinks installer* processes `.dist-info/symlinks.txt`, if any. + 1. *LibPython installer* checks for the presence of `.dist-info/require-libpython`. + 1. The installer determines the location of the installation: venv, user or other. + 2. The list of all libpython library files is located from the `sys.base_exec_prefix`. + 3. If the installation is either venv or user and the link to the libpython library doesn't exist the symlink + is created. + 2. *Symlinks installer* processes `.dist-info/symlinks.txt`, if any. 1. Based on the location of the `.pth` file being executed the current installation `schema` and its paths are determined. Currently, installation into a virtual environment or user location is supported and tested. 2. For each symlink the target path is resolved and `realpath` is used to determine the final target path. 3. If the symlink path and symlink target path are within one of the permitted schema locations the symlink is created. Otherwise, an exception is raised and the processing is aborted. 4. After all symlinks are created, the `.dist-info/RECORD` file is updated to reflect the created symlinks. - 2. *Axle installer* finalizes the installation. This installer is always executed last. + 3. *Axle installer* finalizes the installation. This installer is always executed last. 1. The `.dist-info/RECORD` is updated with `.dist-info/axle.done` file record. 2. `.dist-info/axle.done` is created. 3. `.pth` is then removed. If the file cannot be removed it is left in place. diff --git a/build.py b/build.py index af9ea52..9cda6d4 100644 --- a/build.py +++ b/build.py @@ -30,7 +30,7 @@ use_plugin("filter_resources") name = "wheel-axle-runtime" -version = "0.0.6.dev" +version = "0.0.6" summary = "Axle Runtime is the runtime part of the Python Wheel enhancement library" authors = [Author("Karellen, Inc.", "supervisor@karellen.co")] @@ -100,6 +100,7 @@ def set_properties(project): "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Operating System :: MacOS :: MacOS X", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", diff --git a/src/integrationtest/python/instrumented_axle_tests.py b/src/integrationtest/python/instrumented_axle_tests.py index b7a7671..ab64324 100644 --- a/src/integrationtest/python/instrumented_axle_tests.py +++ b/src/integrationtest/python/instrumented_axle_tests.py @@ -23,7 +23,6 @@ import pkg_resources from pip._internal.locations import get_scheme -from pip._internal.utils.virtualenv import virtualenv_no_global class InstrumentedAxleTest(unittest.TestCase): @@ -104,7 +103,7 @@ def test_verify_install(self): self.assertFalse(exists(dist_info)) - @unittest.skipIf(virtualenv_no_global(), "no user site available under virtualenv") + @unittest.skipIf(sys.base_prefix != sys.prefix, "no user site available under virtualenv") def test_verify_user_install(self): self.install(self.wheel_file, True) diff --git a/src/integrationtest/python/require_libpython_axle_tests.py b/src/integrationtest/python/require_libpython_axle_tests.py new file mode 100644 index 0000000..af860d8 --- /dev/null +++ b/src/integrationtest/python/require_libpython_axle_tests.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +# +# (C) Copyright 2022 Karellen, Inc. (https://www.karellen.co/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import site +import sys +import sysconfig +import unittest +from os.path import join as jp, exists, basename, islink + +import pkg_resources +from pip._internal.locations import get_scheme + +from instrumented_axle_tests import InstrumentedAxleTest +from wheel_axle.runtime._common import PLATLIBDIR + + +def is_enabled_shared(): + enable_shared = sysconfig.get_config_var("PY_ENABLE_SHARED") or sysconfig.get_config_var("Py_ENABLE_SHARED") + return enable_shared and int(enable_shared) + + +class RequireLibPythonAxleTest(InstrumentedAxleTest): + def setUp(self) -> None: + super().setUp() + + self.wheel_file = jp(self.test_dir, "test_axle_2_libpython-0.0.1-py3-none-any.whl") + + def check_libpython_present(self, lib_dir): + in_venv = sys.base_exec_prefix != sys.exec_prefix + is_user_site = lib_dir.startswith(site.USER_SITE) + if in_venv or is_user_site: + self.assertTrue(islink(jp(lib_dir, sysconfig.get_config_var("LDLIBRARY")))) + self.assertTrue(islink(jp(lib_dir, sysconfig.get_config_var("INSTSONAME")))) + + @unittest.skipIf(not is_enabled_shared(), "Python isn't compiled with --enable-shared") + def test_install_uninstall(self): + self.install(self.wheel_file) + self.uninstall(self.wheel_file) + + @unittest.skipIf(not is_enabled_shared(), "Python isn't compiled with --enable-shared") + def test_verify_install(self): + self.install(self.wheel_file) + + ws = pkg_resources.WorkingSet() + list(map(ws.add_entry, sys.path)) + pkg = ws.by_key["test-axle-2-libpython"] + scheme = get_scheme("test-axle-2-libpython") + + prefix = scheme.purelib + pth_file = basename(pkg.egg_info[:-len("dist-info")] + "pth") + pth_path = jp(prefix, pth_file) + dist_info = jp(prefix, basename(pkg.egg_info)) + axle_done = jp(dist_info, "axle.done") + + self.assertTrue(exists(pth_path)) + self.assertFalse(exists(axle_done)) + + site.addpackage(prefix, pth_file, None) + + self.assertFalse(exists(pth_path)) + self.assertTrue(exists(axle_done)) + + self.check_installed_contents(scheme) + self.check_libpython_present(jp(scheme.data, PLATLIBDIR)) + + self.uninstall(self.wheel_file) + + self.assertFalse(exists(dist_info)) + + @unittest.skipIf(not is_enabled_shared(), "Python isn't compiled with --enable-shared") + @unittest.skipIf(sys.base_prefix != sys.prefix, "no user site available under virtualenv") + def test_verify_user_install(self): + self.install(self.wheel_file, True) + + ws = pkg_resources.WorkingSet() + list(map(ws.add_entry, sys.path)) + ws.add_entry(site.getusersitepackages()) + pkg = ws.by_key["test-axle-2-libpython"] + scheme = get_scheme("test-axle-2-libpython", True) + + prefix = scheme.purelib + pth_file = basename(pkg.egg_info[:-len("dist-info")] + "pth") + pth_path = jp(prefix, pth_file) + dist_info = jp(prefix, basename(pkg.egg_info)) + axle_done = jp(dist_info, "axle.done") + + self.assertTrue(exists(pth_path)) + self.assertFalse(exists(axle_done)) + + site.addpackage(prefix, pth_file, None) + + self.assertFalse(exists(pth_path)) + self.assertTrue(exists(axle_done)) + + self.check_installed_contents(scheme) + self.check_libpython_present(jp(scheme.data, PLATLIBDIR)) + + self.uninstall(self.wheel_file) + + self.assertFalse(exists(dist_info)) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/integrationtest/python/test_axle_2_libpython-0.0.1-py3-none-any.whl b/src/integrationtest/python/test_axle_2_libpython-0.0.1-py3-none-any.whl new file mode 100644 index 0000000..33891d6 Binary files /dev/null and b/src/integrationtest/python/test_axle_2_libpython-0.0.1-py3-none-any.whl differ diff --git a/src/main/python/wheel_axle/runtime/__init__.py b/src/main/python/wheel_axle/runtime/__init__.py index 4f4370e..cc42088 100644 --- a/src/main/python/wheel_axle/runtime/__init__.py +++ b/src/main/python/wheel_axle/runtime/__init__.py @@ -33,10 +33,11 @@ def _run_installers(dist_info_dir): # Get metadata + from wheel_axle.runtime._libpython import LibPythonInstaller from wheel_axle.runtime._symlinks import SymlinksInstaller from wheel_axle.runtime._axle import AxleFinalizer - installers = [SymlinksInstaller, AxleFinalizer] # AxleFinalizer is always last! + installers = [LibPythonInstaller, SymlinksInstaller, AxleFinalizer] # AxleFinalizer is always last! for installer in installers: installer(dist_info_dir).run() diff --git a/src/main/python/wheel_axle/runtime/_common.py b/src/main/python/wheel_axle/runtime/_common.py index b2c2d78..0a51797 100644 --- a/src/main/python/wheel_axle/runtime/_common.py +++ b/src/main/python/wheel_axle/runtime/_common.py @@ -17,6 +17,7 @@ import csv import os +import sysconfig from email.message import Message from typing import Dict, Set, List, cast, IO @@ -33,6 +34,9 @@ IO = IO +LIBDIR = sysconfig.get_config_var("LIBDIR") +PLATLIBDIR = os.path.basename(LIBDIR) + class Installer: def __init__(self, dist_info_dir: str): @@ -99,5 +103,5 @@ def record_installed(self, """Map archive RECORD paths to installation RECORD paths.""" newpath = _fs_to_record_path(destfile, self.lib_dir) self.installed[srcfile] = newpath - if modified: - self.changed.add(_fs_to_record_path(destfile)) + # if modified: + # self.changed.add(_fs_to_record_path(destfile)) diff --git a/src/main/python/wheel_axle/runtime/_libpython.py b/src/main/python/wheel_axle/runtime/_libpython.py new file mode 100644 index 0000000..db1d9ea --- /dev/null +++ b/src/main/python/wheel_axle/runtime/_libpython.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# +# (C) Copyright 2022 Karellen, Inc. (https://www.karellen.co/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import os +import site +import sys +import sysconfig + +from pip._internal.exceptions import InstallationError + +from wheel_axle.runtime._common import Installer, PLATLIBDIR, LIBDIR +from wheel_axle.runtime.constants import REQUIRE_LIBPYTHON_FILE + + +class LibPythonInstaller(Installer): + def install(self) -> None: + require_libpython_path = os.path.join(self.dist_info_dir, REQUIRE_LIBPYTHON_FILE) + + if not os.path.exists(require_libpython_path): + return + + self._install_libpython() + + def _install_libpython(self): + enable_shared = sysconfig.get_config_var("PY_ENABLE_SHARED") or sysconfig.get_config_var("Py_ENABLE_SHARED") + if not enable_shared or not int(enable_shared): + message = ( + "The distribution {!r} requires dynamic linking to the `libpython` " + "but current instance of CPython was built without `--enable-shared`." + ) + raise InstallationError( + message.format(self.dist_meta.project_name) + ) + + in_venv = sys.base_exec_prefix != sys.exec_prefix + is_user_site = self.lib_dir.startswith(site.USER_SITE) + + # Find libpython library names and locations + shared_library_path = LIBDIR + all_ld_library_names = list(set(n for n in (sysconfig.get_config_var("LDLIBRARY"), + sysconfig.get_config_var("INSTSONAME")) if n)) + + # There are no libraries to link to + if not all_ld_library_names: + message = ( + "The distribution {!r} requires dynamic linking to the `libpython` " + "but was unable to find any libraries declared available in the current installation of CPython, " + "even though it was compiled with '--enable-shared'" + ) + raise InstallationError( + message.format(self.dist_meta.project_name) + ) + + all_ld_library_paths = list(os.path.join(shared_library_path, n) for n in all_ld_library_names) + for p in all_ld_library_paths: + if not os.path.exists(p) or not os.access(p, os.R_OK): + message = ( + "The distribution {!r} requires dynamic linking to the `libpython` " + "but a library {!r} declared available in the current installation of CPython " + "cannot be accessed or doesn't exist" + ) + raise InstallationError( + message.format(self.dist_meta.project_name, p) + ) + + lib_path = None + if is_user_site: + lib_path = os.path.join(site.USER_BASE, PLATLIBDIR) + elif in_venv: + lib_path = os.path.join(sys.exec_prefix, PLATLIBDIR) + + # We're neither in a venv, nor in a user site, i.e. it's an install into Python proper + if not lib_path: + return + + all_ld_library_links = list(os.path.join(lib_path, p) for p in all_ld_library_names) + + # Check is symlinks already exist + for idx, ld_library_link in enumerate(all_ld_library_links): + if os.path.islink(ld_library_link) or os.path.exists(ld_library_link): + # Link or file already exists, so skip to next + continue + os.symlink(all_ld_library_paths[idx], ld_library_link, False) diff --git a/src/main/python/wheel_axle/runtime/_symlinks.py b/src/main/python/wheel_axle/runtime/_symlinks.py index cd84540..a923f16 100644 --- a/src/main/python/wheel_axle/runtime/_symlinks.py +++ b/src/main/python/wheel_axle/runtime/_symlinks.py @@ -162,5 +162,5 @@ def record_installed(self, """Map archive RECORD paths to installation RECORD paths.""" newpath = _fs_to_record_path(destfile, self.lib_dir) self.installed[newpath] = newpath - if modified: - self.changed.add(_fs_to_record_path(destfile)) + # if modified: + # self.changed.add(_fs_to_record_path(destfile)) diff --git a/src/main/python/wheel_axle/runtime/constants.py b/src/main/python/wheel_axle/runtime/constants.py index a6de7d6..6c1b28d 100644 --- a/src/main/python/wheel_axle/runtime/constants.py +++ b/src/main/python/wheel_axle/runtime/constants.py @@ -18,3 +18,4 @@ AXLE_DONE_FILE = "axle.done" AXLE_LOCK_FILE = "axle.lck" SYMLINKS_FILE = "symlinks.txt" +REQUIRE_LIBPYTHON_FILE = "require-libpython"