diff --git a/.github/workflows/build-distributions.yml b/.github/workflows/build-distributions.yml index 05f7792ce..1cf8a624c 100644 --- a/.github/workflows/build-distributions.yml +++ b/.github/workflows/build-distributions.yml @@ -134,8 +134,8 @@ jobs: fail-fast: false matrix: job: - # - target: x86_64-pc-windows-msvc - # os: windows-2019 + - target: x86_64-pc-windows-msvc + os: windows-2019 - target: aarch64-apple-darwin os: macos-14 - target: x86_64-apple-darwin @@ -160,9 +160,20 @@ jobs: -m pip install ${{ inputs.version && format('hatch=={0}', inputs.version) || '.' }} + - name: Set up Windows trampoline + if: startsWith(matrix.job.os, 'windows-') + run: |- + git clone -q --depth 1 -b relative-trampoline https://github.com/ofek/uv.git + cd uv/crates/uv-trampoline + rustup toolchain install nightly-2024-03-19 + rustup target add ${{ matrix.job.target }} + rustup component add rust-src --toolchain nightly-2024-03-19-${{ matrix.job.target }} + cargo build --release --target ${{ matrix.job.target }} + - name: Make scripts portable - if: startsWith(matrix.job.os, 'macos-') - run: ./python/bin/python release/unix/make_scripts_portable.py + run: >- + ${{ startsWith(matrix.job.os, 'windows-') && '.\\python\\python.exe' || './python/bin/python' }} + release/${{ startsWith(matrix.job.os, 'windows-') && 'windows' || 'unix' }}/make_scripts_portable.py - name: Strip debug symbols if: startsWith(matrix.job.os, 'macos-') diff --git a/.github/workflows/build-hatch.yml b/.github/workflows/build-hatch.yml index 33251a850..87112ad1a 100644 --- a/.github/workflows/build-hatch.yml +++ b/.github/workflows/build-hatch.yml @@ -112,7 +112,7 @@ jobs: # Windows - target: x86_64-pc-windows-msvc os: windows-2022 - # use-dist: true + use-dist: true - target: i686-pc-windows-msvc os: windows-2022 # macOS @@ -514,7 +514,39 @@ jobs: uses: ./.github/workflows/build-distributions.yml # This actually does not need the binary jobs but we want to prioritize # resources for the test jobs therefore this forces these later on - needs: binaries + # needs: binaries + + test-distributions: + name: Test distribution ${{ matrix.job.target }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository + needs: + - distributions-dev + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - target: x86_64-unknown-linux-gnu + os: ubuntu-22.04 + - target: x86_64-pc-windows-msvc + os: windows-2022 + - target: aarch64-apple-darwin + os: macos-14 + - target: x86_64-apple-darwin + os: macos-12 + + steps: + - name: Download distributions + uses: actions/download-artifact@v4 + with: + name: distribution-${{ matrix.job.target }} + + - name: Unpack distribution + run: |- + mkdir out + tar xzf hatch-dist-${{ matrix.job.target }}.tar.gz -C out + - name: Try to run + run: ./out/python/bin/hatch --version distributions-release: name: Build release distributions diff --git a/release/windows/make_scripts_portable.py b/release/windows/make_scripts_portable.py new file mode 100644 index 000000000..5b4d68b82 --- /dev/null +++ b/release/windows/make_scripts_portable.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +import sys +import sysconfig +from contextlib import closing +from importlib.metadata import entry_points +from io import BytesIO +from os.path import relpath +from pathlib import Path +from tempfile import TemporaryDirectory +from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo + +LAUNCHERS_DIR = Path('uv\\crates\\uv-trampoline\\target\\x86_64-pc-windows-msvc\\release').resolve() +SCRIPT_TEMPLATE = """\ +#!{executable} +# -*- coding: utf-8 -*- +import re +import sys +from {module} import {import_name} +if __name__ == "__main__": + sys.argv[0] = re.sub(r"(-script\\.pyw|\\.exe)?$", "", sys.argv[0]) + sys.exit({function}()) +""" + + +def main(): + interpreters_dir = Path(sys.executable).parent + scripts_dir = Path(sysconfig.get_path('scripts')) + + ep = entry_points() + for group, interpreter_name, launcher_name in ( + ('console_scripts', 'python.exe', 'uv-trampoline-console.exe'), + ('gui_scripts', 'pythonw.exe', 'uv-trampoline-gui.exe'), + ): + try: + scripts = ep[group] + except KeyError: + continue + + interpreter = interpreters_dir / interpreter_name + relative_interpreter_path = relpath(interpreter, scripts_dir) + launcher_data = (LAUNCHERS_DIR / launcher_name).read_bytes() + + for script in scripts: + # https://github.com/astral-sh/uv/tree/main/crates/uv-trampoline#how-do-you-use-it + with closing(BytesIO()) as buf: + # Launcher + buf.write(launcher_data) + + # Zipped script + with TemporaryDirectory() as td: + zip_path = Path(td) / 'script.zip' + with ZipFile(zip_path, 'w') as zf: + # Ensure reproducibility + zip_info = ZipInfo('__main__.py', (2020, 2, 2, 0, 0, 0)) + zip_info.external_attr = (0o644 & 0xFFFF) << 16 + + module, _, attrs = script.value.partition(':') + contents = SCRIPT_TEMPLATE.format( + executable=relative_interpreter_path, + module=module, + import_name=attrs.split('.')[0], + function=attrs + ) + zf.writestr(zip_info, contents, compress_type=ZIP_DEFLATED) + + buf.write(zip_path.read_bytes()) + + # Interpreter path + interpreter_path = relative_interpreter_path.encode('utf-8') + buf.write(interpreter_path) + + # Interpreter path length + interpreter_path_length = len(interpreter_path).to_bytes(4, 'little') + buf.write(interpreter_path_length) + + # Magic number + buf.write(b'UVUV') + + script_data = buf.getvalue() + + script_path = scripts_dir / f'{script.name}.exe' + script_path.write_bytes(script_data) + + +if __name__ == '__main__': + main()