From c1df4bad66084d6a9a5b397ba425401dc3a1499a Mon Sep 17 00:00:00 2001 From: Arcadiy Ivanov Date: Thu, 19 Sep 2024 14:21:26 -0400 Subject: [PATCH] Support `require-libpython` in the wheel If we're in a user-local install or a venv and we have no libpython SOs linked, create symlinks and fail if we can't Release 0.0.6 --- .github/exclusions.yaml | 22 ++ .github/workflows/build.yml | 271 +++++++++--------- .github/workflows/template.yml | 91 ++++++ README.md | 9 +- build.py | 3 +- .../python/instrumented_axle_tests.py | 3 +- .../python/require_libpython_axle_tests.py | 114 ++++++++ ...st_axle_2_libpython-0.0.1-py3-none-any.whl | Bin 0 -> 3281 bytes .../python/wheel_axle/runtime/__init__.py | 3 +- src/main/python/wheel_axle/runtime/_common.py | 8 +- .../python/wheel_axle/runtime/_libpython.py | 97 +++++++ .../python/wheel_axle/runtime/_symlinks.py | 4 +- .../python/wheel_axle/runtime/constants.py | 1 + 13 files changed, 476 insertions(+), 150 deletions(-) create mode 100644 .github/exclusions.yaml create mode 100644 .github/workflows/template.yml create mode 100644 src/integrationtest/python/require_libpython_axle_tests.py create mode 100644 src/integrationtest/python/test_axle_2_libpython-0.0.1-py3-none-any.whl create mode 100644 src/main/python/wheel_axle/runtime/_libpython.py diff --git a/.github/exclusions.yaml b/.github/exclusions.yaml new file mode 100644 index 0000000..09f06e0 --- /dev/null +++ b/.github/exclusions.yaml @@ -0,0 +1,22 @@ +- 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' + setuptools-version: '65.7' +- python-version: '3.13' + setuptools-version: '64.0' +- python-version: '3.13' + setuptools-version: '63.4' +- python-version: '3.13' + setuptools-version: '62.6' +- python-version: '3.13-dev' + pip-version: '22.3' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9311010..942b2aa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,154 +7,145 @@ 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 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 }} + - run: | + { + 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: | diff --git a/.github/workflows/template.yml b/.github/workflows/template.yml new file mode 100644 index 0000000..69a39fe --- /dev/null +++ b/.github/workflows/template.yml @@ -0,0 +1,91 @@ +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: + - python-version: '3.12' + setuptools-version: '65.7' + - python-version: '3.12' + pip-version: '22.3' + 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..bb7d98a --- /dev/null +++ b/src/integrationtest/python/require_libpython_axle_tests.py @@ -0,0 +1,114 @@ +# -*- 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 + + +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): + 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): + return + + 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")))) + + def test_install_uninstall(self): + self.install(self.wheel_file) + self.uninstall(self.wheel_file) + + 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(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 0000000000000000000000000000000000000000..33891d623281db324656c50fb6923c1437a98f81 GIT binary patch literal 3281 zcma);3p`YL8^^~jOv&U!$t4qI$7RO2q^wIJhNdytsZrT?(2fja1~W3TXjCpM(vI88 zr8F+a%EA;Bs~(9;^uRK=|QI<(I_-Z7v;nBXl+9_ z>u5S4I&5&fD11S)?H^j2Jgt^CTxpYPc2eRmNy(g333=B~rf@h($v5&SJTZvHFbN~w1 zai`OPk{NVtYsU+%c5x8bi2gfW65xpo%Jm?-{y_F)=zKowqC8q{aK~+tdZHk(($MU4 zC;;UbjRs=Vf*EK1C_YSv&gZkPmL2MeHo^hDp38quk3H2(JB1*ShawONYBMf{!9-GM z?sOfT6~X+dIl(-J(;M4^hJ{Z}%T7b~32|$R^YSzZnlfjehMx>FK6F0{J7hbd8&Q6D z+m47r!bF<%E2WT+>E~n$?9oeB?TTg4?CyOFY-J7D-#-r(XE9UC+510VbVlf2zE!dp@gr|a^@9nC?S~9C#zs0bFk^@d5j5iaI`yRY@Qn7DU1A5l9CsBY$C}~py&q*L z73J)!lqqWp!Mvt`1s{-UfLZ^gtD3 z%H^hojZ1H%t7c-xoSs_P?w@E}s+lq!P`xtgh9t&}DmU!YObjfIoc{StXf|m_U-8+4 zRaOt!4Qxy()bbssHo~hgtvv>nZ`;CXq~$+E)~JS*u}kvopGm!z{<37C%GFUIcuVtV z6N-A)YyG6)Z@_xmL4h~GkL>GD@gpNwM;#b@)n#Vb6~Mg!V$%V%`ra`vcvC4fF9wRq zV#dVqpcmn=%TwlbxABgxEh*t)+=`&w@@vl~>(Q^;T;(Hhd$J&MLhx#9;&?s#9POrlSMpzechqyqM3n$0n7@oFx0txKeg}Uf<;IA^8rV)fQ)#G`(No5vB=B_)>sTiCBqXv$;#f}P9KA|0;R z9fp+}g$F0+>>Xn{CS+>2$izwDMH6o>+XqXl`LeCMY1SoMO#xarao&top9T`$d; zLd4?bm);iX<0Q|KcALaA#;#PiWo3EE6?pv}LS1M+aW6Nwy!2}D?Yq`{Y02Al>gR*@ zlbw&ZR{T}jbnK?H-LyoMGp5wp_quz&JH$XBBH=GA{BC+99-p z_nnbb_@GH7^gxsnvAkNZS-r4YekZb@=N;`*?W|&)Lz#3ReEWwQQ50VLZSyN(jl<_^ z-#tp-@+iaqSn+(pJnpK$ReIuX4y#oqwo+o$PkFSvJ2}-O+NRIYAXVxPHLxny-}mh~ zJM!&1cSp%!^p6fha%>}-&+=ip4B=F9?XAsy$dUYPwLLE|6xm+Fcvw}X)Z!A;2%1@~ zj?l4CJX_W?8uo4$Xjma(S&{$mL4fx9w76{kX`==P9Lajv*9^vI6a>O$`v6}-b_0Af z*lw)doUutIUpF6MKG=4w@#h5L|HFiU`C#|4!k=K<1Kweyo%~ck$cwC}f6are(7Ei~ z&FHIxX2p?!*bhiXT!=@*`L%54CaGY3@2Y@Ru-2X1dKAlz$==w5u2|$D~ro+ z5t#iC1p+4lE6`dJxR{`SV7(rJ`C!3V Sr46zT_!t2@9L9IqkpBU||AH_8 literal 0 HcmV?d00001 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..91df94d --- /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 `--shared` enabled." + ) + 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(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 '--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"