mirror of
https://github.com/astral-sh/uv
synced 2026-01-27 00:10:15 -05:00
Replace Python bootstrapping script with Rust implementation (#2842)
See https://github.com/astral-sh/uv/issues/2617 Note this also includes: - #2918 - #2931 (pending) A first step towards Python toolchain management in Rust. First, we add a new crate to manage Python download metadata: - Adds a new `uv-toolchain` crate - Adds Rust structs for Python version download metadata - Duplicates the script which downloads Python version metadata - Adds a script to generate Rust code from the JSON metadata - Adds a utility to download and extract the Python version I explored some alternatives like a build script using things like `serde` and `uneval` to automatically construct the code from our structs but deemed it to heavy. Unlike Rye, I don't generate the Rust directly from the web requests and have an intermediate JSON layer to speed up iteration on the Rust types. Next, we add add a `uv-dev` command `fetch-python` to download Python versions per the bootstrapping script. - Downloads a requested version or reads from `.python-versions` - Extracts to `UV_BOOTSTRAP_DIR` - Links executables for path extension This command is not really intended to be user facing, but it's a good PoC for the `uv-toolchain` API. Hash checking (via the sha256) isn't implemented yet, we can do that in a follow-up. Finally, we remove the `scripts/bootstrap` directory, update CI to use the new command, and update the CONTRIBUTING docs. <img width="1023" alt="Screenshot 2024-04-08 at 17 12 15" src="https://github.com/astral-sh/uv/assets/2586601/57bd3cf1-7477-4bb8-a8e9-802a00d772cb">
This commit is contained in:
142
crates/uv-dev/src/fetch_python.rs
Normal file
142
crates/uv-dev/src/fetch_python.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use fs_err as fs;
|
||||
#[cfg(unix)]
|
||||
use fs_err::tokio::symlink;
|
||||
use futures::StreamExt;
|
||||
#[cfg(unix)]
|
||||
use itertools::Itertools;
|
||||
use std::str::FromStr;
|
||||
#[cfg(unix)]
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use tokio::time::Instant;
|
||||
use tracing::{info, info_span, Instrument};
|
||||
|
||||
use uv_fs::Simplified;
|
||||
use uv_toolchain::{
|
||||
DownloadResult, Error, PythonDownload, PythonDownloadRequest, TOOLCHAIN_DIRECTORY,
|
||||
};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub(crate) struct FetchPythonArgs {
|
||||
versions: Vec<String>,
|
||||
}
|
||||
|
||||
pub(crate) async fn fetch_python(args: FetchPythonArgs) -> Result<()> {
|
||||
let start = Instant::now();
|
||||
|
||||
let bootstrap_dir = &*TOOLCHAIN_DIRECTORY;
|
||||
|
||||
fs_err::create_dir_all(bootstrap_dir)?;
|
||||
|
||||
let versions = if args.versions.is_empty() {
|
||||
info!("Reading versions from file...");
|
||||
read_versions_file().await?
|
||||
} else {
|
||||
args.versions
|
||||
};
|
||||
|
||||
let requests = versions
|
||||
.iter()
|
||||
.map(|version| {
|
||||
PythonDownloadRequest::from_str(version).and_then(PythonDownloadRequest::fill)
|
||||
})
|
||||
.collect::<Result<Vec<_>, Error>>()?;
|
||||
|
||||
let downloads = requests
|
||||
.iter()
|
||||
.map(|request| match PythonDownload::from_request(request) {
|
||||
Some(download) => download,
|
||||
None => panic!("No download found for request {request:?}"),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let client = uv_client::BaseClientBuilder::new().build();
|
||||
|
||||
info!("Fetching requested versions...");
|
||||
let mut tasks = futures::stream::iter(downloads.iter())
|
||||
.map(|download| {
|
||||
async {
|
||||
let result = download.fetch(&client, bootstrap_dir).await;
|
||||
(download.python_version(), result)
|
||||
}
|
||||
.instrument(info_span!("download", key = %download))
|
||||
})
|
||||
.buffered(4);
|
||||
|
||||
let mut results = Vec::new();
|
||||
let mut downloaded = 0;
|
||||
while let Some(task) = tasks.next().await {
|
||||
let (version, result) = task;
|
||||
let path = match result? {
|
||||
DownloadResult::AlreadyAvailable(path) => {
|
||||
info!("Found existing download for v{}", version);
|
||||
path
|
||||
}
|
||||
DownloadResult::Fetched(path) => {
|
||||
info!("Downloaded v{} to {}", version, path.user_display());
|
||||
downloaded += 1;
|
||||
path
|
||||
}
|
||||
};
|
||||
results.push((version, path));
|
||||
}
|
||||
|
||||
if downloaded > 0 {
|
||||
let s = if downloaded == 1 { "" } else { "s" };
|
||||
info!(
|
||||
"Fetched {} in {}s",
|
||||
format!("{} version{}", downloaded, s),
|
||||
start.elapsed().as_secs()
|
||||
);
|
||||
} else {
|
||||
info!("All versions downloaded already.");
|
||||
};
|
||||
|
||||
// Order matters here, as we overwrite previous links
|
||||
info!("Installing to `{}`...", bootstrap_dir.user_display());
|
||||
|
||||
// On Windows, linking the executable generally results in broken installations
|
||||
// and each toolchain path will need to be added to the PATH separately in the
|
||||
// desired order
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let mut links: HashMap<PathBuf, PathBuf> = HashMap::new();
|
||||
for (version, path) in results {
|
||||
// TODO(zanieb): This path should be a part of the download metadata
|
||||
let executable = path.join("install").join("bin").join("python3");
|
||||
for target in [
|
||||
bootstrap_dir.join(format!("python{}", version.python_full_version())),
|
||||
bootstrap_dir.join(format!("python{}.{}", version.major(), version.minor())),
|
||||
bootstrap_dir.join(format!("python{}", version.major())),
|
||||
bootstrap_dir.join("python"),
|
||||
] {
|
||||
// Attempt to remove it, we'll fail on link if we couldn't remove it for some reason
|
||||
// but if it's missing we don't want to error
|
||||
let _ = fs::remove_file(&target);
|
||||
symlink(&executable, &target).await?;
|
||||
links.insert(target, executable.clone());
|
||||
}
|
||||
}
|
||||
for (target, executable) in links.iter().sorted() {
|
||||
info!(
|
||||
"Linked `{}` to `{}`",
|
||||
target.user_display(),
|
||||
executable.user_display()
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
info!("Installed {} versions", requests.len());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_versions_file() -> Result<Vec<String>> {
|
||||
let lines: Vec<String> = fs::tokio::read_to_string(".python-versions")
|
||||
.await?
|
||||
.lines()
|
||||
.map(ToString::to_string)
|
||||
.collect();
|
||||
Ok(lines)
|
||||
}
|
||||
Reference in New Issue
Block a user