From 1dcbcadd5982c1d86e9c9166595cb6c64e223cd5 Mon Sep 17 00:00:00 2001 From: Chris Pryer Date: Sun, 8 Oct 2023 19:41:47 -0400 Subject: [PATCH] Add toolchain --- .gitignore | 6 +- Cargo.lock | 7 + crates/huak_python_manager/Cargo.toml | 14 + crates/huak_python_manager/README.md | 18 + .../huak_python_manager/requirements-dev.txt | 2 + .../scripts/generate_python_releases.py | 187 +++++++ .../scripts/generated_python_releases.parquet | Bin 0 -> 24743 bytes crates/huak_python_manager/src/main.rs | 7 + crates/huak_python_manager/src/releases.rs | 529 ++++++++++++++++++ 9 files changed, 768 insertions(+), 2 deletions(-) create mode 100644 crates/huak_python_manager/Cargo.toml create mode 100644 crates/huak_python_manager/README.md create mode 100644 crates/huak_python_manager/requirements-dev.txt create mode 100644 crates/huak_python_manager/scripts/generate_python_releases.py create mode 100644 crates/huak_python_manager/scripts/generated_python_releases.parquet create mode 100644 crates/huak_python_manager/src/main.rs create mode 100644 crates/huak_python_manager/src/releases.rs diff --git a/.gitignore b/.gitignore index 8d51d66b..7f84e86f 100644 --- a/.gitignore +++ b/.gitignore @@ -33,13 +33,15 @@ MANIFEST .vscode/ # Environments -/.env -/.venv +.env +.venv /env/ /venv/ /ENV/ /env.bak/ /venv.bak/ +.github_token + # Misc. .DS_Store diff --git a/Cargo.lock b/Cargo.lock index 7def1bf5..32e5b1c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -386,6 +386,13 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "huak_python_manager" +version = "0.0.0" +dependencies = [ + "huak_home", +] + [[package]] name = "human-panic" version = "1.1.5" diff --git a/crates/huak_python_manager/Cargo.toml b/crates/huak_python_manager/Cargo.toml new file mode 100644 index 00000000..7904e0a3 --- /dev/null +++ b/crates/huak_python_manager/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "huak_python_manager" +description = "A Python interpreter management system for Huak." +version = "0.0.0" +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +huak_home = { version = "0.0.0", path = "../huak_home" } + +[lints] +workspace = true diff --git a/crates/huak_python_manager/README.md b/crates/huak_python_manager/README.md new file mode 100644 index 00000000..ad63e9b0 --- /dev/null +++ b/crates/huak_python_manager/README.md @@ -0,0 +1,18 @@ +# Python Manager + +A Python interpreter management system for Huak. + +## Usage + +``` +huak_python_manager install 3.11 +``` + +## How it works + +### Installing a Python interpreter + +1. Fetch the interpreter from https://github.com/indygreg/python-build-standalone using GitHub API. +1. Validate the checksum of the interpreter. +1. Extract the interpreter using `tar`. +1. Place the interpreter in Huak's home directory (~/.huak/bin/). diff --git a/crates/huak_python_manager/requirements-dev.txt b/crates/huak_python_manager/requirements-dev.txt new file mode 100644 index 00000000..1249fdb0 --- /dev/null +++ b/crates/huak_python_manager/requirements-dev.txt @@ -0,0 +1,2 @@ +polars==0.19.8 +requests==2.31.0 diff --git a/crates/huak_python_manager/scripts/generate_python_releases.py b/crates/huak_python_manager/scripts/generate_python_releases.py new file mode 100644 index 00000000..ef9a5ff4 --- /dev/null +++ b/crates/huak_python_manager/scripts/generate_python_releases.py @@ -0,0 +1,187 @@ +"""This module generates releases.rs for the huak_python_manager crate.""" +import re +import subprocess +from typing import NamedTuple +import requests +from pathlib import Path +from urllib.parse import unquote +import polars as pl + + +FILE = Path(__file__) +ROOT = Path( + subprocess.check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip() +) +CRATE = "huak_python_manager" +TOKEN = (FILE.parent / ".github_token").read_text().strip() + +RELEASE_URL = "https://api.github.com/repos/indygreg/python-build-standalone/releases" +HEADERS = headers = { + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {TOKEN}", +} + +VERSION_PATTERN = re.compile(r"cpython-(\d+\.\d+\.\d+)") +OS_PATTERN = re.compile(r"-(windows|apple|linux)-") +ARCHITECTURE_PATTERN = re.compile(r"-(aarch64|i686|x86_64)-") +# TODO(cnpryer): Can we prioritize fastest builds? +# BUILD_PATTERN = re.compile("-(pgo\+lto|pgo|lto|noopt|debug|install_only)-") +BUILD_PATTERN = re.compile(r"-(pgo\+lto|pgo)-") + + +class Release(NamedTuple): + kind: str + version: str + os: str + architecture: str + build_configuration: str + checksum: str + url_suffix: str + + def to_rust_string(self) -> str: + (major, minor, patch) = self.version.split(".") + version = f"Version::new({major}, {minor}, {patch})" + return f"""\ +Release::new("{self.kind}", {version}, "{self.os}", "{self.architecture}", "{self.build_configuration}", "{self.checksum}", "{self.url_suffix}")\ +""" # noqa + + +session = requests.Session() +release_json = session.get(RELEASE_URL).json() + + +def is_checksum_url(url: str) -> bool: + return url.endswith(".sha256") or url.endswith("SHA256SUMS") + + +def get_checksum(url: str) -> str | None: + res = session.get(url) + res.raise_for_status() + return res.text.strip() + + +generated = pl.read_parquet(FILE.parent / "generated_python_releases.parquet") +new_releases = {"url": [], "string": []} + +# Identify releases with checksums published. +has_checksum = set() +for release in release_json: + for asset in release["assets"]: + if asset["browser_download_url"].endswith(".sha256"): + has_checksum.add(asset["browser_download_url"].removesuffix(".sha256")) + + +module = f"""\ +//! This file was generated by `{FILE.name}`. + +const DOWNLOAD_URL: &str = "https://github.com/indygreg/python-build-standalone/releases/download/"; + +#[rustfmt::skip] +pub const RELEASES: &[Release] = &[\ +""" # noqa +for release in release_json: + for asset in release["assets"]: + # Avoid making requests for releases we've already generated. + matching = generated.filter(pl.col("url").eq(asset["browser_download_url"])) + if not matching.is_empty(): + string = matching.select(pl.col("string")).to_series()[0] + module += "\n\t" + string + "," + continue + + # Skip any releases that don't have checksums + if asset["browser_download_url"] not in has_checksum: + print(f"no checksum for {asset['name']}") + continue + + url = unquote(asset["browser_download_url"]) + + # Skip builds not included in the pattern + build_matches = re.search(BUILD_PATTERN, url) + if not build_matches: + continue + build_str = build_matches.group(1) + + # Skip architectures not included in the pattern + arch_matches = re.search(ARCHITECTURE_PATTERN, url) + if not arch_matches: + continue + arch_str = arch_matches.group(1) + + checksum_str = get_checksum(asset["browser_download_url"] + ".sha256") + version_str = re.search(VERSION_PATTERN, url).group(1) + os_str = re.search(OS_PATTERN, url).group(1) + release = Release( + "cpython", + version_str, + os_str, + arch_str, + build_str, + checksum_str, + asset["browser_download_url"].removeprefix( + "https://github.com/indygreg/python-build-standalone/releases/download/" + ), + ) + new_releases["url"].append(asset["browser_download_url"]) + new_releases["string"].append(release.to_rust_string()) + module += "\n\t" + release.to_rust_string() + "," +module += """\n]; + +pub struct Release<'a> { + pub kind: &'a str, + pub version: Version, + pub os: &'a str, + pub architecture: &'a str, + pub build_configuration: &'a str, + pub checksum: &'a str, + url_suffix: &'a str, +} + +impl Release<'static> { + const fn new( + kind: &'static str, + version: Version, + os: &'static str, + architecture: &'static str, + build_configuration: &'static str, + checksum: &'static str, + url_suffix: &'static str, + ) -> Self { + Self { + kind, + version, + os, + architecture, + build_configuration, + checksum, + url_suffix, + } + } + + pub fn url(&self) -> String { + format!("{}{}", DOWNLOAD_URL, self.url_suffix) + } +} + +pub struct Version { + pub major: u8, + pub minor: u8, + pub patch: u8, +} + +impl Version { + const fn new(major: u8, minor: u8, patch: u8) -> Self { + Self { + major, + minor, + patch, + } + } +} +""" + +path = ROOT / "crates" / CRATE / "src" / "releases.rs" +path.write_text(module) + +new_releases = pl.DataFrame(new_releases, schema={"url": pl.Utf8, "string": pl.Utf8}) +path = FILE.parent / "generated_python_releases.parquet" +pl.concat((generated, new_releases)).write_parquet(path) diff --git a/crates/huak_python_manager/scripts/generated_python_releases.parquet b/crates/huak_python_manager/scripts/generated_python_releases.parquet new file mode 100644 index 0000000000000000000000000000000000000000..28abee0c057e632ec5cf6d9212a731ba9b18ca52 GIT binary patch literal 24743 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 { + pub kind: &'a str, + pub version: Version, + pub os: &'a str, + pub architecture: &'a str, + pub build_configuration: &'a str, + pub checksum: &'a str, + url_suffix: &'a str, +} + +impl Release<'static> { + const fn new( + kind: &'static str, + version: Version, + os: &'static str, + architecture: &'static str, + build_configuration: &'static str, + checksum: &'static str, + url_suffix: &'static str, + ) -> Self { + Self { + kind, + version, + os, + architecture, + build_configuration, + checksum, + url_suffix, + } + } + + pub fn url(&self) -> String { + format!("{}{}", DOWNLOAD_URL, self.url_suffix) + } +} + +pub struct Version { + pub major: u8, + pub minor: u8, + pub patch: u8, +} + +impl Version { + const fn new(major: u8, minor: u8, patch: u8) -> Self { + Self { + major, + minor, + patch, + } + } +}