Skip to content

Commit

Permalink
Do it in uv instead
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Oct 1, 2024
1 parent b9bcb37 commit fb5f630
Show file tree
Hide file tree
Showing 21 changed files with 373 additions and 180 deletions.
2 changes: 0 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions crates/uv-build-frontend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,15 @@ anyhow = { workspace = true }
fs-err = { workspace = true }
indoc = { workspace = true }
itertools = { workspace = true }
owo-colors = { workspace = true }
regex = { workspace = true }
rustc-hash = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tempfile = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
toml_edit = { workspace = true }
tracing = { workspace = true }
rustc-hash = { workspace = true }

[dev-dependencies]
insta = { version = "1.40.0" }
96 changes: 30 additions & 66 deletions crates/uv-build-frontend/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,19 @@
use crate::PythonRunnerOutput;
use itertools::Itertools;
use owo_colors::OwoColorize;
use pep440_rs::Version;
use pep508_rs::PackageName;
use regex::Regex;
use rustc_hash::FxHashMap;
use std::env;
use std::fmt::{Display, Formatter};
use std::io;
use std::path::PathBuf;
use std::process::ExitStatus;
use std::str::FromStr;
use std::sync::LazyLock;
use thiserror::Error;
use tracing::error;
use uv_configuration::BuildOutput;
use uv_fs::Simplified;

/// Static map of common package name typos or misconfigurations to their correct package names.
static SUGGESTIONS: LazyLock<FxHashMap<PackageName, PackageName>> = LazyLock::new(|| {
let suggestions: Vec<(String, String)> =
serde_json::from_str(include_str!("suggestions.json")).unwrap();
suggestions
.iter()
.map(|(k, v)| {
(
PackageName::from_str(k).unwrap(),
PackageName::from_str(v).unwrap(),
)
})
.collect()
});

/// e.g. `pygraphviz/graphviz_wrap.c:3020:10: fatal error: graphviz/cgraph.h: No such file or directory`
static MISSING_HEADER_RE_GCC: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
Expand Down Expand Up @@ -95,23 +77,25 @@ pub enum Error {
stderr: String,
},
/// Nudge the user towards installing the missing dev library
#[error("{message} with {exit_code}\n--- stdout:\n{stdout}\n--- stderr:\n{stderr}\n---\n\n{hint}{colon} {missing_header_cause}", hint = "hint".cyan().bold(), colon = ":".bold())]
#[error("{message} with {exit_code}\n--- stdout:\n{stdout}\n--- stderr:\n{stderr}\n---")]
MissingHeaderOutput {
message: String,
exit_code: ExitStatus,
stdout: String,
stderr: String,
#[source]
missing_header_cause: MissingHeaderCause,
},
#[error("{message} with {exit_code}")]
BuildBackend {
message: String,
exit_code: ExitStatus,
},
#[error("{message} with {exit_code}\n\n{hint}{colon} {missing_header_cause}", hint = "hint".cyan().bold(), colon = ":".bold())]
#[error("{message} with {exit_code}")]
MissingHeader {
message: String,
exit_code: ExitStatus,
#[source]
missing_header_cause: MissingHeaderCause,
},
#[error("Failed to build PATH for build script")]
Expand All @@ -124,7 +108,6 @@ enum MissingLibrary {
Linker(String),
BuildDependency(String),
DeprecatedModule(String, Version),
SuggestedPackage(String, String),
}

#[derive(Debug, Error)]
Expand Down Expand Up @@ -213,15 +196,6 @@ impl Display for MissingHeaderCause {
)
}
}
MissingLibrary::SuggestedPackage(package, suggestion) => {
write!(
f,
"`{}` is often confused for `{}` Did you mean to install `{}` instead?",
package.cyan(),
suggestion.cyan(),
suggestion.cyan(),
)
}
}
}
}
Expand All @@ -236,42 +210,32 @@ impl Error {
version: Option<&Version>,
version_id: Option<&str>,
) -> Self {
// Check if we failed to install a known package.
let missing_library = if let Some((name, suggestion)) =
name.and_then(|name| SUGGESTIONS.get(name).map(|suggestion| (name, suggestion)))
{
Some(MissingLibrary::SuggestedPackage(
name.to_string(),
suggestion.to_string(),
))
} else {
// Limit search to the last 10 lines, which typically contains the relevant error.
output.stderr.iter().rev().take(10).find_map(|line| {
if let Some((_, [header])) = MISSING_HEADER_RE_GCC
.captures(line.trim())
.or(MISSING_HEADER_RE_CLANG.captures(line.trim()))
.or(MISSING_HEADER_RE_MSVC.captures(line.trim()))
.map(|c| c.extract())
{
Some(MissingLibrary::Header(header.to_string()))
} else if let Some((_, [library])) =
LD_NOT_FOUND_RE.captures(line.trim()).map(|c| c.extract())
{
Some(MissingLibrary::Linker(library.to_string()))
} else if WHEEL_NOT_FOUND_RE.is_match(line.trim()) {
Some(MissingLibrary::BuildDependency("wheel".to_string()))
} else if TORCH_NOT_FOUND_RE.is_match(line.trim()) {
Some(MissingLibrary::BuildDependency("torch".to_string()))
} else if DISTUTILS_NOT_FOUND_RE.is_match(line.trim()) {
Some(MissingLibrary::DeprecatedModule(
"distutils".to_string(),
Version::new([3, 12]),
))
} else {
None
}
})
};
// In the cases I've seen it was the 5th and 3rd last line (see test case), 10 seems like a reasonable cutoff.
let missing_library = output.stderr.iter().rev().take(10).find_map(|line| {
if let Some((_, [header])) = MISSING_HEADER_RE_GCC
.captures(line.trim())
.or(MISSING_HEADER_RE_CLANG.captures(line.trim()))
.or(MISSING_HEADER_RE_MSVC.captures(line.trim()))
.map(|c| c.extract())
{
Some(MissingLibrary::Header(header.to_string()))
} else if let Some((_, [library])) =
LD_NOT_FOUND_RE.captures(line.trim()).map(|c| c.extract())
{
Some(MissingLibrary::Linker(library.to_string()))
} else if WHEEL_NOT_FOUND_RE.is_match(line.trim()) {
Some(MissingLibrary::BuildDependency("wheel".to_string()))
} else if TORCH_NOT_FOUND_RE.is_match(line.trim()) {
Some(MissingLibrary::BuildDependency("torch".to_string()))
} else if DISTUTILS_NOT_FOUND_RE.is_match(line.trim()) {
Some(MissingLibrary::DeprecatedModule(
"distutils".to_string(),
Version::new([3, 12]),
))
} else {
None
}
});

if let Some(missing_library) = missing_library {
return match level {
Expand Down
5 changes: 4 additions & 1 deletion crates/uv-dispatch/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,10 @@ impl<'a> BuildContext for BuildDispatch<'a> {
let graph = resolver.resolve().await.with_context(|| {
format!(
"No solution found when resolving: {}",
requirements.iter().map(ToString::to_string).join(", "),
requirements
.iter()
.map(|requirement| format!("`{requirement}`"))
.join(", ")
)
})?;
Ok(Resolution::from(graph))
Expand Down
1 change: 0 additions & 1 deletion crates/uv/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ axoupdater = { workspace = true, features = [
"tokio",
], optional = true }
clap = { workspace = true, features = ["derive", "string", "wrap_help"] }
console = { workspace = true }
ctrlc = { workspace = true }
flate2 = { workspace = true, default-features = false }
fs-err = { workspace = true, features = ["tokio"] }
Expand Down
112 changes: 112 additions & 0 deletions crates/uv/src/commands/diagnostics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use distribution_types::{Name, SourceDist};
use owo_colors::OwoColorize;
use rustc_hash::FxHashMap;
use std::str::FromStr;
use std::sync::LazyLock;
use uv_normalize::PackageName;

/// Static map of common package name typos or misconfigurations to their correct package names.
static SUGGESTIONS: LazyLock<FxHashMap<PackageName, PackageName>> = LazyLock::new(|| {
let suggestions: Vec<(String, String)> =
serde_json::from_str(include_str!("suggestions.json")).unwrap();
suggestions
.iter()
.map(|(k, v)| {
(
PackageName::from_str(k).unwrap(),
PackageName::from_str(v).unwrap(),
)
})
.collect()
});

/// Render a [`uv_resolver::ResolveError::FetchAndBuild`] with a help message.
pub(crate) fn fetch_and_build(sdist: Box<SourceDist>, cause: uv_distribution::Error) {
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
#[error("Failed to download and build `{sdist}`")]
#[diagnostic()]
struct Error {
sdist: Box<SourceDist>,
#[source]
cause: uv_distribution::Error,
#[help]
help: Option<String>,
}

let report = miette::Report::new(Error {
help: SUGGESTIONS.get(sdist.name()).map(|suggestion| {
format!(
"`{}` is often confused for `{}` Did you mean to install `{}` instead?",
sdist.name().cyan(),
suggestion.cyan(),
suggestion.cyan(),
)
}),
sdist,
cause,
});
anstream::eprint!("{report:?}");
}

/// Render a [`uv_resolver::ResolveError::Build`] with a help message.
pub(crate) fn build(sdist: Box<SourceDist>, cause: uv_distribution::Error) {
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
#[error("Failed to build `{sdist}`")]
#[diagnostic()]
struct Error {
sdist: Box<SourceDist>,
#[source]
cause: uv_distribution::Error,
#[help]
help: Option<String>,
}

let report = miette::Report::new(Error {
help: SUGGESTIONS.get(sdist.name()).map(|suggestion| {
format!(
"`{}` is often confused for `{}` Did you mean to install `{}` instead?",
sdist.name().cyan(),
suggestion.cyan(),
suggestion.cyan(),
)
}),
sdist,
cause,
});
anstream::eprint!("{report:?}");
}

/// Render a [`uv_resolver::NoSolutionError`].
pub(crate) fn no_solution(err: &uv_resolver::NoSolutionError) {
let report = miette::Report::msg(format!("{err}")).context(err.header());
anstream::eprint!("{report:?}");
}

/// Render a [`uv_resolver::NoSolutionError`] with dedicated context.
pub(crate) fn no_solution_context(err: &uv_resolver::NoSolutionError, context: &'static str) {
let report = miette::Report::msg(format!("{err}")).context(err.header().with_context(context));
anstream::eprint!("{report:?}");
}

/// Render a [`uv_resolver::NoSolutionError`] with a help message.
pub(crate) fn no_solution_hint(err: uv_resolver::NoSolutionError, help: String) {
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
#[error("{header}")]
#[diagnostic()]
struct Error {
/// The header to render in the error message.
header: uv_resolver::NoSolutionHeader,

/// The underlying error.
#[source]
err: uv_resolver::NoSolutionError,

/// The help message to display.
#[help]
help: String,
}

let header = err.header();
let report = miette::Report::new(Error { header, err, help });
anstream::eprint!("{report:?}");
}
1 change: 1 addition & 0 deletions crates/uv/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ pub(crate) mod build_backend;
mod cache_clean;
mod cache_dir;
mod cache_prune;
mod diagnostics;
mod help;
pub(crate) mod pip;
mod project;
Expand Down
14 changes: 10 additions & 4 deletions crates/uv/src/commands/pip/compile.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use std::env;
use std::path::Path;

use anstream::eprint;
use anyhow::{anyhow, Result};
use itertools::Itertools;
use owo_colors::OwoColorize;
Expand Down Expand Up @@ -42,7 +41,7 @@ use uv_warnings::warn_user;

use crate::commands::pip::loggers::DefaultResolveLogger;
use crate::commands::pip::{operations, resolution_environment};
use crate::commands::{ExitStatus, OutputWriter};
use crate::commands::{diagnostics, ExitStatus, OutputWriter};
use crate::printer::Printer;

/// Resolve a set of requirements into a set of pinned versions.
Expand Down Expand Up @@ -391,8 +390,15 @@ pub(crate) async fn pip_compile(
{
Ok(resolution) => resolution,
Err(operations::Error::Resolve(uv_resolver::ResolveError::NoSolution(err))) => {
let report = miette::Report::msg(format!("{err}")).context(err.header());
eprint!("{report:?}");
diagnostics::no_solution(&err);
return Ok(ExitStatus::Failure);
}
Err(operations::Error::Resolve(uv_resolver::ResolveError::FetchAndBuild(dist, err))) => {
diagnostics::fetch_and_build(dist, err);
return Ok(ExitStatus::Failure);
}
Err(operations::Error::Resolve(uv_resolver::ResolveError::Build(dist, err))) => {
diagnostics::build(dist, err);
return Ok(ExitStatus::Failure);
}
Err(err) => return Err(err.into()),
Expand Down
Loading

0 comments on commit fb5f630

Please sign in to comment.