From 217f84065b6a9dda595e8d2d30b2898593b0f8a1 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Thu, 6 Jun 2024 15:23:50 +0530 Subject: [PATCH] Use multiple machine suffixes to resolve Python versions (#18) --- .github/workflows/build.yml | 5 ++++ src/yen/cli.py | 11 +++++---- src/yen/downloader.py | 4 ++-- src/yen/github.py | 46 +++++++++++++++++++++++++------------ yen-rs/src/commands/list.rs | 2 +- yen-rs/src/github.rs | 33 +++++++++++++++----------- 6 files changed, 65 insertions(+), 36 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c22e306..d86aad9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -172,6 +172,11 @@ jobs: echo "BIN_PATH=${BIN_PATH}" >> $GITHUB_OUTPUT echo "BIN_NAME=${BIN_NAME}" >> $GITHUB_OUTPUT + - name: Run the binary + shell: bash + run: | + ${{ steps.bin.outputs.BIN_PATH }} list &>> $GITHUB_STEP_SUMMARY + - name: "Artifact upload: binary" uses: actions/upload-artifact@master with: diff --git a/src/yen/cli.py b/src/yen/cli.py index a394f1c..a4b4c63 100644 --- a/src/yen/cli.py +++ b/src/yen/cli.py @@ -3,10 +3,11 @@ from __future__ import annotations import argparse +import sys from typing import Literal -from yen import NotAvailable, create_symlink, create_venv, ensure_python -from yen.github import list_pythons +from yen import create_symlink, create_venv, ensure_python +from yen.github import NotAvailable, list_pythons class YenArgs: @@ -33,7 +34,7 @@ def cli() -> int: if args.command == "list": versions = list(list_pythons()) - print("Available Pythons:") + print("Available Pythons:", file=sys.stderr) for version in versions: print(version) @@ -44,11 +45,11 @@ def cli() -> int: except NotAvailable: print( "Error: requested Python version is not available." - " Use 'yen list' to get list of available Pythons." + " Use 'yen list' to get list of available Pythons.", + file=sys.stderr, ) return 1 - elif args.command == "use": try: python_version, python_bin_path = ensure_python(args.python) diff --git a/src/yen/downloader.py b/src/yen/downloader.py index 5a3ff39..c600590 100644 --- a/src/yen/downloader.py +++ b/src/yen/downloader.py @@ -35,14 +35,14 @@ DONE = Event() -def handle_sigint(signum, frame): +def handle_sigint(_: object, __: object) -> None: DONE.set() signal.signal(signal.SIGINT, handle_sigint) -def read_url(url: str) -> bytes: +def read_url(url: str) -> str: """Reads the contents of the URL.""" response = urlopen(url) return response.read().decode() diff --git a/src/yen/github.py b/src/yen/github.py index 16c3123..2819e77 100644 --- a/src/yen/github.py +++ b/src/yen/github.py @@ -4,37 +4,45 @@ import os.path import platform import re +import sys import urllib.error from typing import Any +import urllib.parse from urllib.request import urlopen MACHINE_SUFFIX = { "Darwin": { - "arm64": "aarch64-apple-darwin-install_only.tar.gz", - "x86_64": "x86_64-apple-darwin-install_only.tar.gz", + "arm64": ["aarch64-apple-darwin-install_only.tar.gz"], + "x86_64": ["x86_64-apple-darwin-install_only.tar.gz"], }, "Linux": { "aarch64": { - "glibc": "aarch64-unknown-linux-gnu-install_only.tar.gz", + "glibc": ["aarch64-unknown-linux-gnu-install_only.tar.gz"], # musl doesn't exist }, "x86_64": { - "glibc": "x86_64_v3-unknown-linux-gnu-install_only.tar.gz", - "musl": "x86_64_v3-unknown-linux-musl-install_only.tar.gz", + "glibc": [ + "x86_64_v3-unknown-linux-gnu-install_only.tar.gz", + "x86_64-unknown-linux-gnu-install_only.tar.gz", + ], + "musl": ["x86_64_v3-unknown-linux-musl-install_only.tar.gz"], }, }, - "Windows": {"AMD64": "x86_64-pc-windows-msvc-shared-install_only.tar.gz"}, + "Windows": {"AMD64": ["x86_64-pc-windows-msvc-shared-install_only.tar.gz"]}, } -GITHUB_API_URL = ( - "https://api.github.com/repos/indygreg/python-build-standalone/releases/latest" +GITHUB_API_RELEASES_URL = ( + "https://api.github.com/repos/indygreg/python-build-standalone/releases/" ) PYTHON_VERSION_REGEX = re.compile(r"cpython-(\d+\.\d+\.\d+)") def fallback_release_data() -> dict[str, Any]: """Returns the fallback release data, for when GitHub API gives an error.""" - print("\033[33mWarning: GitHub unreachable. Using fallback release data.\033[m") + print( + "\033[33mWarning: GitHub unreachable. Using fallback release data.\033[m", + file=sys.stderr, + ) data_file = os.path.join(os.path.dirname(__file__), "fallback_release_data.json") with open(data_file) as data: return json.load(data) @@ -46,12 +54,12 @@ class NotAvailable(Exception): def get_latest_python_releases() -> list[str]: """Returns the list of python download links from the latest github release.""" + latest_release_url = urllib.parse.urljoin(GITHUB_API_RELEASES_URL, "latest") try: - with urlopen(GITHUB_API_URL) as response: + with urlopen(latest_release_url) as response: release_data = json.load(response) except urllib.error.URLError: - # raise release_data = fallback_release_data() return [asset["browser_download_url"] for asset in release_data["assets"]] @@ -60,17 +68,21 @@ def get_latest_python_releases() -> list[str]: def list_pythons() -> dict[str, str]: """Returns available python versions for your machine and their download links.""" system, machine = platform.system(), platform.machine() - download_link_suffix = MACHINE_SUFFIX[system][machine] + download_link_suffixes = MACHINE_SUFFIX[system][machine] # linux suffixes are nested under glibc or musl builds if system == "Linux": # fallback to musl if libc version is not found libc_version = platform.libc_ver()[0] or "musl" - download_link_suffix = download_link_suffix[libc_version] + download_link_suffixes = download_link_suffixes[libc_version] python_releases = get_latest_python_releases() available_python_links = [ - link for link in python_releases if link.endswith(download_link_suffix) + link + # Suffixes are in order of preference. + for download_link_suffix in download_link_suffixes + for link in python_releases + if link.endswith(download_link_suffix) ] python_versions: dict[str, str] = {} @@ -78,6 +90,10 @@ def list_pythons() -> dict[str, str]: match = PYTHON_VERSION_REGEX.search(link) assert match is not None python_version = match[1] + # Don't override already found versions, as they are in order of preference + if python_version in python_versions: + continue + python_versions[python_version] = link sorted_python_versions = { @@ -96,7 +112,7 @@ def _parse_python_version(version: str) -> tuple[int, ...]: return tuple(int(k) for k in version.split(".")) -def resolve_python_version(requested_version: str | None) -> None: +def resolve_python_version(requested_version: str | None) -> tuple[str, str]: pythons = list_pythons() if requested_version is None: diff --git a/yen-rs/src/commands/list.rs b/yen-rs/src/commands/list.rs index 94dfe4f..dc60a17 100644 --- a/yen-rs/src/commands/list.rs +++ b/yen-rs/src/commands/list.rs @@ -8,7 +8,7 @@ pub struct Args; pub async fn execute(_args: Args) -> miette::Result<()> { let pythons = list_pythons().await?; - println!("Available Pythons:"); + eprintln!("Available Pythons:"); for v in pythons.keys().rev() { println!("{v}"); } diff --git a/yen-rs/src/github.rs b/yen-rs/src/github.rs index 7bdf1cc..6ed87f2 100644 --- a/yen-rs/src/github.rs +++ b/yen-rs/src/github.rs @@ -89,14 +89,17 @@ pub enum MachineSuffix { } impl MachineSuffix { - fn get_suffix(&self) -> String { + fn get_suffixes(&self) -> Vec { match self { - Self::DarwinArm64 => "aarch64-apple-darwin-install_only.tar.gz".into(), - Self::DarwinX64 => "x86_64-apple-darwin-install_only.tar.gz".into(), - Self::LinuxAarch64 => "aarch64-unknown-linux-gnu-install_only.tar.gz".into(), - Self::LinuxX64GlibC => "x86_64_v3-unknown-linux-gnu-install_only.tar.gz".into(), - Self::LinuxX64Musl => "x86_64_v3-unknown-linux-musl-install_only.tar.gz".into(), - Self::WindowsX64 => "x86_64-pc-windows-msvc-shared-install_only.tar.gz".into(), + Self::DarwinArm64 => vec!["aarch64-apple-darwin-install_only.tar.gz".into()], + Self::DarwinX64 => vec!["x86_64-apple-darwin-install_only.tar.gz".into()], + Self::LinuxAarch64 => vec!["aarch64-unknown-linux-gnu-install_only.tar.gz".into()], + Self::LinuxX64GlibC => vec![ + "x86_64_v3-unknown-linux-gnu-install_only.tar.gz".into(), + "x86_64-unknown-linux-gnu-install_only.tar.gz".into(), + ], + Self::LinuxX64Musl => vec!["x86_64_v3-unknown-linux-musl-install_only.tar.gz".into()], + Self::WindowsX64 => vec!["x86_64-pc-windows-msvc-shared-install_only.tar.gz".into()], } } @@ -147,18 +150,22 @@ async fn get_latest_python_release() -> miette::Result> { } pub async fn list_pythons() -> miette::Result> { - let machine_suffix = MachineSuffix::default().await?.get_suffix(); + let machine_suffixes = MachineSuffix::default().await?.get_suffixes(); let releases = get_latest_python_release().await?; let mut map = BTreeMap::new(); for release in releases { - if release.ends_with(&machine_suffix) { - let x = (*RE).captures(&release); - if let Some(v) = x { - let version = Version::from_str(&v[1])?; - map.insert(version, release); + for ref machine_suffix in machine_suffixes.iter() { + if release.ends_with(*machine_suffix) { + let x = (*RE).captures(&release); + if let Some(v) = x { + let version = Version::from_str(&v[1])?; + map.insert(version, release.clone()); + // Only keep the first match from machine suffixes + break; + } } } }