-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Add --compile-bytecode to uv python install and uv python upgrade to compile the standard library
#17088
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add --compile-bytecode to uv python install and uv python upgrade to compile the standard library
#17088
Changes from all commits
20632ff
61da4bd
8312940
27766ed
0f99056
f0b7ad0
84d0f9a
f011af2
e3eebf0
c3aa021
af9ff53
2986f31
9be31e8
df1f39f
eee4e1d
6b7f057
f53be27
182853e
cf4b8c6
2a98a38
cdf398c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,16 +5,18 @@ use std::io::ErrorKind; | |
| use std::path::{Path, PathBuf}; | ||
| use std::str::FromStr; | ||
|
|
||
| use anyhow::{Error, Result}; | ||
| use futures::StreamExt; | ||
| use futures::stream::FuturesUnordered; | ||
| use anyhow::{Context, Error, Result}; | ||
| use futures::{StreamExt, join}; | ||
| use indexmap::IndexSet; | ||
| use itertools::{Either, Itertools}; | ||
| use owo_colors::{AnsiColors, OwoColorize}; | ||
| use rustc_hash::{FxHashMap, FxHashSet}; | ||
| use tracing::{debug, trace}; | ||
| use tokio::sync::mpsc; | ||
| use tracing::{debug, trace, warn}; | ||
|
|
||
| use uv_cache::Cache; | ||
| use uv_client::BaseClientBuilder; | ||
| use uv_configuration::Concurrency; | ||
| use uv_fs::Simplified; | ||
| use uv_platform::{Arch, Libc}; | ||
| use uv_preview::{Preview, PreviewFeatures}; | ||
|
|
@@ -27,8 +29,9 @@ use uv_python::managed::{ | |
| create_link_to_executable, python_executable_dir, | ||
| }; | ||
| use uv_python::{ | ||
| PythonDownloads, PythonInstallationKey, PythonInstallationMinorVersionKey, PythonRequest, | ||
| PythonVersionFile, VersionFileDiscoveryOptions, VersionFilePreference, VersionRequest, | ||
| ImplementationName, Interpreter, PythonDownloads, PythonInstallationKey, | ||
| PythonInstallationMinorVersionKey, PythonRequest, PythonVersionFile, | ||
| VersionFileDiscoveryOptions, VersionFilePreference, VersionRequest, | ||
| }; | ||
| use uv_shell::Shell; | ||
| use uv_trampoline_builder::{Launcher, LauncherKind}; | ||
|
|
@@ -191,6 +194,114 @@ pub(crate) async fn install( | |
| default: bool, | ||
| python_downloads: PythonDownloads, | ||
| no_config: bool, | ||
| compile_bytecode: bool, | ||
| concurrency: &Concurrency, | ||
| cache: &Cache, | ||
| preview: Preview, | ||
| printer: Printer, | ||
| ) -> Result<ExitStatus> { | ||
| let (sender, mut receiver) = mpsc::unbounded_channel(); | ||
| let compiler = async { | ||
| let mut total_files = 0; | ||
| let mut total_elapsed = std::time::Duration::default(); | ||
| let mut total_skipped = 0; | ||
| while let Some(installation) = receiver.recv().await { | ||
| if let Some((files, elapsed)) = | ||
| compile_stdlib_bytecode(&installation, concurrency, cache) | ||
| .await | ||
| .with_context(|| { | ||
| format!( | ||
| "Failed to bytecode-compile Python standard library for: {}", | ||
| installation.key() | ||
| ) | ||
| })? | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we propagate errors or just quietly log them and mark those installs as skipped?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd start with an error, there's no known reason this would be valid to fail and we're already most lenient in the compilation itself. As a user who decided to activate bytecode compilation in my deploy pipeline, I'd want a compilation failure to fail the pipeline.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alright, I'll leave it as is then. |
||
| { | ||
| total_files += files; | ||
| total_elapsed += elapsed; | ||
| } else { | ||
| total_skipped += 1; | ||
| } | ||
| } | ||
| Ok::<_, anyhow::Error>((total_files, total_elapsed, total_skipped)) | ||
| }; | ||
|
|
||
| let installer = perform_install( | ||
| project_dir, | ||
| install_dir, | ||
| targets, | ||
| reinstall, | ||
| upgrade, | ||
| bin, | ||
| registry, | ||
| force, | ||
| python_install_mirror, | ||
| pypy_install_mirror, | ||
| python_downloads_json_url, | ||
| client_builder, | ||
| default, | ||
| python_downloads, | ||
| no_config, | ||
| compile_bytecode.then_some(sender), | ||
| concurrency, | ||
| preview, | ||
| printer, | ||
| ); | ||
|
|
||
| let (installer_result, compiler_result) = join!(installer, compiler); | ||
|
|
||
| let (total_files, total_elapsed, total_skipped) = compiler_result?; | ||
| if total_files > 0 { | ||
| let s = if total_files == 1 { "" } else { "s" }; | ||
| writeln!( | ||
| printer.stderr(), | ||
| "{}", | ||
| format!( | ||
| "Bytecode compiled {} {}{}", | ||
| format!("{total_files} file{s}").bold(), | ||
| format!("in {}", elapsed(total_elapsed)).dimmed(), | ||
| if total_skipped > 0 { | ||
| format!( | ||
| " (skipped {total_skipped} incompatible version{})", | ||
| if total_skipped == 1 { "" } else { "s" } | ||
| ) | ||
| } else { | ||
| String::new() | ||
| } | ||
| .dimmed() | ||
| ) | ||
| .dimmed() | ||
| )?; | ||
| } else if total_skipped > 0 { | ||
| writeln!( | ||
| printer.stderr(), | ||
| "{}", | ||
| format!("No compatible versions to bytecode compile (skipped {total_skipped})") | ||
| .dimmed() | ||
| )?; | ||
| } | ||
|
|
||
| installer_result | ||
| } | ||
|
|
||
| #[allow(clippy::fn_params_excessive_bools)] | ||
| async fn perform_install( | ||
| project_dir: &Path, | ||
| install_dir: Option<PathBuf>, | ||
| targets: Vec<String>, | ||
| reinstall: bool, | ||
| upgrade: PythonUpgrade, | ||
| bin: Option<bool>, | ||
| registry: Option<bool>, | ||
| force: bool, | ||
| python_install_mirror: Option<String>, | ||
| pypy_install_mirror: Option<String>, | ||
| python_downloads_json_url: Option<String>, | ||
| client_builder: BaseClientBuilder<'_>, | ||
| default: bool, | ||
| python_downloads: PythonDownloads, | ||
| no_config: bool, | ||
| bytecode_compilation_sender: Option<mpsc::UnboundedSender<ManagedPythonInstallation>>, | ||
| concurrency: &Concurrency, | ||
| preview: Preview, | ||
| printer: Printer, | ||
| ) -> Result<ExitStatus> { | ||
|
|
@@ -433,6 +544,16 @@ pub(crate) async fn install( | |
| }) | ||
| }; | ||
|
|
||
| // For all satisfied installs, bytecode compile them now before any future | ||
| // early return. | ||
| if let Some(ref sender) = bytecode_compilation_sender { | ||
| satisfied | ||
| .iter() | ||
| .copied() | ||
| .cloned() | ||
| .try_for_each(|installation| sender.send(installation))?; | ||
| } | ||
|
|
||
| // Check if Python downloads are banned | ||
| if matches!(python_downloads, PythonDownloads::Never) && !unsatisfied.is_empty() { | ||
| writeln!( | ||
|
|
@@ -458,10 +579,9 @@ pub(crate) async fn install( | |
|
|
||
| // Download and unpack the Python versions concurrently | ||
| let reporter = PythonDownloadReporter::new(printer, Some(downloads.len() as u64)); | ||
| let mut tasks = FuturesUnordered::new(); | ||
|
|
||
| for download in &downloads { | ||
| tasks.push(async { | ||
| let mut tasks = futures::stream::iter(&downloads) | ||
EliteTK marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| .map(async |download| { | ||
| ( | ||
| *download, | ||
| download | ||
|
|
@@ -477,8 +597,8 @@ pub(crate) async fn install( | |
| ) | ||
| .await, | ||
| ) | ||
| }); | ||
| } | ||
| }) | ||
| .buffer_unordered(concurrency.downloads); | ||
|
|
||
| let mut errors = vec![]; | ||
| let mut downloaded = Vec::with_capacity(downloads.len()); | ||
|
|
@@ -493,6 +613,9 @@ pub(crate) async fn install( | |
| }; | ||
|
|
||
| let installation = ManagedPythonInstallation::new(path, download); | ||
| if let Some(ref sender) = bytecode_compilation_sender { | ||
| sender.send(installation.clone())?; | ||
| } | ||
| changelog.installed.insert(installation.key().clone()); | ||
| for request in &requests { | ||
| // Take note of which installations satisfied which requests | ||
|
|
@@ -1071,6 +1194,53 @@ fn create_bin_links( | |
| } | ||
| } | ||
|
|
||
| /// Attempt to compile the bytecode for a [`ManagedPythonInstallation`]'s stdlib | ||
| async fn compile_stdlib_bytecode( | ||
| installation: &ManagedPythonInstallation, | ||
| concurrency: &Concurrency, | ||
| cache: &Cache, | ||
| ) -> Result<Option<(usize, std::time::Duration)>> { | ||
| let start = std::time::Instant::now(); | ||
|
|
||
| // Explicit matching so this heuristic is updated for future additions | ||
| match installation.implementation() { | ||
| ImplementationName::Pyodide => return Ok(None), | ||
| ImplementationName::GraalPy | ImplementationName::PyPy | ImplementationName::CPython => (), | ||
| } | ||
|
|
||
| let interpreter = Interpreter::query(installation.executable(false), cache) | ||
| .context("Couldn't locate the interpreter")?; | ||
|
|
||
| // Ensure the bytecode compilation occurs in the correct place, in case the installed | ||
| // interpreter reports a weird stdlib path. | ||
| let interpreter_path = installation.path().canonicalize()?; | ||
| let stdlib_path = match interpreter.stdlib().canonicalize() { | ||
| Ok(path) if path.starts_with(&interpreter_path) => path, | ||
| _ => { | ||
| warn!( | ||
| "The stdlib path for {} ({}) is not a subdirectory of its installation path ({}).", | ||
| installation.key(), | ||
| interpreter.stdlib().display(), | ||
| interpreter_path.display() | ||
| ); | ||
| return Ok(None); | ||
| } | ||
| }; | ||
|
|
||
| let files = uv_installer::compile_tree( | ||
| &stdlib_path, | ||
| &installation.executable(false), | ||
| concurrency, | ||
| cache.root(), | ||
| ) | ||
| .await | ||
| .with_context(|| format!("Error compiling bytecode in: {}", stdlib_path.display()))?; | ||
| if files == 0 { | ||
| return Ok(None); | ||
| } | ||
| Ok(Some((files, start.elapsed()))) | ||
| } | ||
|
|
||
| pub(crate) fn format_executables( | ||
| event: &ChangeEvent, | ||
| executables: &FxHashMap<PythonInstallationKey, FxHashSet<PathBuf>>, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.