mirror of https://github.com/astral-sh/uv
Add an option to use Resolvo in lieu of PubGrub
This commit is contained in:
parent
beadd3274a
commit
40bc049b51
|
|
@ -335,6 +335,18 @@ version = "2.4.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
|
||||
|
||||
[[package]]
|
||||
name = "bitvec"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
|
||||
dependencies = [
|
||||
"funty",
|
||||
"radium",
|
||||
"tap",
|
||||
"wyz",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
|
|
@ -877,6 +889,15 @@ version = "1.9.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
|
||||
|
||||
[[package]]
|
||||
name = "elsa"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "714f766f3556b44e7e4776ad133fcc3445a489517c25c704ace411bb14790194"
|
||||
dependencies = [
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "0.3.6"
|
||||
|
|
@ -985,6 +1006,12 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "funty"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.29"
|
||||
|
|
@ -2589,6 +2616,7 @@ dependencies = [
|
|||
"puffin-normalize",
|
||||
"puffin-traits",
|
||||
"pypi-types",
|
||||
"resolvo",
|
||||
"sha2",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
|
|
@ -2753,6 +2781,12 @@ version = "0.4.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a3866219251662ec3b26fc217e3e05bf9c4f84325234dfb96bf0bf840889e49"
|
||||
|
||||
[[package]]
|
||||
name = "radium"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
|
|
@ -3000,6 +3034,19 @@ dependencies = [
|
|||
"wasm-timer",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "resolvo"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "554db165775d6858d17a9626c327b796d81db95db0a4cd6ca0efdfb7e7e3a264"
|
||||
dependencies = [
|
||||
"bitvec",
|
||||
"elsa",
|
||||
"itertools 0.11.0",
|
||||
"petgraph",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "retry-policies"
|
||||
version = "0.2.1"
|
||||
|
|
@ -3328,6 +3375,12 @@ dependencies = [
|
|||
"xxhash-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
|
||||
[[package]]
|
||||
name = "stacker"
|
||||
version = "0.1.15"
|
||||
|
|
@ -3418,6 +3471,12 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tap"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.40"
|
||||
|
|
@ -4379,6 +4438,15 @@ dependencies = [
|
|||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wyz"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
|
||||
dependencies = [
|
||||
"tap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.0.1"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use std::time::Duration;
|
|||
pub(crate) use add::add;
|
||||
pub(crate) use clean::clean;
|
||||
pub(crate) use freeze::freeze;
|
||||
pub(crate) use pip_compile::{extra_name_with_clap_error, pip_compile};
|
||||
pub(crate) use pip_compile::{extra_name_with_clap_error, pip_compile, Resolver};
|
||||
pub(crate) use pip_sync::pip_sync;
|
||||
pub(crate) use pip_uninstall::pip_uninstall;
|
||||
pub(crate) use remove::remove;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use std::borrow::Cow;
|
||||
use std::fmt::Write;
|
||||
use std::io::{stdout, BufWriter};
|
||||
use std::path::Path;
|
||||
|
|
@ -41,6 +40,7 @@ pub(crate) async fn pip_compile(
|
|||
upgrade_mode: UpgradeMode,
|
||||
index_urls: Option<IndexUrls>,
|
||||
no_build: bool,
|
||||
resolver: Resolver,
|
||||
python_version: Option<PythonVersion>,
|
||||
cache: &Path,
|
||||
mut printer: Printer,
|
||||
|
|
@ -122,8 +122,8 @@ pub(crate) async fn pip_compile(
|
|||
|
||||
// Determine the markers to use for resolution.
|
||||
let markers = python_version.map_or_else(
|
||||
|| Cow::Borrowed(venv.interpreter_info().markers()),
|
||||
|python_version| Cow::Owned(python_version.markers(venv.interpreter_info().markers())),
|
||||
|| venv.interpreter_info().markers().clone(),
|
||||
|python_version| python_version.markers(venv.interpreter_info().markers()),
|
||||
);
|
||||
|
||||
// Instantiate a client.
|
||||
|
|
@ -149,21 +149,35 @@ pub(crate) async fn pip_compile(
|
|||
);
|
||||
|
||||
// Resolve the dependencies.
|
||||
let resolver =
|
||||
puffin_resolver::Resolver::new(manifest, &markers, &tags, &client, &build_dispatch)
|
||||
let resolution = match resolver {
|
||||
Resolver::Pubgrub => {
|
||||
let resolver = puffin_resolver::pubgrub::Resolver::new(
|
||||
manifest,
|
||||
&markers,
|
||||
&tags,
|
||||
&client,
|
||||
&build_dispatch,
|
||||
)
|
||||
.with_reporter(ResolverReporter::from(printer));
|
||||
let resolution = match resolver.resolve().await {
|
||||
Err(puffin_resolver::ResolveError::PubGrub(err)) => {
|
||||
#[allow(clippy::print_stderr)]
|
||||
{
|
||||
let report = miette::Report::msg(format!("{err}"))
|
||||
.context("No solution found when resolving dependencies:");
|
||||
eprint!("{report:?}");
|
||||
}
|
||||
return Ok(ExitStatus::Failure);
|
||||
let resolution = match resolver.resolve().await {
|
||||
Err(puffin_resolver::ResolveError::PubGrub(err)) => {
|
||||
#[allow(clippy::print_stderr)]
|
||||
{
|
||||
let report = miette::Report::msg(format!("{err}"))
|
||||
.context("No solution found when resolving dependencies:");
|
||||
eprint!("{report:?}");
|
||||
}
|
||||
return Ok(ExitStatus::Failure);
|
||||
}
|
||||
result => result,
|
||||
}?;
|
||||
resolution.into()
|
||||
}
|
||||
result => result,
|
||||
}?;
|
||||
Resolver::Resolvo => {
|
||||
puffin_resolver::resolvo::resolve(manifest, markers, tags, client, build_dispatch)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
|
||||
let s = if resolution.len() == 1 { "" } else { "s" };
|
||||
writeln!(
|
||||
|
|
@ -232,6 +246,13 @@ impl From<bool> for UpgradeMode {
|
|||
}
|
||||
}
|
||||
|
||||
/// Whether to allow package upgrades.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
|
||||
pub(crate) enum Resolver {
|
||||
Pubgrub,
|
||||
Resolvo,
|
||||
}
|
||||
|
||||
pub(crate) fn extra_name_with_clap_error(arg: &str) -> Result<ExtraName> {
|
||||
ExtraName::from_str(arg).map_err(|_err| {
|
||||
anyhow!(
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ impl From<Printer> for ResolverReporter {
|
|||
}
|
||||
}
|
||||
|
||||
impl puffin_resolver::ResolverReporter for ResolverReporter {
|
||||
impl puffin_resolver::pubgrub::ResolverReporter for ResolverReporter {
|
||||
fn on_progress(
|
||||
&self,
|
||||
name: &PackageName,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use puffin_normalize::{ExtraName, PackageName};
|
|||
use puffin_resolver::{PreReleaseMode, ResolutionMode};
|
||||
use requirements::ExtrasSpecification;
|
||||
|
||||
use crate::commands::{extra_name_with_clap_error, ExitStatus};
|
||||
use crate::commands::{extra_name_with_clap_error, ExitStatus, Resolver};
|
||||
use crate::index_urls::IndexUrls;
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::requirements::RequirementsSource;
|
||||
|
|
@ -135,6 +135,10 @@ struct PipCompileArgs {
|
|||
#[clap(long)]
|
||||
no_build: bool,
|
||||
|
||||
/// The resolver to use when resolving dependencies.
|
||||
#[arg(long, value_enum)]
|
||||
resolver: Resolver,
|
||||
|
||||
/// The minimum Python version that should be supported.
|
||||
#[arg(long, short, value_enum)]
|
||||
python_version: Option<PythonVersion>,
|
||||
|
|
@ -271,6 +275,7 @@ async fn inner() -> Result<ExitStatus> {
|
|||
args.upgrade.into(),
|
||||
index_urls,
|
||||
args.no_build,
|
||||
args.resolver,
|
||||
args.python_version,
|
||||
&cache_dir,
|
||||
printer,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ use puffin_client::RegistryClient;
|
|||
use puffin_distribution::Metadata;
|
||||
use puffin_installer::{Builder, Downloader, InstallPlan, Installer, Unzipper};
|
||||
use puffin_interpreter::{InterpreterInfo, Virtualenv};
|
||||
use puffin_resolver::{DistFinder, Manifest, PreReleaseMode, ResolutionMode, Resolver};
|
||||
use puffin_resolver::{pubgrub, DistFinder, Manifest, PreReleaseMode, ResolutionMode};
|
||||
use puffin_traits::BuildContext;
|
||||
|
||||
/// The main implementation of [`BuildContext`], used by the CLI, see [`BuildContext`]
|
||||
|
|
@ -78,7 +78,7 @@ impl BuildContext for BuildDispatch {
|
|||
self.interpreter_info.platform(),
|
||||
self.interpreter_info.simple_version(),
|
||||
)?;
|
||||
let resolver = Resolver::new(
|
||||
let resolver = pubgrub::Resolver::new(
|
||||
Manifest::new(
|
||||
requirements.to_vec(),
|
||||
Vec::default(),
|
||||
|
|
|
|||
|
|
@ -29,12 +29,15 @@ anyhow = { workspace = true }
|
|||
bitflags = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive"], optional = true }
|
||||
colored = { workspace = true }
|
||||
derivative = { version = "2.2.0" }
|
||||
fs-err = { workspace = true, features = ["tokio"] }
|
||||
futures = { workspace = true }
|
||||
fxhash = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
petgraph = { workspace = true }
|
||||
resolvo = { version = "0.2.0" }
|
||||
sha2 = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
|
@ -43,8 +46,6 @@ tracing = { workspace = true }
|
|||
url = { workspace = true }
|
||||
waitmap = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
derivative = { version = "2.2.0" }
|
||||
sha2 = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
gourgeist = { path = "../gourgeist" }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,422 @@
|
|||
use std::collections::hash_map::RandomState;
|
||||
use std::collections::BTreeMap;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
use futures::{StreamExt, TryFutureExt};
|
||||
use fxhash::FxHashSet;
|
||||
use tracing::{debug, error, info};
|
||||
use url::Url;
|
||||
use waitmap::{Ref, WaitMap};
|
||||
|
||||
use distribution_filename::{SourceDistFilename, WheelFilename};
|
||||
use pep440_rs::Version;
|
||||
use platform_tags::Tags;
|
||||
use puffin_client::RegistryClient;
|
||||
use puffin_distribution::{
|
||||
BuiltDist, DirectUrlSourceDist, Dist, GitSourceDist, Identifier, Metadata, SourceDist,
|
||||
};
|
||||
use puffin_normalize::PackageName;
|
||||
use puffin_traits::BuildContext;
|
||||
use pypi_types::{Metadata21, SimpleJson};
|
||||
|
||||
use crate::distribution::{BuiltDistFetcher, SourceDistFetcher};
|
||||
use crate::file::{DistFile, SdistFile, WheelFile};
|
||||
use crate::locks::Locks;
|
||||
use crate::ResolveError;
|
||||
|
||||
pub(crate) type VersionMap = BTreeMap<Version, DistFile>;
|
||||
|
||||
pub(crate) struct Database<Context: BuildContext> {
|
||||
client: RegistryClient,
|
||||
tags: Tags,
|
||||
build_context: Context,
|
||||
index: Arc<Index>,
|
||||
locks: Arc<Locks>,
|
||||
in_flight: Arc<Mutex<InFlight>>,
|
||||
}
|
||||
|
||||
impl<Context: BuildContext> Database<Context> {
|
||||
pub(crate) fn new(tags: Tags, client: RegistryClient, build_context: Context) -> Self {
|
||||
Self {
|
||||
client,
|
||||
tags,
|
||||
build_context,
|
||||
index: Arc::default(),
|
||||
locks: Arc::default(),
|
||||
in_flight: Arc::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn listen(
|
||||
&self,
|
||||
receiver: UnboundedReceiver<Request>,
|
||||
) -> Result<(), ResolveError> {
|
||||
self.fetch(receiver).await
|
||||
}
|
||||
|
||||
/// Emit a request to fetch the metadata for a registry-based package.
|
||||
pub(crate) fn fetch_package(
|
||||
&self,
|
||||
sender: &UnboundedSender<Request>,
|
||||
package_name: &PackageName,
|
||||
) -> Result<bool, ResolveError> {
|
||||
Ok(
|
||||
if self.in_flight.lock().unwrap().insert_package(package_name) {
|
||||
sender.unbounded_send(Request::Package(package_name.clone()))?;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Emit a request to fetch the metadata for a direct URL-based package.
|
||||
pub(crate) fn fetch_url(
|
||||
&self,
|
||||
sender: &UnboundedSender<Request>,
|
||||
package_name: &PackageName,
|
||||
url: &Url,
|
||||
) -> Result<bool, ResolveError> {
|
||||
Ok(if self.in_flight.lock().unwrap().insert_url(url) {
|
||||
sender.unbounded_send(Request::Dist(Dist::from_url(
|
||||
package_name.clone(),
|
||||
url.clone(),
|
||||
)))?;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
/// Emit a request to fetch the metadata for an individual distribution.
|
||||
pub(crate) fn fetch_file(
|
||||
&self,
|
||||
sender: &UnboundedSender<Request>,
|
||||
package_name: &PackageName,
|
||||
version: &Version,
|
||||
file: &DistFile,
|
||||
) -> Result<bool, ResolveError> {
|
||||
Ok(if self.in_flight.lock().unwrap().insert_file(file) {
|
||||
let distribution =
|
||||
Dist::from_registry(package_name.clone(), version.clone(), file.clone().into());
|
||||
sender.unbounded_send(Request::Dist(distribution))?;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn get_package(
|
||||
&self,
|
||||
package_name: &PackageName,
|
||||
) -> Option<Ref<PackageName, VersionMap, RandomState>> {
|
||||
self.index.packages.get(package_name)
|
||||
}
|
||||
|
||||
pub(crate) async fn wait_package(
|
||||
&self,
|
||||
sender: &UnboundedSender<Request>,
|
||||
package_name: &PackageName,
|
||||
) -> Ref<PackageName, VersionMap, RandomState> {
|
||||
self.fetch_package(sender, package_name)
|
||||
.expect("Failed to emit request");
|
||||
self.index.packages.wait(package_name).await.unwrap()
|
||||
}
|
||||
|
||||
pub(crate) async fn wait_url(
|
||||
&self,
|
||||
sender: &UnboundedSender<Request>,
|
||||
package_name: &PackageName,
|
||||
url: &Url,
|
||||
) -> Ref<String, Metadata21, RandomState> {
|
||||
self.fetch_url(sender, package_name, url)
|
||||
.expect("Failed to emit request");
|
||||
self.index
|
||||
.distributions
|
||||
.wait(&url.distribution_id())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub(crate) async fn wait_file(
|
||||
&self,
|
||||
sender: &UnboundedSender<Request>,
|
||||
package_name: &PackageName,
|
||||
version: &Version,
|
||||
file: &DistFile,
|
||||
) -> Ref<String, Metadata21, RandomState> {
|
||||
self.fetch_file(sender, package_name, version, file)
|
||||
.expect("Failed to emit request");
|
||||
self.index
|
||||
.distributions
|
||||
.wait(&file.distribution_id())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Fetch the metadata for a stream of packages and versions.
|
||||
async fn fetch(&self, request_stream: UnboundedReceiver<Request>) -> Result<(), ResolveError> {
|
||||
let mut response_stream = request_stream
|
||||
.map(|request| self.process_request(request))
|
||||
.buffer_unordered(50);
|
||||
|
||||
while let Some(response) = response_stream.next().await {
|
||||
match response? {
|
||||
Response::Package(package_name, metadata) => {
|
||||
info!("Received package metadata for: {package_name}");
|
||||
|
||||
// Group the distributions by version and kind, discarding any incompatible
|
||||
// distributions.
|
||||
let mut version_map: VersionMap = BTreeMap::new();
|
||||
for file in metadata.files {
|
||||
if let Ok(filename) = WheelFilename::from_str(file.filename.as_str()) {
|
||||
if filename.is_compatible(&self.tags) {
|
||||
match version_map.entry(filename.version) {
|
||||
std::collections::btree_map::Entry::Occupied(mut entry) => {
|
||||
if matches!(entry.get(), DistFile::Sdist(_)) {
|
||||
// Wheels get precedence over source distributions.
|
||||
entry.insert(DistFile::from(WheelFile(file)));
|
||||
}
|
||||
}
|
||||
std::collections::btree_map::Entry::Vacant(entry) => {
|
||||
entry.insert(DistFile::from(WheelFile(file)));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Ok(filename) =
|
||||
SourceDistFilename::parse(file.filename.as_str(), &package_name)
|
||||
{
|
||||
if let std::collections::btree_map::Entry::Vacant(entry) =
|
||||
version_map.entry(filename.version)
|
||||
{
|
||||
entry.insert(DistFile::from(SdistFile(file)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.index
|
||||
.packages
|
||||
.insert(package_name.clone(), version_map);
|
||||
}
|
||||
Response::Dist(Dist::Built(distribution), metadata, ..) => {
|
||||
info!("Received built distribution metadata for: {distribution}");
|
||||
self.index
|
||||
.distributions
|
||||
.insert(distribution.distribution_id(), metadata);
|
||||
}
|
||||
Response::Dist(Dist::Source(distribution), metadata, precise) => {
|
||||
info!("Received source distribution metadata for: {distribution}");
|
||||
self.index
|
||||
.distributions
|
||||
.insert(distribution.distribution_id(), metadata);
|
||||
if let Some(precise) = precise {
|
||||
match distribution {
|
||||
SourceDist::DirectUrl(sdist) => {
|
||||
self.index.redirects.insert(sdist.url.clone(), precise);
|
||||
}
|
||||
SourceDist::Git(sdist) => {
|
||||
self.index.redirects.insert(sdist.url.clone(), precise);
|
||||
}
|
||||
SourceDist::Registry(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), ResolveError>(())
|
||||
}
|
||||
|
||||
async fn process_request(&self, request: Request) -> Result<Response, ResolveError> {
|
||||
match request {
|
||||
// Fetch package metadata from the registry.
|
||||
Request::Package(package_name) => {
|
||||
info!("Fetching package metadata for: {package_name}");
|
||||
|
||||
self.client
|
||||
.simple(package_name.clone())
|
||||
.map_ok(move |metadata| Response::Package(package_name, metadata))
|
||||
.map_err(ResolveError::Client)
|
||||
.await
|
||||
}
|
||||
|
||||
// Fetch wheel metadata.
|
||||
Request::Dist(Dist::Built(distribution)) => {
|
||||
info!("Fetching built distribution metadata for: {distribution}");
|
||||
|
||||
let metadata = match &distribution {
|
||||
BuiltDist::Registry(wheel) => {
|
||||
self.client
|
||||
.wheel_metadata(wheel.file.clone())
|
||||
.map_err(ResolveError::Client)
|
||||
.await?
|
||||
}
|
||||
BuiltDist::DirectUrl(wheel) => {
|
||||
let fetcher = BuiltDistFetcher::new(self.build_context.cache());
|
||||
match fetcher.find_dist_info(wheel, &self.tags) {
|
||||
Ok(Some(metadata)) => {
|
||||
debug!("Found wheel metadata in cache: {wheel}");
|
||||
metadata
|
||||
}
|
||||
Ok(None) => {
|
||||
debug!("Downloading wheel: {wheel}");
|
||||
fetcher.download_wheel(wheel, &self.client).await.map_err(
|
||||
|err| ResolveError::from_built_dist(distribution.clone(), err),
|
||||
)?
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to read wheel from cache: {err}");
|
||||
fetcher.download_wheel(wheel, &self.client).await.map_err(
|
||||
|err| ResolveError::from_built_dist(distribution.clone(), err),
|
||||
)?
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if metadata.name != *distribution.name() {
|
||||
return Err(ResolveError::NameMismatch {
|
||||
metadata: metadata.name,
|
||||
given: distribution.name().clone(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Response::Dist(Dist::Built(distribution), metadata, None))
|
||||
}
|
||||
|
||||
// Fetch source distribution metadata.
|
||||
Request::Dist(Dist::Source(sdist)) => {
|
||||
info!("Fetching source distribution metadata for: {sdist}");
|
||||
|
||||
let lock = self.locks.acquire(&sdist).await;
|
||||
let _guard = lock.lock().await;
|
||||
|
||||
let fetcher = SourceDistFetcher::new(&self.build_context);
|
||||
|
||||
let precise = fetcher
|
||||
.precise(&sdist)
|
||||
.await
|
||||
.map_err(|err| ResolveError::from_source_dist(sdist.clone(), err))?;
|
||||
|
||||
let metadata = {
|
||||
// Insert the `precise`, if it exists.
|
||||
let sdist = match sdist.clone() {
|
||||
SourceDist::DirectUrl(sdist) => {
|
||||
SourceDist::DirectUrl(DirectUrlSourceDist {
|
||||
url: precise.clone().unwrap_or_else(|| sdist.url.clone()),
|
||||
..sdist
|
||||
})
|
||||
}
|
||||
SourceDist::Git(sdist) => SourceDist::Git(GitSourceDist {
|
||||
url: precise.clone().unwrap_or_else(|| sdist.url.clone()),
|
||||
..sdist
|
||||
}),
|
||||
sdist @ SourceDist::Registry(_) => sdist,
|
||||
};
|
||||
|
||||
match fetcher.find_dist_info(&sdist, &self.tags) {
|
||||
Ok(Some(metadata)) => {
|
||||
debug!("Found source distribution metadata in cache: {sdist}");
|
||||
metadata
|
||||
}
|
||||
Ok(None) => {
|
||||
debug!("Downloading source distribution: {sdist}");
|
||||
fetcher
|
||||
.download_and_build_sdist(&sdist, &self.client)
|
||||
.await
|
||||
.map_err(|err| ResolveError::from_source_dist(sdist.clone(), err))?
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to read source distribution from cache: {err}",);
|
||||
fetcher
|
||||
.download_and_build_sdist(&sdist, &self.client)
|
||||
.await
|
||||
.map_err(|err| ResolveError::from_source_dist(sdist.clone(), err))?
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if metadata.name != *sdist.name() {
|
||||
return Err(ResolveError::NameMismatch {
|
||||
metadata: metadata.name,
|
||||
given: sdist.name().clone(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Response::Dist(Dist::Source(sdist), metadata, precise))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch the metadata for an item
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub(crate) enum Request {
|
||||
/// A request to fetch the metadata for a package.
|
||||
Package(PackageName),
|
||||
/// A request to fetch the metadata for a built or source distribution.
|
||||
Dist(Dist),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub(crate) enum Response {
|
||||
/// The returned metadata for a package hosted on a registry.
|
||||
Package(PackageName, SimpleJson),
|
||||
/// The returned metadata for a distribution.
|
||||
Dist(Dist, Metadata21, Option<Url>),
|
||||
}
|
||||
|
||||
/// In-memory index of in-flight network requests. Any request in an [`InFlight`] state will be
|
||||
/// eventually be inserted into an [`Index`].
|
||||
#[derive(Debug, Default)]
|
||||
struct InFlight {
|
||||
/// The set of requested [`PackageName`]s.
|
||||
packages: FxHashSet<PackageName>,
|
||||
/// The set of requested registry-based files, represented by their SHAs.
|
||||
files: FxHashSet<String>,
|
||||
/// The set of requested URLs.
|
||||
urls: FxHashSet<Url>,
|
||||
}
|
||||
|
||||
impl InFlight {
|
||||
fn insert_package(&mut self, package_name: &PackageName) -> bool {
|
||||
self.packages.insert(package_name.clone())
|
||||
}
|
||||
|
||||
fn insert_file(&mut self, file: &DistFile) -> bool {
|
||||
match file {
|
||||
DistFile::Wheel(file) => self.files.insert(file.hashes.sha256.clone()),
|
||||
DistFile::Sdist(file) => self.files.insert(file.hashes.sha256.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_url(&mut self, url: &Url) -> bool {
|
||||
self.urls.insert(url.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// In-memory index of package metadata.
|
||||
struct Index {
|
||||
/// A map from package name to the metadata for that package.
|
||||
packages: WaitMap<PackageName, VersionMap>,
|
||||
|
||||
/// A map from distribution SHA to metadata for that distribution.
|
||||
distributions: WaitMap<String, Metadata21>,
|
||||
|
||||
/// A map from source URL to precise URL.
|
||||
redirects: WaitMap<Url, Url>,
|
||||
}
|
||||
|
||||
impl Default for Index {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
packages: WaitMap::new(),
|
||||
distributions: WaitMap::new(),
|
||||
redirects: WaitMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,8 +9,7 @@ use pep508_rs::Requirement;
|
|||
use puffin_distribution::{BuiltDist, SourceDist};
|
||||
use puffin_normalize::PackageName;
|
||||
|
||||
use crate::pubgrub::{PubGrubPackage, PubGrubVersion};
|
||||
use crate::ResolutionFailureReporter;
|
||||
use crate::pubgrub::{PubGrubPackage, PubGrubVersion, ResolutionFailureReporter};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ResolveError {
|
||||
|
|
@ -32,6 +31,9 @@ pub enum ResolveError {
|
|||
#[error(transparent)]
|
||||
PubGrub(#[from] RichPubGrubError),
|
||||
|
||||
#[error(transparent)]
|
||||
Resolvo(anyhow::Error),
|
||||
|
||||
#[error("Package metadata name `{metadata}` does not match given name `{given}`")]
|
||||
NameMismatch {
|
||||
given: PackageName,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use puffin_distribution::Identifier;
|
||||
use std::ops::Deref;
|
||||
|
||||
use pypi_types::File;
|
||||
|
|
@ -72,3 +73,19 @@ impl From<DistFile> for File {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Identifier for DistFile {
|
||||
fn distribution_id(&self) -> String {
|
||||
match self {
|
||||
DistFile::Wheel(file) => file.distribution_id(),
|
||||
DistFile::Sdist(file) => file.distribution_id(),
|
||||
}
|
||||
}
|
||||
|
||||
fn resource_id(&self) -> String {
|
||||
match self {
|
||||
DistFile::Wheel(file) => file.resource_id(),
|
||||
DistFile::Sdist(file) => file.resource_id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,10 @@ pub use error::ResolveError;
|
|||
pub use finder::{DistFinder, Reporter as FinderReporter};
|
||||
pub use manifest::Manifest;
|
||||
pub use prerelease_mode::PreReleaseMode;
|
||||
pub use pubgrub::ResolutionFailureReporter;
|
||||
pub use resolution::Graph;
|
||||
pub use resolution_mode::ResolutionMode;
|
||||
pub use resolver::{BuildId, Reporter as ResolverReporter, Resolver};
|
||||
|
||||
mod candidate_selector;
|
||||
mod database;
|
||||
mod distribution;
|
||||
mod error;
|
||||
mod file;
|
||||
|
|
@ -15,7 +13,7 @@ mod finder;
|
|||
mod locks;
|
||||
mod manifest;
|
||||
mod prerelease_mode;
|
||||
mod pubgrub;
|
||||
pub mod pubgrub;
|
||||
mod resolution;
|
||||
mod resolution_mode;
|
||||
mod resolver;
|
||||
pub mod resolvo;
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ use puffin_normalize::PackageName;
|
|||
|
||||
use crate::file::DistFile;
|
||||
use crate::prerelease_mode::PreReleaseStrategy;
|
||||
use crate::pubgrub::resolver::VersionMap;
|
||||
use crate::pubgrub::PubGrubVersion;
|
||||
use crate::resolution_mode::ResolutionStrategy;
|
||||
use crate::resolver::VersionMap;
|
||||
use crate::Manifest;
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -1,14 +1,16 @@
|
|||
pub(crate) use crate::pubgrub::candidate_selector::CandidateSelector;
|
||||
pub(crate) use crate::pubgrub::dependencies::PubGrubDependencies;
|
||||
pub(crate) use crate::pubgrub::package::PubGrubPackage;
|
||||
pub(crate) use crate::pubgrub::priority::{PubGrubPriorities, PubGrubPriority};
|
||||
pub use crate::pubgrub::report::ResolutionFailureReporter;
|
||||
|
||||
pub use crate::pubgrub::resolver::{BuildId, Reporter as ResolverReporter, Resolver};
|
||||
pub(crate) use crate::pubgrub::version::{PubGrubVersion, MIN_VERSION};
|
||||
|
||||
pub(crate) use crate::pubgrub::dependencies::PubGrubDependencies;
|
||||
|
||||
mod candidate_selector;
|
||||
mod dependencies;
|
||||
mod package;
|
||||
mod priority;
|
||||
mod report;
|
||||
mod resolver;
|
||||
mod specifier;
|
||||
mod version;
|
||||
|
|
|
|||
|
|
@ -30,14 +30,14 @@ use puffin_normalize::{ExtraName, PackageName};
|
|||
use puffin_traits::BuildContext;
|
||||
use pypi_types::{File, Metadata21, SimpleJson};
|
||||
|
||||
use crate::candidate_selector::CandidateSelector;
|
||||
use crate::distribution::{BuiltDistFetcher, SourceDistFetcher, SourceDistributionReporter};
|
||||
use crate::error::ResolveError;
|
||||
use crate::file::{DistFile, SdistFile, WheelFile};
|
||||
use crate::locks::Locks;
|
||||
use crate::manifest::Manifest;
|
||||
use crate::pubgrub::{
|
||||
PubGrubDependencies, PubGrubPackage, PubGrubPriorities, PubGrubVersion, MIN_VERSION,
|
||||
CandidateSelector, PubGrubDependencies, PubGrubPackage, PubGrubPriorities, PubGrubVersion,
|
||||
MIN_VERSION,
|
||||
};
|
||||
use crate::resolution::Graph;
|
||||
|
||||
|
|
@ -2,6 +2,7 @@ use std::hash::BuildHasherDefault;
|
|||
|
||||
use colored::Colorize;
|
||||
use fxhash::FxHashMap;
|
||||
use itertools::Itertools;
|
||||
use petgraph::visit::EdgeRef;
|
||||
use pubgrub::range::Range;
|
||||
use pubgrub::solver::{Kind, State};
|
||||
|
|
@ -48,6 +49,26 @@ impl Resolution {
|
|||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Resolution {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
for dist in self.0.values().sorted_unstable_by_key(|dist| dist.name()) {
|
||||
writeln!(f, "{dist}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Graph> for Resolution {
|
||||
fn from(value: Graph) -> Self {
|
||||
let mut packages =
|
||||
FxHashMap::with_capacity_and_hasher(value.len(), BuildHasherDefault::default());
|
||||
for package in value.0.node_indices().map(|node| &value.0[node]) {
|
||||
packages.insert(package.name().clone(), package.clone());
|
||||
}
|
||||
Self(packages)
|
||||
}
|
||||
}
|
||||
|
||||
/// A complete resolution graph in which every node represents a pinned package and every edge
|
||||
/// represents a dependency between two pinned packages.
|
||||
#[derive(Debug)]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
pub(crate) use package::ResolvoPackage;
|
||||
pub use resolver::resolve;
|
||||
pub(crate) use version::{ResolvoVersion, ResolvoVersionSet};
|
||||
|
||||
mod package;
|
||||
mod provider;
|
||||
mod resolver;
|
||||
mod version;
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
use puffin_normalize::{ExtraName, PackageName};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub(crate) enum ResolvoPackage {
|
||||
Package(PackageName),
|
||||
Extra(PackageName, ExtraName),
|
||||
}
|
||||
|
||||
impl ResolvoPackage {
|
||||
/// Return the [`PackageName`] of the [`ResolvoPackage`].
|
||||
pub(crate) fn name(&self) -> &PackageName {
|
||||
match self {
|
||||
ResolvoPackage::Package(name) => name,
|
||||
ResolvoPackage::Extra(name, ..) => name,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the [`ExtraName`] of the [`ResolvoPackage`], if any.
|
||||
pub(crate) fn extra(&self) -> Option<&ExtraName> {
|
||||
match self {
|
||||
ResolvoPackage::Package(_) => None,
|
||||
ResolvoPackage::Extra(_, extra) => Some(extra),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ResolvoPackage {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ResolvoPackage::Package(name) => write!(f, "{name}"),
|
||||
ResolvoPackage::Extra(name, extra) => write!(f, "{name}[{extra}]"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use futures::channel::mpsc::UnboundedSender;
|
||||
use resolvo::{Candidates, Dependencies, NameId, Pool, SolvableId, SolverCache};
|
||||
use tokio::runtime::Handle;
|
||||
use tracing::info;
|
||||
|
||||
use pep440_rs::{VersionSpecifier, VersionSpecifiers};
|
||||
use pep508_rs::{MarkerEnvironment, VersionOrUrl};
|
||||
use puffin_distribution::Dist;
|
||||
use puffin_traits::BuildContext;
|
||||
|
||||
use crate::database::{Database, Request};
|
||||
use crate::file::DistFile;
|
||||
use crate::resolvo::{ResolvoPackage, ResolvoVersion, ResolvoVersionSet};
|
||||
|
||||
/// A [`resolvo::DependencyProvider`] that uses a [`Database`] to fetch dependencies.
|
||||
pub(crate) struct ResolvoDependencyProvider<Context: BuildContext> {
|
||||
database: Arc<Database<Context>>,
|
||||
sender: UnboundedSender<Request>,
|
||||
markers: MarkerEnvironment,
|
||||
pool: Pool<ResolvoVersionSet, ResolvoPackage>,
|
||||
}
|
||||
|
||||
impl<Context: BuildContext> ResolvoDependencyProvider<Context> {
|
||||
/// Initialize a new [`ResolvoDependencyProvider`] with the given [`Database`].
|
||||
pub(crate) fn new(
|
||||
database: Arc<Database<Context>>,
|
||||
sender: UnboundedSender<Request>,
|
||||
markers: MarkerEnvironment,
|
||||
) -> Self {
|
||||
Self {
|
||||
database,
|
||||
sender,
|
||||
markers,
|
||||
pool: Pool::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the underlying [`Pool`].
|
||||
pub(crate) fn pool(&self) -> &Pool<ResolvoVersionSet, ResolvoPackage> {
|
||||
&self.pool
|
||||
}
|
||||
|
||||
/// Convert a [`SolvableId`] into a [`Dist`].
|
||||
pub(crate) fn dist(&self, solvable: SolvableId) -> Dist {
|
||||
let solvable = self.pool.resolve_solvable(solvable);
|
||||
let package = self.pool.resolve_package_name(solvable.name_id());
|
||||
match solvable.inner() {
|
||||
ResolvoVersion::Version(version) => {
|
||||
let metadata = self.database.get_package(package.name()).unwrap();
|
||||
let version_map = metadata.value();
|
||||
let file = version_map.get(&version.clone()).unwrap();
|
||||
match file {
|
||||
DistFile::Wheel(file) => Dist::from_registry(
|
||||
package.name().clone(),
|
||||
version.clone(),
|
||||
file.clone().into(),
|
||||
),
|
||||
DistFile::Sdist(file) => Dist::from_registry(
|
||||
package.name().clone(),
|
||||
version.clone(),
|
||||
file.clone().into(),
|
||||
),
|
||||
}
|
||||
}
|
||||
ResolvoVersion::Url(url) => Dist::from_url(package.name().clone(), url.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Context: BuildContext> resolvo::DependencyProvider<ResolvoVersionSet, ResolvoPackage>
|
||||
for &ResolvoDependencyProvider<Context>
|
||||
{
|
||||
fn pool(&self) -> &Pool<ResolvoVersionSet, ResolvoPackage> {
|
||||
&self.pool
|
||||
}
|
||||
|
||||
/// Sort candidates such that the highest version is preferred.
|
||||
fn sort_candidates(
|
||||
&self,
|
||||
solver: &SolverCache<ResolvoVersionSet, ResolvoPackage, Self>,
|
||||
solvables: &mut [SolvableId],
|
||||
) {
|
||||
solvables.sort_by_key(|&solvable| {
|
||||
let solvable = solver.pool().resolve_solvable(solvable);
|
||||
std::cmp::Reverse(solvable.inner())
|
||||
});
|
||||
}
|
||||
|
||||
/// Return all candidate distributions for a given package.
|
||||
fn get_candidates(&self, name: NameId) -> Option<Candidates> {
|
||||
let package = self.pool.resolve_package_name(name);
|
||||
let package_name = package.name();
|
||||
|
||||
info!("Fetching candidates for: {package_name}");
|
||||
|
||||
// Get the metadata for this package, which includes the `VersionMap`.
|
||||
let entry = tokio::task::block_in_place(|| {
|
||||
Handle::current().block_on(self.database.wait_package(&self.sender, package.name()))
|
||||
});
|
||||
let version_map = entry.value();
|
||||
|
||||
// Create a candidate for each version in the `VersionMap`.
|
||||
let mut candidates = Candidates::default();
|
||||
for version in version_map.keys() {
|
||||
// TODO(charlie): Implement proper pre-release support.
|
||||
if version.any_prerelease() {
|
||||
continue;
|
||||
}
|
||||
let solvable_id = self
|
||||
.pool
|
||||
.intern_solvable(name, ResolvoVersion::Version(version.clone()));
|
||||
candidates.candidates.push(solvable_id);
|
||||
}
|
||||
|
||||
Some(candidates)
|
||||
}
|
||||
|
||||
fn get_dependencies(&self, solvable: SolvableId) -> Dependencies {
|
||||
let solvable = self.pool.resolve_solvable(solvable);
|
||||
let package = self.pool.resolve_package_name(solvable.name_id());
|
||||
let package_name = package.name();
|
||||
let extra = package.extra();
|
||||
|
||||
info!("Fetching dependencies for: {package_name}");
|
||||
|
||||
let entry = match solvable.inner() {
|
||||
ResolvoVersion::Version(version) => {
|
||||
let metadata = self.database.get_package(package_name).unwrap();
|
||||
let version_map = metadata.value();
|
||||
let file = version_map.get(&version.clone()).unwrap();
|
||||
tokio::task::block_in_place(|| {
|
||||
Handle::current().block_on(self.database.wait_file(
|
||||
&self.sender,
|
||||
package_name,
|
||||
version,
|
||||
file,
|
||||
))
|
||||
})
|
||||
}
|
||||
ResolvoVersion::Url(url) => tokio::task::block_in_place(|| {
|
||||
Handle::current().block_on(self.database.wait_url(&self.sender, package_name, url))
|
||||
}),
|
||||
};
|
||||
let metadata = entry.value();
|
||||
|
||||
let mut dependencies = Dependencies::default();
|
||||
|
||||
match package {
|
||||
ResolvoPackage::Package(package_name) => {
|
||||
// Ensure that extra packages are pinned to the same version as the base package.
|
||||
for extra in &metadata.provides_extras {
|
||||
let solvable = self.pool.intern_package_name(ResolvoPackage::Extra(
|
||||
package_name.clone(),
|
||||
extra.clone(),
|
||||
));
|
||||
let specifiers =
|
||||
VersionSpecifiers::from_iter([VersionSpecifier::equals_version(
|
||||
metadata.version.clone(),
|
||||
)]);
|
||||
let version_set_id = self.pool.intern_version_set(
|
||||
solvable,
|
||||
Some(VersionOrUrl::VersionSpecifier(specifiers)).into(),
|
||||
);
|
||||
dependencies.constrains.push(version_set_id);
|
||||
}
|
||||
}
|
||||
ResolvoPackage::Extra(package_name, _extra) => {
|
||||
// Mark the extra as a dependency of the base package.
|
||||
let ResolvoVersion::Version(package_version) = solvable.inner() else {
|
||||
unreachable!("extra should only be set for registry packages");
|
||||
};
|
||||
|
||||
let base_name_id = self
|
||||
.pool
|
||||
.lookup_package_name(&ResolvoPackage::Package(package_name.clone()))
|
||||
.expect("extra should have base");
|
||||
let specifiers = VersionSpecifiers::from_iter([VersionSpecifier::equals_version(
|
||||
package_version.clone(),
|
||||
)]);
|
||||
let version_set_id = self.pool.intern_version_set(
|
||||
base_name_id,
|
||||
Some(VersionOrUrl::VersionSpecifier(specifiers)).into(),
|
||||
);
|
||||
dependencies.requirements.push(version_set_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate over all declared requirements.
|
||||
for requirement in &metadata.requires_dist {
|
||||
// If the requirement isn't relevant for the current platform, skip it.
|
||||
if let Some(extra) = extra {
|
||||
if !requirement.evaluate_markers(&self.markers, &[extra.as_ref()]) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if !requirement.evaluate_markers(&self.markers, &[]) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Add a dependency on the package itself.
|
||||
let dependency_name_id = self
|
||||
.pool
|
||||
.intern_package_name(ResolvoPackage::Package(requirement.name.clone()));
|
||||
let version_set_id = self.pool.intern_version_set(
|
||||
dependency_name_id,
|
||||
requirement.version_or_url.clone().into(),
|
||||
);
|
||||
dependencies.requirements.push(version_set_id);
|
||||
|
||||
// Add an additional package for each extra.
|
||||
for extra in requirement.extras.iter().flatten() {
|
||||
let dependency_name_id = self.pool.intern_package_name(ResolvoPackage::Extra(
|
||||
requirement.name.clone(),
|
||||
extra.clone(),
|
||||
));
|
||||
let version_set_id = self.pool.intern_version_set(
|
||||
dependency_name_id,
|
||||
requirement.version_or_url.clone().into(),
|
||||
);
|
||||
dependencies.requirements.push(version_set_id);
|
||||
}
|
||||
}
|
||||
|
||||
dependencies
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use fxhash::FxHashMap;
|
||||
use resolvo::DefaultSolvableDisplay;
|
||||
use resolvo::Solver;
|
||||
|
||||
use pep508_rs::MarkerEnvironment;
|
||||
use platform_tags::Tags;
|
||||
use puffin_client::RegistryClient;
|
||||
use puffin_traits::BuildContext;
|
||||
|
||||
use crate::database::Database;
|
||||
use crate::resolution::Resolution;
|
||||
use crate::resolvo::provider::ResolvoDependencyProvider;
|
||||
use crate::resolvo::ResolvoPackage;
|
||||
use crate::{Manifest, ResolveError};
|
||||
|
||||
/// Resolve a [`Manifest`] into a [`Resolution`].
|
||||
pub async fn resolve<Context: BuildContext + Send + Sync + 'static>(
|
||||
manifest: Manifest,
|
||||
markers: MarkerEnvironment,
|
||||
tags: Tags,
|
||||
client: RegistryClient,
|
||||
build_context: Context,
|
||||
) -> Result<Resolution, ResolveError> {
|
||||
let database = Arc::new(Database::new(tags, client, build_context));
|
||||
|
||||
// A channel to fetch package metadata (e.g., given `flask`, fetch all versions) and version
|
||||
// metadata (e.g., given `flask==1.0.0`, fetch the metadata for that version).
|
||||
let (request_sink, request_stream) = futures::channel::mpsc::unbounded();
|
||||
|
||||
// Run the fetcher.
|
||||
let requests_fut = database.listen(request_stream);
|
||||
|
||||
// Construct a provider
|
||||
let provider = ResolvoDependencyProvider::new(database.clone(), request_sink, markers);
|
||||
|
||||
// Generate the root requirements.
|
||||
let pool = provider.pool();
|
||||
let mut root_requirements = Vec::with_capacity(manifest.requirements.len());
|
||||
for requirement in &manifest.requirements {
|
||||
let package_name =
|
||||
pool.intern_package_name(ResolvoPackage::Package(requirement.name.clone()));
|
||||
let version_set_id =
|
||||
pool.intern_version_set(package_name, requirement.version_or_url.clone().into());
|
||||
root_requirements.push(version_set_id);
|
||||
|
||||
for extra in requirement.extras.iter().flatten() {
|
||||
let dependency_package_name = pool.intern_package_name(ResolvoPackage::Extra(
|
||||
requirement.name.clone(),
|
||||
extra.clone(),
|
||||
));
|
||||
let version_set_id = pool.intern_version_set(
|
||||
dependency_package_name,
|
||||
requirement.version_or_url.clone().into(),
|
||||
);
|
||||
root_requirements.push(version_set_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the solver.
|
||||
let resolve_fut = tokio::task::spawn_blocking(move || solve(&provider, root_requirements));
|
||||
|
||||
// The requests stream should terminate before the solver.
|
||||
requests_fut.await?;
|
||||
let resolution = resolve_fut.await??;
|
||||
|
||||
Ok(resolution)
|
||||
}
|
||||
|
||||
/// Run the Resolvo solver.
|
||||
fn solve<Context: BuildContext>(
|
||||
provider: &ResolvoDependencyProvider<Context>,
|
||||
root_requirements: Vec<resolvo::VersionSetId>,
|
||||
) -> Result<Resolution, ResolveError> {
|
||||
// Run the solver itself.
|
||||
let mut solver = Solver::new(provider);
|
||||
let solvables = match solver.solve(root_requirements) {
|
||||
Ok(solvables) => solvables,
|
||||
Err(err) => {
|
||||
return Err(ResolveError::Resolvo(anyhow::anyhow!(
|
||||
"{}",
|
||||
err.display_user_friendly(&solver, &DefaultSolvableDisplay)
|
||||
.to_string()
|
||||
.trim()
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
// Convert the solution to a `Resolution`.
|
||||
let pool = provider.pool();
|
||||
let mut packages = FxHashMap::default();
|
||||
for solvable_id in solvables {
|
||||
let solvable = pool.resolve_solvable(solvable_id);
|
||||
let package = pool.resolve_package_name(solvable.name_id());
|
||||
packages.insert(package.name().clone(), provider.dist(solvable_id));
|
||||
}
|
||||
|
||||
Ok(Resolution::new(packages))
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
use resolvo::VersionSet;
|
||||
|
||||
/// A wrapper around [`pep508_rs::VersionOrUrl`] that implements [`VersionSet`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct ResolvoVersionSet(Option<pep508_rs::VersionOrUrl>);
|
||||
|
||||
impl From<Option<pep508_rs::VersionOrUrl>> for ResolvoVersionSet {
|
||||
fn from(value: Option<pep508_rs::VersionOrUrl>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ResolvoVersionSet {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match &self.0 {
|
||||
None => write!(f, "*"),
|
||||
Some(pep508_rs::VersionOrUrl::VersionSpecifier(specifiers)) => {
|
||||
write!(f, "{specifiers}")
|
||||
}
|
||||
Some(pep508_rs::VersionOrUrl::Url(url)) => write!(f, "{url}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) enum ResolvoVersion {
|
||||
Version(pep440_rs::Version),
|
||||
Url(url::Url),
|
||||
}
|
||||
|
||||
impl VersionSet for ResolvoVersionSet {
|
||||
type V = ResolvoVersion;
|
||||
|
||||
fn contains(&self, version: &Self::V) -> bool {
|
||||
match (self.0.as_ref(), version) {
|
||||
(
|
||||
Some(pep508_rs::VersionOrUrl::VersionSpecifier(specifiers)),
|
||||
ResolvoVersion::Version(version),
|
||||
) => specifiers.contains(version),
|
||||
(Some(pep508_rs::VersionOrUrl::Url(url_a)), ResolvoVersion::Url(url_b)) => {
|
||||
url_a == url_b
|
||||
}
|
||||
(None, _) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ResolvoVersion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ResolvoVersion::Version(v) => write!(f, "{v}"),
|
||||
ResolvoVersion::Url(u) => write!(f, "{u}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ use platform_host::{Arch, Os, Platform};
|
|||
use platform_tags::Tags;
|
||||
use puffin_client::RegistryClientBuilder;
|
||||
use puffin_interpreter::{InterpreterInfo, Virtualenv};
|
||||
use puffin_resolver::{Manifest, PreReleaseMode, ResolutionMode, Resolver};
|
||||
use puffin_resolver::{Manifest, PreReleaseMode, ResolutionMode};
|
||||
use puffin_traits::BuildContext;
|
||||
|
||||
struct DummyContext;
|
||||
|
|
@ -77,7 +77,13 @@ async fn black() -> Result<()> {
|
|||
None,
|
||||
);
|
||||
|
||||
let resolver = Resolver::new(manifest, &MARKERS_311, &TAGS_311, &client, &DummyContext);
|
||||
let resolver = puffin_resolver::pubgrub::Resolver::new(
|
||||
manifest,
|
||||
&MARKERS_311,
|
||||
&TAGS_311,
|
||||
&client,
|
||||
&DummyContext,
|
||||
);
|
||||
let resolution = resolver.resolve().await?;
|
||||
|
||||
insta::assert_display_snapshot!(resolution);
|
||||
|
|
@ -101,7 +107,13 @@ async fn black_colorama() -> Result<()> {
|
|||
None,
|
||||
);
|
||||
|
||||
let resolver = Resolver::new(manifest, &MARKERS_311, &TAGS_311, &client, &DummyContext);
|
||||
let resolver = puffin_resolver::pubgrub::Resolver::new(
|
||||
manifest,
|
||||
&MARKERS_311,
|
||||
&TAGS_311,
|
||||
&client,
|
||||
&DummyContext,
|
||||
);
|
||||
let resolution = resolver.resolve().await?;
|
||||
|
||||
insta::assert_display_snapshot!(resolution);
|
||||
|
|
@ -125,7 +137,13 @@ async fn black_python_310() -> Result<()> {
|
|||
None,
|
||||
);
|
||||
|
||||
let resolver = Resolver::new(manifest, &MARKERS_310, &TAGS_310, &client, &DummyContext);
|
||||
let resolver = puffin_resolver::pubgrub::Resolver::new(
|
||||
manifest,
|
||||
&MARKERS_310,
|
||||
&TAGS_310,
|
||||
&client,
|
||||
&DummyContext,
|
||||
);
|
||||
let resolution = resolver.resolve().await?;
|
||||
|
||||
insta::assert_display_snapshot!(resolution);
|
||||
|
|
@ -151,7 +169,13 @@ async fn black_mypy_extensions() -> Result<()> {
|
|||
None,
|
||||
);
|
||||
|
||||
let resolver = Resolver::new(manifest, &MARKERS_311, &TAGS_311, &client, &DummyContext);
|
||||
let resolver = puffin_resolver::pubgrub::Resolver::new(
|
||||
manifest,
|
||||
&MARKERS_311,
|
||||
&TAGS_311,
|
||||
&client,
|
||||
&DummyContext,
|
||||
);
|
||||
let resolution = resolver.resolve().await?;
|
||||
|
||||
insta::assert_display_snapshot!(resolution);
|
||||
|
|
@ -177,7 +201,13 @@ async fn black_mypy_extensions_extra() -> Result<()> {
|
|||
None,
|
||||
);
|
||||
|
||||
let resolver = Resolver::new(manifest, &MARKERS_311, &TAGS_311, &client, &DummyContext);
|
||||
let resolver = puffin_resolver::pubgrub::Resolver::new(
|
||||
manifest,
|
||||
&MARKERS_311,
|
||||
&TAGS_311,
|
||||
&client,
|
||||
&DummyContext,
|
||||
);
|
||||
let resolution = resolver.resolve().await?;
|
||||
|
||||
insta::assert_display_snapshot!(resolution);
|
||||
|
|
@ -203,7 +233,13 @@ async fn black_flake8() -> Result<()> {
|
|||
None,
|
||||
);
|
||||
|
||||
let resolver = Resolver::new(manifest, &MARKERS_311, &TAGS_311, &client, &DummyContext);
|
||||
let resolver = puffin_resolver::pubgrub::Resolver::new(
|
||||
manifest,
|
||||
&MARKERS_311,
|
||||
&TAGS_311,
|
||||
&client,
|
||||
&DummyContext,
|
||||
);
|
||||
let resolution = resolver.resolve().await?;
|
||||
|
||||
insta::assert_display_snapshot!(resolution);
|
||||
|
|
@ -227,7 +263,13 @@ async fn black_lowest() -> Result<()> {
|
|||
None,
|
||||
);
|
||||
|
||||
let resolver = Resolver::new(manifest, &MARKERS_311, &TAGS_311, &client, &DummyContext);
|
||||
let resolver = puffin_resolver::pubgrub::Resolver::new(
|
||||
manifest,
|
||||
&MARKERS_311,
|
||||
&TAGS_311,
|
||||
&client,
|
||||
&DummyContext,
|
||||
);
|
||||
let resolution = resolver.resolve().await?;
|
||||
|
||||
insta::assert_display_snapshot!(resolution);
|
||||
|
|
@ -251,7 +293,13 @@ async fn black_lowest_direct() -> Result<()> {
|
|||
None,
|
||||
);
|
||||
|
||||
let resolver = Resolver::new(manifest, &MARKERS_311, &TAGS_311, &client, &DummyContext);
|
||||
let resolver = puffin_resolver::pubgrub::Resolver::new(
|
||||
manifest,
|
||||
&MARKERS_311,
|
||||
&TAGS_311,
|
||||
&client,
|
||||
&DummyContext,
|
||||
);
|
||||
let resolution = resolver.resolve().await?;
|
||||
|
||||
insta::assert_display_snapshot!(resolution);
|
||||
|
|
@ -275,7 +323,13 @@ async fn black_respect_preference() -> Result<()> {
|
|||
None,
|
||||
);
|
||||
|
||||
let resolver = Resolver::new(manifest, &MARKERS_311, &TAGS_311, &client, &DummyContext);
|
||||
let resolver = puffin_resolver::pubgrub::Resolver::new(
|
||||
manifest,
|
||||
&MARKERS_311,
|
||||
&TAGS_311,
|
||||
&client,
|
||||
&DummyContext,
|
||||
);
|
||||
let resolution = resolver.resolve().await?;
|
||||
|
||||
insta::assert_display_snapshot!(resolution);
|
||||
|
|
@ -299,7 +353,13 @@ async fn black_ignore_preference() -> Result<()> {
|
|||
None,
|
||||
);
|
||||
|
||||
let resolver = Resolver::new(manifest, &MARKERS_311, &TAGS_311, &client, &DummyContext);
|
||||
let resolver = puffin_resolver::pubgrub::Resolver::new(
|
||||
manifest,
|
||||
&MARKERS_311,
|
||||
&TAGS_311,
|
||||
&client,
|
||||
&DummyContext,
|
||||
);
|
||||
let resolution = resolver.resolve().await?;
|
||||
|
||||
insta::assert_display_snapshot!(resolution);
|
||||
|
|
@ -323,7 +383,13 @@ async fn black_disallow_prerelease() -> Result<()> {
|
|||
None,
|
||||
);
|
||||
|
||||
let resolver = Resolver::new(manifest, &MARKERS_311, &TAGS_311, &client, &DummyContext);
|
||||
let resolver = puffin_resolver::pubgrub::Resolver::new(
|
||||
manifest,
|
||||
&MARKERS_311,
|
||||
&TAGS_311,
|
||||
&client,
|
||||
&DummyContext,
|
||||
);
|
||||
let err = resolver.resolve().await.unwrap_err();
|
||||
|
||||
insta::assert_display_snapshot!(err);
|
||||
|
|
@ -347,7 +413,13 @@ async fn black_allow_prerelease_if_necessary() -> Result<()> {
|
|||
None,
|
||||
);
|
||||
|
||||
let resolver = Resolver::new(manifest, &MARKERS_311, &TAGS_311, &client, &DummyContext);
|
||||
let resolver = puffin_resolver::pubgrub::Resolver::new(
|
||||
manifest,
|
||||
&MARKERS_311,
|
||||
&TAGS_311,
|
||||
&client,
|
||||
&DummyContext,
|
||||
);
|
||||
let resolution = resolver.resolve().await.unwrap_err();
|
||||
|
||||
insta::assert_display_snapshot!(resolution);
|
||||
|
|
@ -371,7 +443,13 @@ async fn pylint_disallow_prerelease() -> Result<()> {
|
|||
None,
|
||||
);
|
||||
|
||||
let resolver = Resolver::new(manifest, &MARKERS_311, &TAGS_311, &client, &DummyContext);
|
||||
let resolver = puffin_resolver::pubgrub::Resolver::new(
|
||||
manifest,
|
||||
&MARKERS_311,
|
||||
&TAGS_311,
|
||||
&client,
|
||||
&DummyContext,
|
||||
);
|
||||
let resolution = resolver.resolve().await?;
|
||||
|
||||
insta::assert_display_snapshot!(resolution);
|
||||
|
|
@ -395,7 +473,13 @@ async fn pylint_allow_prerelease() -> Result<()> {
|
|||
None,
|
||||
);
|
||||
|
||||
let resolver = Resolver::new(manifest, &MARKERS_311, &TAGS_311, &client, &DummyContext);
|
||||
let resolver = puffin_resolver::pubgrub::Resolver::new(
|
||||
manifest,
|
||||
&MARKERS_311,
|
||||
&TAGS_311,
|
||||
&client,
|
||||
&DummyContext,
|
||||
);
|
||||
let resolution = resolver.resolve().await?;
|
||||
|
||||
insta::assert_display_snapshot!(resolution);
|
||||
|
|
@ -422,7 +506,13 @@ async fn pylint_allow_explicit_prerelease_without_marker() -> Result<()> {
|
|||
None,
|
||||
);
|
||||
|
||||
let resolver = Resolver::new(manifest, &MARKERS_311, &TAGS_311, &client, &DummyContext);
|
||||
let resolver = puffin_resolver::pubgrub::Resolver::new(
|
||||
manifest,
|
||||
&MARKERS_311,
|
||||
&TAGS_311,
|
||||
&client,
|
||||
&DummyContext,
|
||||
);
|
||||
let resolution = resolver.resolve().await?;
|
||||
|
||||
insta::assert_display_snapshot!(resolution);
|
||||
|
|
@ -449,7 +539,13 @@ async fn pylint_allow_explicit_prerelease_with_marker() -> Result<()> {
|
|||
None,
|
||||
);
|
||||
|
||||
let resolver = Resolver::new(manifest, &MARKERS_311, &TAGS_311, &client, &DummyContext);
|
||||
let resolver = puffin_resolver::pubgrub::Resolver::new(
|
||||
manifest,
|
||||
&MARKERS_311,
|
||||
&TAGS_311,
|
||||
&client,
|
||||
&DummyContext,
|
||||
);
|
||||
let resolution = resolver.resolve().await?;
|
||||
|
||||
insta::assert_display_snapshot!(resolution);
|
||||
|
|
|
|||
|
|
@ -2,10 +2,7 @@
|
|||
# A small set of pure-Python packages.
|
||||
###
|
||||
packaging>=23.1
|
||||
pygls>=1.0.1
|
||||
lsprotocol>=2023.0.0a1
|
||||
ruff>=0.0.274
|
||||
flask @ git+https://github.com/pallets/flask.git@d92b64a
|
||||
typing_extensions
|
||||
scipy
|
||||
numpy
|
||||
|
|
@ -23,10 +20,8 @@ trio<0.20
|
|||
trio-websocket
|
||||
trio-asyncio
|
||||
trio-typing
|
||||
trio-protocol
|
||||
fastapi
|
||||
typer
|
||||
pydantic
|
||||
uvicorn
|
||||
traitlets
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue