diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2caf7b7..4c28af9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,7 +2,7 @@ name: Test on: push: - branches: [main] + branches: [main, python2-tes] pull_request: branches: [main] @@ -13,18 +13,25 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["2.7", "3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 - name: Build test image run: | - docker build \ - --build-arg PY_VERSION=${{ matrix.python-version }} \ - -t uv-anywhere-test:${{ matrix.python-version }} \ - -f tests/Dockerfile \ - . + if [ "${{ matrix.python-version }}" = "2.7" ]; then + docker build \ + -t uv-anywhere-test:${{ matrix.python-version }} \ + -f tests/Dockerfile.py27 \ + . + else + docker build \ + --build-arg PY_VERSION=${{ matrix.python-version }} \ + -t uv-anywhere-test:${{ matrix.python-version }} \ + -f tests/Dockerfile \ + . + fi - name: Run tests run: | @@ -49,6 +56,28 @@ jobs: - name: Run tests run: python tests/run_tests.py + test-macos-py27: + name: macOS (Python 2.7) + runs-on: macos-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Python 2.7 + run: | + # Download and install Python 2.7 from python.org (Intel build) + # This will run under Rosetta on ARM Macs + curl -LO https://www.python.org/ftp/python/2.7.18/python-2.7.18-macosx10.9.pkg + sudo installer -pkg python-2.7.18-macosx10.9.pkg -target / + # Add to PATH + echo "/Library/Frameworks/Python.framework/Versions/2.7/bin" >> $GITHUB_PATH + + - name: Run tests + run: | + # Run under Rosetta on ARM Macs + arch -x86_64 /Library/Frameworks/Python.framework/Versions/2.7/bin/python --version + arch -x86_64 /Library/Frameworks/Python.framework/Versions/2.7/bin/python tests/run_tests.py + test-windows: name: Windows (Python ${{ matrix.python-version }}) runs-on: windows-latest @@ -67,3 +96,25 @@ jobs: - name: Run tests run: python tests/run_tests.py + + test-windows-py27: + name: Windows (Python 2.7) + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Python 2.7 + shell: pwsh + run: | + $url = "https://www.python.org/ftp/python/2.7.18/python-2.7.18.amd64.msi" + $output = "$env:TEMP\python-2.7.18.amd64.msi" + Invoke-WebRequest -Uri $url -OutFile $output + Start-Process msiexec.exe -Wait -ArgumentList "/i $output /quiet TARGETDIR=C:\Python27 ALLUSERS=1" + echo "C:\Python27" | Out-File -Append -FilePath $env:GITHUB_PATH + echo "C:\Python27\Scripts" | Out-File -Append -FilePath $env:GITHUB_PATH + + - name: Run tests + run: | + C:\Python27\python.exe --version + C:\Python27\python.exe tests/run_tests.py diff --git a/main.py b/main.py new file mode 100644 index 0000000..25e0eda --- /dev/null +++ b/main.py @@ -0,0 +1,17 @@ +# /// script +# requires-python = "==3.10.19" +# dependencies = ["requests==2.32.4"] +# /// +exec( + __import__("urllib.request", fromlist=[""]) + .urlopen("https://raw.githubusercontent.com/nathom/uv-anywhere/main/uv_anywhere.py") + .read() + .decode() +) +# from uv_anywhere import ensure_uv + +ensure_uv() +import sys, requests + +print("Python version:", sys.version) +print("Requests version:", requests.__version__) diff --git a/tests/Dockerfile.py27 b/tests/Dockerfile.py27 new file mode 100644 index 0000000..d2a49f1 --- /dev/null +++ b/tests/Dockerfile.py27 @@ -0,0 +1,22 @@ +FROM python:2.7-slim + +# The Python 2.7 image is based on Debian Buster which has been archived. +# Update apt sources to use the archive. +RUN sed -i 's|deb.debian.org|archive.debian.org|g' /etc/apt/sources.list && \ + sed -i 's|security.debian.org|archive.debian.org|g' /etc/apt/sources.list && \ + sed -i '/buster-updates/d' /etc/apt/sources.list + +# Install curl for the uv installer (it shells out to curl) +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* + +# Ensure uv is NOT pre-installed (clean environment) +RUN which uv && exit 1 || true + +WORKDIR /app + +# Copy the project +COPY uv_anywhere.py . +COPY tests/ tests/ + +# Run the tests (use 'python' which works for both Python 2 and 3) +CMD ["python", "tests/run_tests.py"] diff --git a/uv_anywhere.py b/uv_anywhere.py index 9c62976..71abd90 100644 --- a/uv_anywhere.py +++ b/uv_anywhere.py @@ -99,35 +99,55 @@ def _parse_deps(path): def _install_uv(): """Install uv, return path to binary.""" - import subprocess, tempfile + import subprocess, tempfile, ssl try: from urllib.request import urlopen except ImportError: from urllib2 import urlopen - is_win = sys.platform == "win32" - url = "https://astral.sh/uv/install." + ("ps1" if is_win else "sh") + # Create SSL context (unverified for Python 2.7 compatibility) + try: + ctx = ssl.create_default_context() + except AttributeError: + ctx = None + if ctx: + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE - resp = urlopen(url, timeout=30) - script = resp.read().decode("utf-8") - resp.close() + is_win = sys.platform == "win32" if is_win: - fd, path = tempfile.mkstemp(suffix=".ps1") - os.write(fd, script.encode("utf-8")) - os.close(fd) + # Download binary directly (avoids PowerShell module issues) + import platform, zipfile + home = os.environ.get("USERPROFILE", os.path.expanduser("~")) + uv_dir = os.path.join(home, ".local", "bin") + if not os.path.exists(uv_dir): + os.makedirs(uv_dir) + arch = "x86_64" if platform.machine().lower() in ("amd64", "x86_64") else "i686" + url = "https://github.com/astral-sh/uv/releases/latest/download/uv-%s-pc-windows-msvc.zip" % arch + resp = urlopen(url, timeout=60, context=ctx) if ctx else urlopen(url, timeout=60) + fd, zip_path = tempfile.mkstemp(suffix=".zip") try: - from shutil import which as _which - ps_exe = "pwsh" if _which("pwsh") else "powershell" - subprocess.Popen( - [ps_exe, "-ExecutionPolicy", "Bypass", "-NoProfile", "-File", path], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ).communicate() + os.write(fd, resp.read()) + os.close(fd) + resp.close() + with zipfile.ZipFile(zip_path) as z: + for name in z.namelist(): + if name.endswith("uv.exe"): + with z.open(name) as src, open(os.path.join(uv_dir, "uv.exe"), "wb") as dst: + dst.write(src.read()) + break finally: - os.unlink(path) + try: + os.unlink(zip_path) + except OSError: + pass else: + url = "https://astral.sh/uv/install.sh" + resp = urlopen(url, timeout=30, context=ctx) if ctx else urlopen(url, timeout=30) + script = resp.read().decode("utf-8") + resp.close() subprocess.Popen(["sh", "-c", script], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() return _find_uv()