diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index dcad568..3beeb18 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,14 +1,17 @@ -name: Upload Package +name: Build and upload package on: + pull_request: release: types: [published] + workflow_dispatch: permissions: contents: read jobs: - release-build: + build-sdist: + name: Build source distribution runs-on: ubuntu-latest timeout-minutes: 15 steps: @@ -20,9 +23,75 @@ jobs: - name: Prepare environment uses: ./.github/actions/prepare - - name: Build release distributions - run: | - uv build -v --sdist + - name: Build source distribution + run: uv build -v --sdist + + - name: Check source distribution + run: uv run twine check dist/* + + - name: Upload source distribution + uses: actions/upload-artifact@v4 + with: + name: distribution-sdist + path: dist/*.tar.gz + if-no-files-found: error + + build-wheel: + name: Build ${{ matrix.build }} + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + build: + - cp312-manylinux_x86_64 + - cp313-manylinux_x86_64 + - cp312-musllinux_x86_64 + - cp313-musllinux_x86_64 + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Build and test wheel + uses: pypa/cibuildwheel@v3.4.1 + with: + package-dir: . + output-dir: wheelhouse + env: + CIBW_BUILD: ${{ matrix.build }} + CIBW_BUILD_VERBOSITY: "1" + CIBW_TEST_COMMAND: python {project}/scripts/verify_wheel.py + + - name: Upload wheel + uses: actions/upload-artifact@v4 + with: + name: distribution-${{ matrix.build }} + path: wheelhouse/*.whl + if-no-files-found: error + + publish: + name: Upload distributions to PyPI + if: github.event_name == 'release' + needs: [build-sdist, build-wheel] + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Prepare environment + uses: ./.github/actions/prepare + + - name: Download distributions + uses: actions/download-artifact@v4 + with: + pattern: distribution-* + path: dist + merge-multiple: true # TODO: switch to PyPI Trusted Publishing (OIDC) and drop the token: # https://docs.pypi.org/trusted-publishers/ @@ -30,5 +99,4 @@ jobs: env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - run: | - uv run twine upload --verbose --repository pypi dist/* + run: uv run twine upload --verbose --repository pypi dist/* diff --git a/scripts/verify_wheel.py b/scripts/verify_wheel.py new file mode 100644 index 0000000..6f31fa3 --- /dev/null +++ b/scripts/verify_wheel.py @@ -0,0 +1,43 @@ +"""Verify that an installed wheel is self-contained and importable.""" + +from importlib import import_module +from pathlib import Path +import shutil +import subprocess + + +EXTENSION_MODULES = ( + "uringloop._liburing", + "uringloop._uringcore_liburing", +) + + +def dynamic_section(module_name: str) -> str: + module = import_module(module_name) + module_file = getattr(module, "__file__", None) + if module_file is None: + raise RuntimeError(f"{module_name} does not have an extension module path") + + readelf = shutil.which("readelf") + if readelf is None: + raise RuntimeError("readelf is required to verify wheel dependencies") + + result = subprocess.run( + [readelf, "-d", Path(module_file)], + check=True, + capture_output=True, + text=True, + ) + return result.stdout + + +def main() -> None: + for module_name in EXTENSION_MODULES: + dependencies = dynamic_section(module_name) + if "liburing.so" in dependencies: + raise RuntimeError(f"{module_name} dynamically links liburing") + print(f"verified {module_name}") + + +if __name__ == "__main__": + main()