Preserve hashes for pinned packages (#2532)
## Summary When a user runs with `--output-file` and `--generate-hashes`, we should _only_ update the hashes if the pinned version itself changes. Closes https://github.com/astral-sh/uv/issues/1530.
This commit is contained in:
Generated
+1
@@ -4798,6 +4798,7 @@ dependencies = [
|
||||
"platform-tags",
|
||||
"pubgrub",
|
||||
"pypi-types",
|
||||
"requirements-txt",
|
||||
"reqwest",
|
||||
"rkyv",
|
||||
"rustc-hash",
|
||||
|
||||
@@ -162,3 +162,93 @@ impl Hashes {
|
||||
self.sha256.as_deref().or(self.md5.as_deref())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Hashes {
|
||||
type Err = HashError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut parts = s.split(':');
|
||||
|
||||
// Extract the key and value.
|
||||
let name = parts
|
||||
.next()
|
||||
.ok_or_else(|| HashError::InvalidStructure(s.to_string()))?;
|
||||
let value = parts
|
||||
.next()
|
||||
.ok_or_else(|| HashError::InvalidStructure(s.to_string()))?;
|
||||
|
||||
// Ensure there are no more parts.
|
||||
if parts.next().is_some() {
|
||||
return Err(HashError::InvalidStructure(s.to_string()));
|
||||
}
|
||||
|
||||
match name {
|
||||
"md5" => {
|
||||
let md5 = value.to_string();
|
||||
Ok(Hashes {
|
||||
md5: Some(md5),
|
||||
sha256: None,
|
||||
})
|
||||
}
|
||||
"sha256" => {
|
||||
let sha256 = value.to_string();
|
||||
Ok(Hashes {
|
||||
md5: None,
|
||||
sha256: Some(sha256),
|
||||
})
|
||||
}
|
||||
_ => Err(HashError::UnsupportedHashAlgorithm(s.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum HashError {
|
||||
#[error("Unexpected hash (expected `sha256:<hash>` or `md5:<hash>`) on: {0}")]
|
||||
InvalidStructure(String),
|
||||
|
||||
#[error("Unsupported hash algorithm (expected `sha256` or `md5`) on: {0}")]
|
||||
UnsupportedHashAlgorithm(String),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{HashError, Hashes};
|
||||
|
||||
#[test]
|
||||
fn parse_hashes() -> Result<(), HashError> {
|
||||
let hashes: Hashes =
|
||||
"sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".parse()?;
|
||||
assert_eq!(
|
||||
hashes,
|
||||
Hashes {
|
||||
md5: None,
|
||||
sha256: Some(
|
||||
"40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".to_string()
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
let hashes: Hashes =
|
||||
"md5:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2".parse()?;
|
||||
assert_eq!(
|
||||
hashes,
|
||||
Hashes {
|
||||
md5: Some(
|
||||
"090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2".to_string()
|
||||
),
|
||||
sha256: None
|
||||
}
|
||||
);
|
||||
|
||||
let result = "sha256=40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"
|
||||
.parse::<Hashes>();
|
||||
assert!(result.is_err());
|
||||
|
||||
let result = "sha512:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"
|
||||
.parse::<Hashes>();
|
||||
assert!(result.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ pub enum Error {
|
||||
#[error("Unexpected fragment (expected `#sha256=...`) on URL: {0}")]
|
||||
FragmentParse(String),
|
||||
|
||||
#[error("Unsupported hash algorithm (expected `sha256`) on: {0}")]
|
||||
#[error("Unsupported hash algorithm (expected `sha256` or `md5`) on: {0}")]
|
||||
UnsupportedHashAlgorithm(String),
|
||||
|
||||
#[error("Invalid `requires-python` specifier: {0}")]
|
||||
@@ -672,7 +672,7 @@ mod tests {
|
||||
"#;
|
||||
let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap();
|
||||
let result = SimpleHtml::parse(text, &base).unwrap_err();
|
||||
insta::assert_snapshot!(result, @"Unsupported hash algorithm (expected `sha256`) on: sha512=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61");
|
||||
insta::assert_snapshot!(result, @"Unsupported hash algorithm (expected `sha256` or `md5`) on: sha512=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -22,6 +22,7 @@ pep440_rs = { path = "../pep440-rs", features = ["pubgrub"] }
|
||||
pep508_rs = { path = "../pep508-rs" }
|
||||
platform-tags = { path = "../platform-tags" }
|
||||
pypi-types = { path = "../pypi-types" }
|
||||
requirements-txt = { path = "../requirements-txt" }
|
||||
uv-cache = { path = "../uv-cache" }
|
||||
uv-client = { path = "../uv-client" }
|
||||
uv-distribution = { path = "../uv-distribution" }
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use pubgrub::range::Range;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use distribution_types::{CompatibleDist, IncompatibleDist, IncompatibleSource};
|
||||
use distribution_types::{DistributionMetadata, IncompatibleWheel, Name, PrioritizedDist};
|
||||
use pep440_rs::Version;
|
||||
use pep508_rs::{MarkerEnvironment, Requirement, VersionOrUrl};
|
||||
use pep508_rs::MarkerEnvironment;
|
||||
use uv_normalize::PackageName;
|
||||
|
||||
use crate::preferences::Preferences;
|
||||
use crate::prerelease_mode::PreReleaseStrategy;
|
||||
use crate::resolution_mode::ResolutionStrategy;
|
||||
use crate::version_map::{VersionMap, VersionMapDistHandle};
|
||||
@@ -16,7 +16,6 @@ use crate::{Manifest, Options};
|
||||
pub(crate) struct CandidateSelector {
|
||||
resolution_strategy: ResolutionStrategy,
|
||||
prerelease_strategy: PreReleaseStrategy,
|
||||
preferences: Preferences,
|
||||
}
|
||||
|
||||
impl CandidateSelector {
|
||||
@@ -37,7 +36,6 @@ impl CandidateSelector {
|
||||
manifest,
|
||||
markers,
|
||||
),
|
||||
preferences: Preferences::from_requirements(manifest.preferences.as_slice(), markers),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,43 +52,6 @@ impl CandidateSelector {
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of pinned packages that should be preserved during resolution, if possible.
|
||||
#[derive(Debug, Clone)]
|
||||
struct Preferences(FxHashMap<PackageName, Version>);
|
||||
|
||||
impl Preferences {
|
||||
/// Create a set of [`Preferences`] from a set of requirements.
|
||||
fn from_requirements(requirements: &[Requirement], markers: &MarkerEnvironment) -> Self {
|
||||
Self(
|
||||
requirements
|
||||
.iter()
|
||||
.filter_map(|requirement| {
|
||||
if !requirement.evaluate_markers(markers, &[]) {
|
||||
return None;
|
||||
}
|
||||
let Some(VersionOrUrl::VersionSpecifier(version_specifiers)) =
|
||||
requirement.version_or_url.as_ref()
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
let [version_specifier] = version_specifiers.as_ref() else {
|
||||
return None;
|
||||
};
|
||||
Some((
|
||||
requirement.name.clone(),
|
||||
version_specifier.version().clone(),
|
||||
))
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Return the pinned version for a package, if any.
|
||||
fn get(&self, package_name: &PackageName) -> Option<&Version> {
|
||||
self.0.get(package_name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum AllowPreRelease {
|
||||
Yes,
|
||||
@@ -105,10 +66,11 @@ impl CandidateSelector {
|
||||
package_name: &'a PackageName,
|
||||
range: &'a Range<Version>,
|
||||
version_map: &'a VersionMap,
|
||||
preferences: &'a Preferences,
|
||||
) -> Option<Candidate<'a>> {
|
||||
// If the package has a preference (e.g., an existing version from an existing lockfile),
|
||||
// and the preference satisfies the current range, use that.
|
||||
if let Some(version) = self.preferences.get(package_name) {
|
||||
if let Some(version) = preferences.version(package_name) {
|
||||
if range.contains(version) {
|
||||
if let Some(file) = version_map.get(version) {
|
||||
return Some(Candidate::new(package_name, version, file));
|
||||
|
||||
@@ -3,6 +3,7 @@ pub use error::ResolveError;
|
||||
pub use finder::{DistFinder, Reporter as FinderReporter};
|
||||
pub use manifest::Manifest;
|
||||
pub use options::{Options, OptionsBuilder};
|
||||
pub use preferences::Preference;
|
||||
pub use prerelease_mode::PreReleaseMode;
|
||||
pub use python_requirement::PythonRequirement;
|
||||
pub use resolution::{AnnotationStyle, Diagnostic, DisplayResolutionGraph, ResolutionGraph};
|
||||
@@ -24,6 +25,7 @@ mod manifest;
|
||||
mod options;
|
||||
mod overrides;
|
||||
mod pins;
|
||||
mod preferences;
|
||||
mod prerelease_mode;
|
||||
mod pubgrub;
|
||||
mod python_requirement;
|
||||
|
||||
@@ -3,13 +3,15 @@ use pep508_rs::Requirement;
|
||||
use pypi_types::Metadata23;
|
||||
use uv_normalize::PackageName;
|
||||
|
||||
use crate::preferences::Preference;
|
||||
|
||||
/// A manifest of requirements, constraints, and preferences.
|
||||
#[derive(Debug)]
|
||||
pub struct Manifest {
|
||||
pub(crate) requirements: Vec<Requirement>,
|
||||
pub(crate) constraints: Vec<Requirement>,
|
||||
pub(crate) overrides: Vec<Requirement>,
|
||||
pub(crate) preferences: Vec<Requirement>,
|
||||
pub(crate) preferences: Vec<Preference>,
|
||||
pub(crate) project: Option<PackageName>,
|
||||
pub(crate) editables: Vec<(LocalEditable, Metadata23)>,
|
||||
}
|
||||
@@ -19,7 +21,7 @@ impl Manifest {
|
||||
requirements: Vec<Requirement>,
|
||||
constraints: Vec<Requirement>,
|
||||
overrides: Vec<Requirement>,
|
||||
preferences: Vec<Requirement>,
|
||||
preferences: Vec<Preference>,
|
||||
project: Option<PackageName>,
|
||||
editables: Vec<(LocalEditable, Metadata23)>,
|
||||
) -> Self {
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use pep440_rs::{Operator, Version};
|
||||
use pep508_rs::{MarkerEnvironment, Requirement, VersionOrUrl};
|
||||
use pypi_types::{HashError, Hashes};
|
||||
use requirements_txt::RequirementEntry;
|
||||
use uv_normalize::PackageName;
|
||||
|
||||
/// A pinned requirement, as extracted from a `requirements.txt` file.
|
||||
#[derive(Debug)]
|
||||
pub struct Preference {
|
||||
requirement: Requirement,
|
||||
hashes: Vec<Hashes>,
|
||||
}
|
||||
|
||||
impl Preference {
|
||||
/// Create a [`Preference`] from a [`RequirementEntry`].
|
||||
pub fn from_entry(entry: RequirementEntry) -> Result<Self, HashError> {
|
||||
Ok(Self {
|
||||
requirement: entry.requirement,
|
||||
hashes: entry
|
||||
.hashes
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.map(Hashes::from_str)
|
||||
.collect::<Result<_, _>>()?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a [`Preference`] from a [`Requirement`].
|
||||
pub fn from_requirement(requirement: Requirement) -> Self {
|
||||
Self {
|
||||
requirement,
|
||||
hashes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the name of the package for this preference.
|
||||
pub fn name(&self) -> &PackageName {
|
||||
&self.requirement.name
|
||||
}
|
||||
|
||||
/// Return the [`Requirement`] for this preference.
|
||||
pub fn requirement(&self) -> &Requirement {
|
||||
&self.requirement
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of pinned packages that should be preserved during resolution, if possible.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Preferences(FxHashMap<PackageName, Pin>);
|
||||
|
||||
impl Preferences {
|
||||
/// Create a map of pinned packages from a list of [`Preference`] entries.
|
||||
pub(crate) fn from_requirements(
|
||||
requirements: Vec<Preference>,
|
||||
markers: &MarkerEnvironment,
|
||||
) -> Self {
|
||||
Self(
|
||||
requirements
|
||||
.into_iter()
|
||||
.filter_map(|preference| {
|
||||
let Preference {
|
||||
requirement,
|
||||
hashes,
|
||||
} = preference;
|
||||
|
||||
// Search for, e.g., `flask==1.2.3` entries that match the current environment.
|
||||
if !requirement.evaluate_markers(markers, &[]) {
|
||||
return None;
|
||||
}
|
||||
let Some(VersionOrUrl::VersionSpecifier(version_specifiers)) =
|
||||
requirement.version_or_url.as_ref()
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
let [version_specifier] = version_specifiers.as_ref() else {
|
||||
return None;
|
||||
};
|
||||
if *version_specifier.operator() != Operator::Equal {
|
||||
return None;
|
||||
}
|
||||
Some((
|
||||
requirement.name,
|
||||
Pin {
|
||||
version: version_specifier.version().clone(),
|
||||
hashes,
|
||||
},
|
||||
))
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Return the pinned version for a package, if any.
|
||||
pub(crate) fn version(&self, package_name: &PackageName) -> Option<&Version> {
|
||||
self.0.get(package_name).map(Pin::version)
|
||||
}
|
||||
|
||||
/// Return the hashes for a package, if the version matches that of the pin.
|
||||
pub(crate) fn match_hashes(
|
||||
&self,
|
||||
package_name: &PackageName,
|
||||
version: &Version,
|
||||
) -> Option<&[Hashes]> {
|
||||
self.0
|
||||
.get(package_name)
|
||||
.filter(|pin| pin.version() == version)
|
||||
.map(Pin::hashes)
|
||||
}
|
||||
}
|
||||
|
||||
/// The pinned data associated with a package in a locked `requirements.txt` file (e.g., `flask==1.2.3`).
|
||||
#[derive(Debug, Clone)]
|
||||
struct Pin {
|
||||
version: Version,
|
||||
hashes: Vec<Hashes>,
|
||||
}
|
||||
|
||||
impl Pin {
|
||||
/// Return the version of the pinned package.
|
||||
fn version(&self) -> &Version {
|
||||
&self.version
|
||||
}
|
||||
|
||||
/// Return the hashes of the pinned package.
|
||||
fn hashes(&self) -> &[Hashes] {
|
||||
&self.hashes
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ use uv_normalize::{ExtraName, PackageName};
|
||||
|
||||
use crate::editables::Editables;
|
||||
use crate::pins::FilePins;
|
||||
use crate::preferences::Preferences;
|
||||
use crate::pubgrub::{PubGrubDistribution, PubGrubPackage, PubGrubPriority};
|
||||
use crate::redirect::apply_redirect;
|
||||
use crate::resolver::VersionsResponse;
|
||||
@@ -53,6 +54,7 @@ pub struct ResolutionGraph {
|
||||
|
||||
impl ResolutionGraph {
|
||||
/// Create a new graph from the resolved `PubGrub` state.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn from_state(
|
||||
selection: &SelectedDependencies<PubGrubPackage, Version>,
|
||||
pins: &FilePins,
|
||||
@@ -60,6 +62,7 @@ impl ResolutionGraph {
|
||||
distributions: &OnceMap<PackageId, Metadata23>,
|
||||
redirects: &DashMap<Url, Url>,
|
||||
state: &State<PubGrubPackage, Range<Version>, PubGrubPriority>,
|
||||
preferences: &Preferences,
|
||||
editables: Editables,
|
||||
) -> Result<Self, ResolveError> {
|
||||
// TODO(charlie): petgraph is a really heavy and unnecessary dependency here. We should
|
||||
@@ -84,13 +87,16 @@ impl ResolutionGraph {
|
||||
.clone()
|
||||
};
|
||||
|
||||
// Add its hashes to the index.
|
||||
if let Some(versions_response) = packages.get(package_name) {
|
||||
// Add its hashes to the index, preserving those that were already present in
|
||||
// the lockfile if necessary.
|
||||
if let Some(hash) = preferences.match_hashes(package_name, version) {
|
||||
hashes.insert(package_name.clone(), hash.to_vec());
|
||||
} else if let Some(versions_response) = packages.get(package_name) {
|
||||
if let VersionsResponse::Found(ref version_map) = *versions_response {
|
||||
hashes.insert(package_name.clone(), {
|
||||
let mut hashes = version_map.hashes(version);
|
||||
hashes.sort_unstable();
|
||||
hashes
|
||||
let mut hash = version_map.hashes(version);
|
||||
hash.sort_unstable();
|
||||
hash
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -111,13 +117,16 @@ impl ResolutionGraph {
|
||||
Dist::from_url(package_name.clone(), url)?
|
||||
};
|
||||
|
||||
// Add its hashes to the index.
|
||||
if let Some(versions_response) = packages.get(package_name) {
|
||||
// Add its hashes to the index, preserving those that were already present in
|
||||
// the lockfile if necessary.
|
||||
if let Some(hash) = preferences.match_hashes(package_name, version) {
|
||||
hashes.insert(package_name.clone(), hash.to_vec());
|
||||
} else if let Some(versions_response) = packages.get(package_name) {
|
||||
if let VersionsResponse::Found(ref version_map) = *versions_response {
|
||||
hashes.insert(package_name.clone(), {
|
||||
let mut hashes = version_map.hashes(version);
|
||||
hashes.sort_unstable();
|
||||
hashes
|
||||
let mut hash = version_map.hashes(version);
|
||||
hash.sort_unstable();
|
||||
hash
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ use crate::error::ResolveError;
|
||||
use crate::manifest::Manifest;
|
||||
use crate::overrides::Overrides;
|
||||
use crate::pins::FilePins;
|
||||
use crate::preferences::Preferences;
|
||||
use crate::pubgrub::{
|
||||
PubGrubDependencies, PubGrubDistribution, PubGrubPackage, PubGrubPriorities, PubGrubPython,
|
||||
PubGrubSpecifier,
|
||||
@@ -93,6 +94,7 @@ pub struct Resolver<'a, Provider: ResolverProvider> {
|
||||
requirements: Vec<Requirement>,
|
||||
constraints: Constraints,
|
||||
overrides: Overrides,
|
||||
preferences: Preferences,
|
||||
editables: Editables,
|
||||
urls: Urls,
|
||||
locals: Locals,
|
||||
@@ -169,6 +171,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
|
||||
requirements: manifest.requirements,
|
||||
constraints: Constraints::from_requirements(manifest.constraints),
|
||||
overrides: Overrides::from_requirements(manifest.overrides),
|
||||
preferences: Preferences::from_requirements(manifest.preferences, markers),
|
||||
editables: Editables::from_requirements(manifest.editables),
|
||||
markers,
|
||||
python_requirement,
|
||||
@@ -278,6 +281,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
|
||||
&self.index.distributions,
|
||||
&self.index.redirects,
|
||||
&state,
|
||||
&self.preferences,
|
||||
self.editables.clone(),
|
||||
);
|
||||
};
|
||||
@@ -641,7 +645,10 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
|
||||
}
|
||||
|
||||
// Find a version.
|
||||
let Some(candidate) = self.selector.select(package_name, range, version_map) else {
|
||||
let Some(candidate) =
|
||||
self.selector
|
||||
.select(package_name, range, version_map, &self.preferences)
|
||||
else {
|
||||
// Short circuit: we couldn't find _any_ versions for a package.
|
||||
return Ok(None);
|
||||
};
|
||||
@@ -1002,7 +1009,9 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
|
||||
|
||||
// Try to find a compatible version. If there aren't any compatible versions,
|
||||
// short-circuit.
|
||||
let Some(candidate) = self.selector.select(&package_name, &range, version_map)
|
||||
let Some(candidate) =
|
||||
self.selector
|
||||
.select(&package_name, &range, version_map, &self.preferences)
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ use pep440_rs::Version;
|
||||
use pep508_rs::MarkerEnvironment;
|
||||
use uv_normalize::PackageName;
|
||||
|
||||
use crate::preferences::Preference;
|
||||
use crate::Manifest;
|
||||
|
||||
/// A set of package versions that are permitted, even if they're marked as yanked by the
|
||||
@@ -19,7 +20,7 @@ impl AllowedYanks {
|
||||
.iter()
|
||||
.chain(manifest.constraints.iter())
|
||||
.chain(manifest.overrides.iter())
|
||||
.chain(manifest.preferences.iter())
|
||||
.chain(manifest.preferences.iter().map(Preference::requirement))
|
||||
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
|
||||
.chain(manifest.editables.iter().flat_map(|(editable, metadata)| {
|
||||
metadata
|
||||
|
||||
@@ -18,7 +18,7 @@ use uv_client::{FlatIndex, RegistryClientBuilder};
|
||||
use uv_interpreter::{find_default_python, Interpreter, PythonEnvironment};
|
||||
use uv_resolver::{
|
||||
DisplayResolutionGraph, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode,
|
||||
ResolutionGraph, ResolutionMode, Resolver,
|
||||
Preference, ResolutionGraph, ResolutionMode, Resolver,
|
||||
};
|
||||
use uv_traits::{
|
||||
BuildContext, BuildIsolation, BuildKind, NoBinary, NoBuild, SetupPyStrategy, SourceBuildTrait,
|
||||
@@ -422,10 +422,12 @@ async fn black_lowest_direct() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn black_respect_preference() -> Result<()> {
|
||||
let manifest = Manifest::new(
|
||||
vec![Requirement::from_str("black<=23.9.1").unwrap()],
|
||||
vec![Requirement::from_str("black<=23.9.1")?],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![Requirement::from_str("black==23.9.0").unwrap()],
|
||||
vec![Preference::from_requirement(Requirement::from_str(
|
||||
"black==23.9.0",
|
||||
)?)],
|
||||
None,
|
||||
vec![],
|
||||
);
|
||||
@@ -455,10 +457,12 @@ async fn black_respect_preference() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn black_ignore_preference() -> Result<()> {
|
||||
let manifest = Manifest::new(
|
||||
vec![Requirement::from_str("black<=23.9.1").unwrap()],
|
||||
vec![Requirement::from_str("black<=23.9.1")?],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![Requirement::from_str("black==23.9.2").unwrap()],
|
||||
vec![Preference::from_requirement(Requirement::from_str(
|
||||
"black==23.9.2",
|
||||
)?)],
|
||||
None,
|
||||
vec![],
|
||||
);
|
||||
|
||||
@@ -31,7 +31,7 @@ use uv_installer::{
|
||||
use uv_interpreter::{Interpreter, PythonEnvironment};
|
||||
use uv_normalize::PackageName;
|
||||
use uv_resolver::{
|
||||
DependencyMode, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode,
|
||||
DependencyMode, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode, Preference,
|
||||
ResolutionGraph, ResolutionMode, Resolver,
|
||||
};
|
||||
use uv_traits::{BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
|
||||
@@ -500,7 +500,8 @@ async fn resolve(
|
||||
// Prefer current site packages, unless in the upgrade or reinstall lists
|
||||
site_packages
|
||||
.requirements()
|
||||
.filter(|requirement| !exclusions.contains(&requirement.name))
|
||||
.map(Preference::from_requirement)
|
||||
.filter(|preference| !exclusions.contains(preference.name()))
|
||||
.collect()
|
||||
};
|
||||
|
||||
|
||||
@@ -11,10 +11,12 @@ use tracing::{instrument, Level};
|
||||
|
||||
use distribution_types::{FlatIndexLocation, IndexUrl};
|
||||
use pep508_rs::Requirement;
|
||||
use pypi_types::HashError;
|
||||
use requirements_txt::{EditableRequirement, FindLink, RequirementsTxt};
|
||||
use uv_client::Connectivity;
|
||||
use uv_fs::Simplified;
|
||||
use uv_normalize::{ExtraName, PackageName};
|
||||
use uv_resolver::Preference;
|
||||
use uv_warnings::warn_user;
|
||||
|
||||
use crate::commands::Upgrade;
|
||||
@@ -445,7 +447,7 @@ fn flatten_extra(
|
||||
pub(crate) async fn read_lockfile(
|
||||
output_file: Option<&Path>,
|
||||
upgrade: Upgrade,
|
||||
) -> Result<Vec<Requirement>> {
|
||||
) -> Result<Vec<Preference>> {
|
||||
// As an optimization, skip reading the lockfile is we're upgrading all packages anyway.
|
||||
let Some(output_file) = output_file
|
||||
.filter(|_| !upgrade.is_all())
|
||||
@@ -458,28 +460,23 @@ pub(crate) async fn read_lockfile(
|
||||
let requirements_txt =
|
||||
RequirementsTxt::parse(output_file, std::env::current_dir()?, Connectivity::Offline)
|
||||
.await?;
|
||||
let requirements = requirements_txt
|
||||
let preferences = requirements_txt
|
||||
.requirements
|
||||
.into_iter()
|
||||
.filter_map(|entry| {
|
||||
if entry.editable {
|
||||
None
|
||||
} else {
|
||||
Some(entry.requirement)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.filter(|entry| !entry.editable)
|
||||
.map(Preference::from_entry)
|
||||
.collect::<Result<Vec<_>, HashError>>()?;
|
||||
|
||||
// Apply the upgrade strategy to the requirements.
|
||||
Ok(match upgrade {
|
||||
// Respect all pinned versions from the existing lockfile.
|
||||
Upgrade::None => requirements,
|
||||
Upgrade::None => preferences,
|
||||
// Ignore all pinned versions from the existing lockfile.
|
||||
Upgrade::All => vec![],
|
||||
// Ignore pinned versions for the specified packages.
|
||||
Upgrade::Packages(packages) => requirements
|
||||
Upgrade::Packages(packages) => preferences
|
||||
.into_iter()
|
||||
.filter(|requirement| !packages.contains(&requirement.name))
|
||||
.filter(|preference| !packages.contains(preference.name()))
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5208,3 +5208,237 @@ fn requirement_wheel_name_mismatch() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `--generate-hashes` should not update the hashes in the "lockfile" if the package is not
|
||||
/// upgraded.
|
||||
#[test]
|
||||
fn preserve_hashes_no_upgrade() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
requirements_in.write_str("markupsafe")?;
|
||||
|
||||
// Write a subset of the hashes to the "lockfile".
|
||||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||||
requirements_txt.write_str(indoc! {r"
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile requirements.in --python-version 3.12 --cache-dir [CACHE_DIR]
|
||||
markupsafe==2.1.2 \
|
||||
--hash=sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed \
|
||||
--hash=sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc \
|
||||
--hash=sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2
|
||||
"})?;
|
||||
|
||||
// Avoid adding any additional hashes to the "lockfile".
|
||||
uv_snapshot!(context.compile()
|
||||
.arg("requirements.in")
|
||||
.arg("--output-file")
|
||||
.arg("requirements.txt")
|
||||
.arg("--generate-hashes"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2023-11-18T12:00:00Z requirements.in --output-file requirements.txt --generate-hashes
|
||||
markupsafe==2.1.2 \
|
||||
--hash=sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed \
|
||||
--hash=sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc \
|
||||
--hash=sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2
|
||||
|
||||
----- stderr -----
|
||||
Resolved 1 package in [TIME]
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `--generate-hashes` should update the hashes in the "lockfile" if the package is upgraded via
|
||||
/// `--upgrade`.
|
||||
#[test]
|
||||
fn preserve_hashes_upgrade() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
requirements_in.write_str("markupsafe==2.1.2")?;
|
||||
|
||||
// Write a subset of the hashes to the "lockfile".
|
||||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||||
requirements_txt.write_str(indoc! {r"
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile requirements.in --python-version 3.12 --cache-dir [CACHE_DIR]
|
||||
markupsafe==2.1.2 \
|
||||
--hash=sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed \
|
||||
--hash=sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc \
|
||||
--hash=sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2
|
||||
"})?;
|
||||
|
||||
// Requesting an upgrade should update the hashes, even if the version didn't change.
|
||||
uv_snapshot!(context.compile()
|
||||
.arg("requirements.in")
|
||||
.arg("--output-file")
|
||||
.arg("requirements.txt")
|
||||
.arg("--generate-hashes")
|
||||
.arg("--upgrade"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2023-11-18T12:00:00Z requirements.in --output-file requirements.txt --generate-hashes
|
||||
markupsafe==2.1.2 \
|
||||
--hash=sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed \
|
||||
--hash=sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc \
|
||||
--hash=sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2 \
|
||||
--hash=sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460 \
|
||||
--hash=sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7 \
|
||||
--hash=sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0 \
|
||||
--hash=sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1 \
|
||||
--hash=sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa \
|
||||
--hash=sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03 \
|
||||
--hash=sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323 \
|
||||
--hash=sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65 \
|
||||
--hash=sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013 \
|
||||
--hash=sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036 \
|
||||
--hash=sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f \
|
||||
--hash=sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4 \
|
||||
--hash=sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419 \
|
||||
--hash=sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2 \
|
||||
--hash=sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619 \
|
||||
--hash=sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a \
|
||||
--hash=sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a \
|
||||
--hash=sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd \
|
||||
--hash=sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7 \
|
||||
--hash=sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666 \
|
||||
--hash=sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65 \
|
||||
--hash=sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859 \
|
||||
--hash=sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625 \
|
||||
--hash=sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff \
|
||||
--hash=sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156 \
|
||||
--hash=sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd \
|
||||
--hash=sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba \
|
||||
--hash=sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f \
|
||||
--hash=sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1 \
|
||||
--hash=sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094 \
|
||||
--hash=sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a \
|
||||
--hash=sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513 \
|
||||
--hash=sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed \
|
||||
--hash=sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d \
|
||||
--hash=sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3 \
|
||||
--hash=sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147 \
|
||||
--hash=sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c \
|
||||
--hash=sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603 \
|
||||
--hash=sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601 \
|
||||
--hash=sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a \
|
||||
--hash=sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1 \
|
||||
--hash=sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d \
|
||||
--hash=sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3 \
|
||||
--hash=sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54 \
|
||||
--hash=sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2 \
|
||||
--hash=sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6 \
|
||||
--hash=sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58
|
||||
|
||||
----- stderr -----
|
||||
Resolved 1 package in [TIME]
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `--generate-hashes` should update the hashes in the "lockfile" if the package is upgraded due
|
||||
/// to a change in requirements.
|
||||
#[test]
|
||||
fn preserve_hashes_newer_version() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
requirements_in.write_str("markupsafe==2.1.3")?;
|
||||
|
||||
// Write a subset of the hashes to the "lockfile".
|
||||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||||
requirements_txt.write_str(indoc! {r"
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile requirements.in --python-version 3.12 --cache-dir [CACHE_DIR]
|
||||
markupsafe==2.1.2 \
|
||||
--hash=sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed \
|
||||
--hash=sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc \
|
||||
--hash=sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2
|
||||
"})?;
|
||||
|
||||
// Requesting a more specific version should update the hashes.
|
||||
uv_snapshot!(context.compile()
|
||||
.arg("requirements.in")
|
||||
.arg("--output-file")
|
||||
.arg("requirements.txt")
|
||||
.arg("--generate-hashes"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2023-11-18T12:00:00Z requirements.in --output-file requirements.txt --generate-hashes
|
||||
markupsafe==2.1.3 \
|
||||
--hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \
|
||||
--hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \
|
||||
--hash=sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431 \
|
||||
--hash=sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686 \
|
||||
--hash=sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c \
|
||||
--hash=sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559 \
|
||||
--hash=sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc \
|
||||
--hash=sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb \
|
||||
--hash=sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939 \
|
||||
--hash=sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c \
|
||||
--hash=sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0 \
|
||||
--hash=sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4 \
|
||||
--hash=sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9 \
|
||||
--hash=sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575 \
|
||||
--hash=sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba \
|
||||
--hash=sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d \
|
||||
--hash=sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd \
|
||||
--hash=sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3 \
|
||||
--hash=sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00 \
|
||||
--hash=sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155 \
|
||||
--hash=sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac \
|
||||
--hash=sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52 \
|
||||
--hash=sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f \
|
||||
--hash=sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8 \
|
||||
--hash=sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b \
|
||||
--hash=sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007 \
|
||||
--hash=sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24 \
|
||||
--hash=sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea \
|
||||
--hash=sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198 \
|
||||
--hash=sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0 \
|
||||
--hash=sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee \
|
||||
--hash=sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be \
|
||||
--hash=sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2 \
|
||||
--hash=sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1 \
|
||||
--hash=sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707 \
|
||||
--hash=sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6 \
|
||||
--hash=sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c \
|
||||
--hash=sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58 \
|
||||
--hash=sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823 \
|
||||
--hash=sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779 \
|
||||
--hash=sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636 \
|
||||
--hash=sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c \
|
||||
--hash=sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad \
|
||||
--hash=sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee \
|
||||
--hash=sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc \
|
||||
--hash=sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2 \
|
||||
--hash=sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48 \
|
||||
--hash=sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7 \
|
||||
--hash=sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e \
|
||||
--hash=sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b \
|
||||
--hash=sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa \
|
||||
--hash=sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5 \
|
||||
--hash=sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e \
|
||||
--hash=sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb \
|
||||
--hash=sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9 \
|
||||
--hash=sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57 \
|
||||
--hash=sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc \
|
||||
--hash=sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc \
|
||||
--hash=sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2 \
|
||||
--hash=sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11
|
||||
|
||||
----- stderr -----
|
||||
Resolved 1 package in [TIME]
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user