Skip to content

Commit

Permalink
Use multiple machine suffixes to resolve Python versions (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
tusharsadhwani authored Jun 6, 2024
1 parent 274bda4 commit 217f840
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 36 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
11 changes: 6 additions & 5 deletions src/yen/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)

Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions src/yen/downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
46 changes: 31 additions & 15 deletions src/yen/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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"]]
Expand All @@ -60,24 +68,32 @@ 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] = {}
for link in available_python_links:
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 = {
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion yen-rs/src/commands/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
}
Expand Down
33 changes: 20 additions & 13 deletions yen-rs/src/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,17 @@ pub enum MachineSuffix {
}

impl MachineSuffix {
fn get_suffix(&self) -> String {
fn get_suffixes(&self) -> Vec<String> {
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()],
}
}

Expand Down Expand Up @@ -147,18 +150,22 @@ async fn get_latest_python_release() -> miette::Result<Vec<String>> {
}

pub async fn list_pythons() -> miette::Result<BTreeMap<Version, String>> {
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;
}
}
}
}
Expand Down

0 comments on commit 217f840

Please sign in to comment.