#![deny(missing_docs, dead_code)]
use std::{collections::HashMap, io::Read, path::Path, sync::Arc};
mod builder;
mod channel;
mod conda;
mod file_format_version;
mod hash;
pub mod options;
mod parse;
mod platform;
mod pypi;
mod pypi_indexes;
pub mod source;
mod source_identifier;
mod url_or_path;
mod utils;
mod verbatim;
pub use builder::{LockFileBuilder, LockedPackage, RegisterSourcePackageError};
pub use channel::Channel;
pub use conda::{
CondaBinaryData, CondaPackageData, CondaSourceData, ConversionError, GitShallowSpec, InputHash,
PackageBuildSource, PartialSourceMetadata, SourceMetadata, VariantValue,
};
pub use file_format_version::FileFormatVersion;
pub use hash::PackageHashes;
pub use options::{PypiPrereleaseMode, SolveOptions};
pub use parse::ParseCondaLockError;
pub use platform::{OwnedPlatform, ParsePlatformError, Platform, PlatformData, PlatformName};
pub use pypi::{PypiDistributionData, PypiPackageData, PypiSourceData, PypiSourceTreeHashable};
pub use pypi_indexes::{FindLinksUrlOrPath, PypiIndexes};
pub use rattler_conda_types::{Matches, RepoDataRecord};
pub use source_identifier::{ParseSourceIdentifierError, SourceIdentifier};
pub use url_or_path::UrlOrPath;
pub use verbatim::Verbatim;
pub const DEFAULT_ENVIRONMENT_NAME: &str = "default";
#[derive(Clone, Default, Debug)]
pub struct LockFile {
inner: Arc<LockFileInner>,
}
#[derive(Default, Debug)]
struct LockFileInner {
version: FileFormatVersion,
platforms: Vec<PlatformData>,
environments: Vec<EnvironmentData>,
packages: Vec<LockedPackage>,
environment_lookup: ahash::HashMap<String, EnvironmentIndex>,
}
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
pub struct PackageIndex(usize);
impl PackageIndex {
pub fn as_usize(self) -> usize {
self.0
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub(crate) struct SelectorId {
kind: SelectorKind,
id: String,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub(crate) enum SelectorKind {
CondaBinary,
CondaSource,
Pypi,
}
impl SelectorKind {
fn prefix(self) -> &'static str {
match self {
Self::CondaBinary => "conda",
Self::CondaSource => "source",
Self::Pypi => "pypi",
}
}
}
impl SelectorId {
pub(crate) fn from_parts(kind: SelectorKind, id: &str) -> Self {
Self {
kind,
id: id.to_owned(),
}
}
pub(crate) fn new(package: &LockedPackage) -> Self {
match package {
LockedPackage::Conda(CondaPackageData::Binary(data)) => {
Self::from_parts(SelectorKind::CondaBinary, data.location.as_str())
}
LockedPackage::Conda(CondaPackageData::Source(data)) => Self::from_parts(
SelectorKind::CondaSource,
&SourceIdentifier::from_source_data(data).to_string(),
),
LockedPackage::Pypi(data) => {
let location = data
.location()
.given()
.unwrap_or_else(|| data.location().inner().as_str());
Self::from_parts(SelectorKind::Pypi, location)
}
}
}
pub(crate) fn as_str(&self) -> &str {
&self.id
}
pub(crate) fn kind(&self) -> SelectorKind {
self.kind
}
}
impl std::fmt::Display for SelectorId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.kind.prefix(), self.id)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PackageHandle {
pub(crate) selector_id: SelectorId,
pub(crate) index: PackageIndex,
}
impl std::hash::Hash for PackageHandle {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
let id = self.selector_id.as_str();
id.len().hash(state);
id.hash(state);
}
}
impl Ord for PackageHandle {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.selector_id.cmp(&other.selector_id)
}
}
impl PartialOrd for PackageHandle {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl PackageHandle {
pub(crate) fn new(index: PackageIndex, package: &LockedPackage) -> Self {
Self {
index,
selector_id: SelectorId::new(package),
}
}
#[doc(hidden)]
pub fn as_usize(&self) -> usize {
self.index.as_usize()
}
pub fn get<'a>(
&self,
lock_file: &'a LockFile,
) -> Result<&'a LockedPackage, InvalidPackageHandleError> {
self.get_from_slice(&lock_file.inner.packages)
}
pub(crate) fn get_from_slice<'a>(
&self,
packages: &'a [LockedPackage],
) -> Result<&'a LockedPackage, InvalidPackageHandleError> {
let package = packages
.get(self.index.0)
.ok_or(InvalidPackageHandleError::OutOfBounds {
index: self.index.0,
len: packages.len(),
})?;
let actual_selector_id = SelectorId::new(package);
if actual_selector_id != self.selector_id {
return Err(InvalidPackageHandleError::SelectorMismatch {
index: self.index.0,
expected: self.selector_id.to_string(),
actual: actual_selector_id.to_string(),
});
}
Ok(package)
}
}
#[derive(Debug, thiserror::Error)]
pub enum InvalidPackageHandleError {
#[error("PackageHandle index {index} is out of bounds for a package list of length {len}")]
OutOfBounds {
index: usize,
len: usize,
},
#[error(
"PackageHandle index {index} stores selector id {expected} but the \
package at that index has selector id {actual}"
)]
SelectorMismatch {
index: usize,
expected: String,
actual: String,
},
}
#[derive(Debug, thiserror::Error)]
#[error(
"EnvironmentPackages: inconsistent insert of (PackageIndex({index}), {selector_id}) \
— either the index or the selector id is already mapped to a different value"
)]
pub struct InconsistentInsertError {
pub index: usize,
pub selector_id: String,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Hash)]
pub struct EnvironmentPackages {
entries: Vec<PackageHandle>,
}
impl EnvironmentPackages {
pub(crate) fn to_selector_ids(&self) -> Vec<SelectorId> {
self.entries
.iter()
.map(|handle| handle.selector_id.clone())
.collect()
}
pub(crate) fn from_selector_ids<I, T, E>(
items: I,
mut resolve: impl FnMut(&T) -> Result<PackageHandle, E>,
) -> Result<Self, FromSelectorIdsError<E>>
where
I: IntoIterator<Item = T>,
{
let mut environment_packages = Self::default();
for item in items {
let handle = resolve(&item).map_err(FromSelectorIdsError::Resolve)?;
environment_packages
.insert(handle)
.map_err(FromSelectorIdsError::Inconsistent)?;
}
Ok(environment_packages)
}
pub fn insert(&mut self, handle: PackageHandle) -> Result<bool, InconsistentInsertError> {
let selector_search = self.entries.binary_search(&handle);
let position_by_index = self
.entries
.iter()
.position(|existing| existing.index == handle.index);
match (position_by_index, selector_search) {
(None, Err(insert_position)) => {
self.entries.insert(insert_position, handle);
Ok(true)
}
(Some(a), Ok(b)) if a == b => Ok(false),
_ => Err(InconsistentInsertError {
index: handle.index.0,
selector_id: handle.selector_id.as_str().to_string(),
}),
}
}
pub fn from_handles(
handles: impl IntoIterator<Item = PackageHandle>,
) -> Result<Self, InconsistentInsertError> {
let mut environment_packages = Self::default();
for handle in handles {
environment_packages.insert(handle)?;
}
Ok(environment_packages)
}
pub fn from_indices(
indices: impl IntoIterator<Item = PackageIndex>,
packages: &[LockedPackage],
) -> Result<Self, InconsistentInsertError> {
Self::from_handles(
indices
.into_iter()
.map(|index| PackageHandle::new(index, &packages[index.0])),
)
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn iter<'a>(
&'a self,
lock_file: &'a LockFile,
) -> impl Iterator<Item = Result<&'a LockedPackage, InvalidPackageHandleError>> + 'a {
self.entries
.iter()
.map(move |h| h.get_from_slice(&lock_file.inner.packages))
}
pub(crate) fn handles(&self) -> std::slice::Iter<'_, PackageHandle> {
self.entries.iter()
}
#[doc(hidden)]
pub fn raw_handles(&self) -> std::slice::Iter<'_, PackageHandle> {
self.handles()
}
}
#[derive(Debug, thiserror::Error)]
pub enum FromSelectorIdsError<E> {
#[error(transparent)]
Resolve(E),
#[error(transparent)]
Inconsistent(#[from] InconsistentInsertError),
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Hash)]
pub struct SourceData {
pub build_packages: EnvironmentPackages,
pub host_packages: EnvironmentPackages,
}
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
struct PlatformIndex(usize);
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
struct EnvironmentIndex(usize);
#[derive(Clone, Debug)]
struct EnvironmentData {
channels: Vec<Channel>,
indexes: Option<PypiIndexes>,
options: SolveOptions,
packages: ahash::HashMap<PlatformIndex, EnvironmentPackages>,
}
impl LockFile {
pub fn builder() -> LockFileBuilder {
LockFileBuilder::new()
}
pub fn from_reader(
mut reader: impl Read,
base_dir: Option<&Path>,
) -> Result<Self, ParseCondaLockError> {
let mut str = String::new();
reader.read_to_string(&mut str)?;
parse::from_str_with_base_directory(&str, base_dir)
}
pub fn from_path(path: &Path) -> Result<Self, ParseCondaLockError> {
let base_dir = path.parent();
let source = std::fs::read_to_string(path)?;
parse::from_str_with_base_directory(&source, base_dir)
}
pub fn from_str_with_base_directory(
source: &str,
base_dir: Option<&Path>,
) -> Result<Self, ParseCondaLockError> {
parse::from_str_with_base_directory(source, base_dir)
}
pub fn to_path(&self, path: &Path) -> Result<(), std::io::Error> {
let file = std::fs::File::create(path)?;
serde_yaml::to_writer(file, self).map_err(std::io::Error::other)
}
pub fn render_to_string(&self) -> Result<String, std::io::Error> {
serde_yaml::to_string(self).map_err(std::io::Error::other)
}
pub fn platform(&self, name: &str) -> Option<Platform<'_>> {
crate::platform::find_index_by_name(&self.inner.platforms, name)
.map(|index| Platform::new(&self.inner, index))
}
pub fn platforms(&self) -> impl ExactSizeIterator<Item = Platform<'_>> {
self.inner
.platforms
.iter()
.enumerate()
.map(|(index, _)| Platform::new(&self.inner, PlatformIndex(index)))
}
pub fn environment(&self, name: &str) -> Option<Environment<'_>> {
let index = *self.inner.environment_lookup.get(name)?;
Some(Environment {
lock_file: self,
index,
})
}
pub fn default_environment(&self) -> Option<Environment<'_>> {
self.environment(DEFAULT_ENVIRONMENT_NAME)
}
pub fn environments(&self) -> impl ExactSizeIterator<Item = (&str, Environment<'_>)> + '_ {
self.inner
.environment_lookup
.iter()
.map(move |(name, index)| {
(
name.as_str(),
Environment {
lock_file: self,
index: *index,
},
)
})
}
pub fn version(&self) -> FileFormatVersion {
self.inner.version
}
pub fn packages(&self) -> &[LockedPackage] {
&self.inner.packages
}
pub fn is_empty(&self) -> bool {
self.inner.packages.is_empty()
}
}
#[derive(Clone, Copy)]
pub struct Environment<'lock> {
lock_file: &'lock LockFile,
index: EnvironmentIndex,
}
impl<'lock> Environment<'lock> {
fn data(&self) -> &'lock EnvironmentData {
&self.lock_file.inner.environments[self.index.0]
}
pub fn lock_file(&self) -> &'lock LockFile {
self.lock_file
}
pub fn platforms(&self) -> impl ExactSizeIterator<Item = Platform<'lock>> + '_ {
let indices = self
.data()
.packages
.keys()
.map(|index| Platform::new(&self.lock_file.inner, *index))
.collect::<Vec<_>>();
crate::platform::PlatformIterator::new(indices)
}
pub fn channels(&self) -> &[Channel] {
&self.data().channels
}
pub fn pypi_indexes(&self) -> Option<&PypiIndexes> {
self.data().indexes.as_ref()
}
pub fn pypi_prerelease_mode(&self) -> PypiPrereleaseMode {
self.data().options.pypi_prerelease_mode
}
pub fn solve_options(&self) -> &SolveOptions {
&self.data().options
}
fn handles_for_platform(
&self,
platform: Platform<'lock>,
) -> Option<std::slice::Iter<'lock, PackageHandle>> {
if std::ptr::from_ref(self.lock_file.inner.as_ref())
!= std::ptr::from_ref(platform.lock_file_inner)
{
return None;
}
Some(self.data().packages.get(&platform.index)?.handles())
}
pub fn packages(
&self,
platform: Platform<'lock>,
) -> Option<impl DoubleEndedIterator<Item = &'lock LockedPackage> + ExactSizeIterator + '_>
{
Some(self.handles_for_platform(platform)?.map(|handle| {
handle
.get(self.lock_file)
.expect("environment handle must be valid for its own lock file")
}))
}
#[doc(hidden)]
pub fn indexed_packages(
&self,
platform: Platform<'lock>,
) -> Option<
impl DoubleEndedIterator<Item = (usize, &'lock LockedPackage)> + ExactSizeIterator + '_,
> {
Some(self.handles_for_platform(platform)?.map(|handle| {
let package = handle
.get(self.lock_file)
.expect("environment handle must be valid for its own lock file");
(handle.as_usize(), package)
}))
}
pub fn packages_by_platform(
&self,
) -> impl ExactSizeIterator<
Item = (
Platform<'lock>,
impl DoubleEndedIterator<Item = &'lock LockedPackage> + ExactSizeIterator + '_,
),
> + '_ {
self.data()
.packages
.iter()
.map(|(platform_index, data)| {
(Platform::new(&self.lock_file.inner, *platform_index), data)
})
.map(move |(platform, data)| {
(
platform,
data.handles().map(|handle| {
handle
.get(self.lock_file)
.expect("environment handle must be valid for its own lock file")
}),
)
})
}
pub fn pypi_packages_by_platform(
&self,
) -> impl ExactSizeIterator<
Item = (
Platform<'_>,
impl DoubleEndedIterator<Item = &'_ PypiPackageData>,
),
> + '_ {
self.packages_by_platform()
.map(move |(platform, packages)| {
(platform, packages.filter_map(LockedPackage::as_pypi))
})
}
pub fn conda_packages_by_platform(
&self,
) -> impl ExactSizeIterator<
Item = (
Platform<'lock>,
impl DoubleEndedIterator<Item = &'lock CondaPackageData> + '_,
),
> + '_ {
self.packages_by_platform()
.map(move |(platform, packages)| {
(platform, packages.filter_map(LockedPackage::as_conda))
})
}
pub fn conda_repodata_records_by_platform(
&self,
) -> Result<HashMap<Platform<'lock>, Vec<RepoDataRecord>>, ConversionError> {
self.conda_packages_by_platform()
.map(|(platform, packages)| {
Ok((
platform,
packages
.filter_map(CondaPackageData::as_binary)
.map(RepoDataRecord::try_from)
.collect::<Result<Vec<_>, ConversionError>>()?,
))
})
.collect()
}
pub fn conda_packages(
&self,
platform: Platform<'lock>,
) -> Option<impl DoubleEndedIterator<Item = &'lock CondaPackageData> + '_> {
self.packages(platform)
.map(|packages| packages.filter_map(LockedPackage::as_conda))
}
pub fn conda_repodata_records(
&self,
platform: Platform<'lock>,
) -> Result<Option<Vec<RepoDataRecord>>, ConversionError> {
self.conda_packages(platform)
.map(|packages| {
packages
.filter_map(CondaPackageData::as_binary)
.map(RepoDataRecord::try_from)
.collect()
})
.transpose()
}
pub fn pypi_packages(
&self,
platform: Platform<'lock>,
) -> Option<impl DoubleEndedIterator<Item = &'lock PypiPackageData> + '_> {
self.packages(platform)
.map(|pkgs| pkgs.filter_map(LockedPackage::as_pypi))
}
pub fn has_pypi_packages(&self, platform: Platform<'lock>) -> bool {
self.pypi_packages(platform)
.is_some_and(|mut packages| packages.next().is_some())
}
pub fn to_owned(self) -> OwnedEnvironment {
OwnedEnvironment {
lock_file: self.lock_file.clone(),
index: self.index,
}
}
}
#[derive(Clone)]
pub struct OwnedEnvironment {
lock_file: LockFile,
index: EnvironmentIndex,
}
impl OwnedEnvironment {
pub fn as_ref(&self) -> Environment<'_> {
Environment {
lock_file: &self.lock_file,
index: self.index,
}
}
pub fn lock_file(&self) -> LockFile {
self.lock_file.clone()
}
}
#[cfg(test)]
mod test {
use std::path::{Path, PathBuf};
use rattler_conda_types::RepoDataRecord;
use rstest::*;
use crate::{LockedPackage, platform::PlatformName};
use super::{DEFAULT_ENVIRONMENT_NAME, LockFile};
fn test_path() -> PathBuf {
if cfg!(windows) {
PathBuf::from("C:\\tmp\\some\\test\\path")
} else {
PathBuf::from("/tmp/some/test/path")
}
}
#[rstest]
#[case::v0_numpy("v0/numpy-conda-lock.yml")]
#[case::v0_python("v0/python-conda-lock.yml")]
#[case::v0_pypi_matplotlib("v0/pypi-matplotlib-conda-lock.yml")]
#[case::v3_robostack("v3/robostack-turtlesim-conda-lock.yml")]
#[case::v3_numpy("v4/numpy-lock.yml")]
#[case::v4_python("v4/python-lock.yml")]
#[case::v4_pypi_matplotlib("v4/pypi-matplotlib-lock.yml")]
#[case::v4_turtlesim("v4/turtlesim-lock.yml")]
#[case::v4_pypi_path("v4/path-based-lock.yml")]
#[case::v4_pypi_absolute_path("v4/absolute-path-lock.yml")]
#[case::v5_pypi_flat_index("v5/flat-index-lock.yml")]
#[case::v5_with_and_without_purl("v5/similar-with-and-without-purl.yml")]
#[case::v6_conda_source_path("v6/conda-path-lock.yml")]
#[case::v6_derived_channel("v6/derived-channel-lock.yml")]
#[case::v6_sources("v6/sources-lock.yml")]
#[case::v6_options("v6/options-lock.yml")]
#[case::v6_pixi_build_pinned_source("v6/pixi-build-pinned-source-lock.yml")]
#[case::v6_pixi_build_url_source("v6/pixi-build-url-source-lock.yml")]
#[case::v6_pixi_build_git_tag_source("v6/pixi-build-git-tag-source-lock.yml")]
#[case::v6_pixi_build_git_rev_only_source("v6/pixi-build-git-rev-only-source-lock.yml")]
#[case::v6_source_package_with_variants("v6/source-package-with-variants-lock.yml")]
#[case::v6_multiple_source_packages_with_variants(
"v6/multiple-source-packages-with-variants-lock.yml"
)]
#[case::v7_conda_source_path("v7/conda-path-lock.yml")]
#[case::v7_derived_channel("v7/derived-channel-lock.yml")]
#[case::v7_sources("v7/sources-lock.yml")]
#[case::v7_options("v7/options-lock.yml")]
#[case::v7_pixi_build_pinned_source("v7/pixi-build-pinned-source-lock.yml")]
#[case::v7_pixi_build_url_source("v7/pixi-build-url-source-lock.yml")]
#[case::v7_pixi_build_git_tag_source("v7/pixi-build-git-tag-source-lock.yml")]
#[case::v7_pixi_build_git_rev_only_source("v7/pixi-build-git-rev-only-source-lock.yml")]
#[case::v7_multiple_source_packages_with_variants(
"v7/multiple-source-packages-with-variants-lock.yml"
)]
#[case::v7_conda_source_with_build_and_host_packages(
"v7/conda-source-build-host-packages-lock.yml"
)]
#[case::v7_pypi_absolute_url("v7/pypi_absolute_url.yml")]
#[case::v7_pypi_relative_url("v7/pypi_relative_url.yml")]
#[case::v7_pypi_relative_outside_url("v7/pypi_relative_outside_url.yml")]
#[case::v7_pypi_custom_index("v7/pypi_custom_index.yml")]
#[case::v7_partial_source_purls("v7/partial-source-purls-lock.yml")]
#[case::v7_partial_source_extra_depends("v7/partial-source-extra-depends-lock.yml")]
#[case::v7_partial_source_flags("v7/partial-source-flags-lock.yml")]
#[case::v7_partial_source_run_exports("v7/partial-source-run-exports-lock.yml")]
fn test_parse(#[case] file_name: &str) {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test-data/conda-lock")
.join(file_name);
let conda_lock = LockFile::from_path(&path).unwrap();
insta::assert_yaml_snapshot!(file_name, conda_lock);
}
#[rstest]
#[case::v7_invalid_platform("v7/invalid_platform_name.yml")]
#[case::v7_invalid_platform("v7/invalid_platform_subdir.yml")]
#[case::v7_missing_platform("v7/missing_platform.yml")]
#[case::v7_missing_platform("v7/duplicate_platform_definition.yml")]
#[case::v7_missing_platform("v7/duplicate_platform_use.yml")]
fn test_parse_fail(#[case] file_name: &str) {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test-data/lock_parse_fails")
.join(file_name);
let error_message = if let Err(error) = LockFile::from_path(&path) {
format!("{error}")
} else {
"Lockfile was read fine, this is unexpected in a test for error cases".to_string()
};
insta::assert_yaml_snapshot!(file_name, error_message);
}
#[rstest]
fn test_roundtrip(
#[files("../../test-data/conda-lock/**/*.yml")]
#[exclude("forward-compatible-lock")]
path: PathBuf,
) {
let conda_lock = LockFile::from_path(&path).unwrap();
let rendered_lock_file = conda_lock.render_to_string().unwrap();
let source_path = test_path();
let parsed_lock_file =
LockFile::from_str_with_base_directory(&rendered_lock_file, Some(&source_path))
.unwrap();
let rerendered_lock_file = parsed_lock_file.render_to_string().unwrap();
similar_asserts::assert_eq!(rendered_lock_file, rerendered_lock_file);
}
#[test]
fn test_issue_615() {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test-data/conda-lock/v4/absolute-path-lock.yml");
let conda_lock = LockFile::from_path(&path);
assert!(conda_lock.is_ok());
}
#[test]
fn packages_for_platform() {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test-data/conda-lock")
.join("v0/numpy-conda-lock.yml");
let conda_lock = LockFile::from_path(&path).unwrap();
let linux = conda_lock.platform("linux-64").unwrap();
let osx = conda_lock.platform("osx-64").unwrap();
insta::assert_yaml_snapshot!(
conda_lock
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(linux)
.unwrap()
.map(|p| p.location().to_string())
.collect::<Vec<_>>()
);
insta::assert_yaml_snapshot!(
conda_lock
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(osx)
.unwrap()
.map(|p| p.location().to_string())
.collect::<Vec<_>>()
);
}
#[test]
fn test_has_pypi_packages() {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test-data/conda-lock")
.join("v4/pypi-matplotlib-lock.yml");
let conda_lock = LockFile::from_path(&path).unwrap();
let linux = conda_lock.platform("linux-64").unwrap();
assert!(
conda_lock
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.has_pypi_packages(linux)
);
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test-data/conda-lock")
.join("v6/numpy-as-pypi-lock.yml");
let conda_lock = LockFile::from_path(&path).unwrap();
let osx_arm64 = conda_lock.platform("osx-arm64").unwrap();
assert!(
conda_lock
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.has_pypi_packages(osx_arm64)
);
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test-data/conda-lock")
.join("v6/python-from-conda-only-lock.yml");
let conda_lock = LockFile::from_path(&path).unwrap();
let osx_arm64 = conda_lock.platform("osx-arm64").unwrap();
assert!(
!conda_lock
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.has_pypi_packages(osx_arm64)
);
}
#[test]
fn test_pypi_index_url() {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test-data/conda-lock/v7/pypi_custom_index.yml");
let lock_file = LockFile::from_path(&path).unwrap();
let linux = lock_file.platform("linux-64").unwrap();
let env = lock_file.environment(DEFAULT_ENVIRONMENT_NAME).unwrap();
let pypi_packages: Vec<_> = env
.packages(linux)
.unwrap()
.filter_map(LockedPackage::as_pypi)
.collect();
let requests = pypi_packages
.iter()
.find(|p| p.name().as_ref() == "requests")
.expect("requests package");
assert_eq!(
requests
.as_wheel()
.unwrap()
.index_url
.as_ref()
.map(url::Url::as_str),
Some("https://my-custom-index.example.com/simple")
);
let numpy = pypi_packages
.iter()
.find(|p| p.name().as_ref() == "numpy")
.expect("numpy package");
assert_eq!(
numpy
.as_wheel()
.unwrap()
.index_url
.as_ref()
.map(url::Url::as_str),
Some("https://my-custom-index.example.com/simple")
);
let local_pkg = pypi_packages
.iter()
.find(|p| p.name().as_ref() == "local-pkg")
.expect("local-pkg package");
assert!(local_pkg.as_source().is_some());
}
#[test]
fn v7_pypi_default_index_from_environment() {
let lock_file_str = "\
version: 7
platforms:
- name: linux-64
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://first-index.example.com/simple
- https://second-index.example.com/simple
packages:
linux-64:
- pypi: https://first-index.example.com/packages/requests-2.31.0-py3-none-any.whl
- pypi: https://second-index.example.com/packages/numpy-1.26.0-cp311-linux_x86_64.whl
packages:
- pypi: https://first-index.example.com/packages/requests-2.31.0-py3-none-any.whl
name: requests
version: 2.31.0
sha256: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- pypi: https://second-index.example.com/packages/numpy-1.26.0-cp311-linux_x86_64.whl
name: numpy
version: 1.26.0
index: https://second-index.example.com/simple
sha256: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
";
let lock_file = LockFile::from_str_with_base_directory(lock_file_str, None).unwrap();
let linux = lock_file.platform("linux-64").unwrap();
let env = lock_file.environment(DEFAULT_ENVIRONMENT_NAME).unwrap();
let pypi_packages: Vec<_> = env
.packages(linux)
.unwrap()
.filter_map(LockedPackage::as_pypi)
.collect();
let requests = pypi_packages
.iter()
.find(|p| p.name().as_ref() == "requests")
.expect("requests package");
assert_eq!(
requests
.as_wheel()
.unwrap()
.index_url
.as_ref()
.map(url::Url::as_str),
Some("https://first-index.example.com/simple"),
);
let numpy = pypi_packages
.iter()
.find(|p| p.name().as_ref() == "numpy")
.expect("numpy package");
assert_eq!(
numpy
.as_wheel()
.unwrap()
.index_url
.as_ref()
.map(url::Url::as_str),
Some("https://second-index.example.com/simple"),
);
}
#[test]
fn v5_pypi_index_url_is_none() {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test-data/conda-lock/v5/flat-index-lock.yml");
let lock_file = LockFile::from_path(&path).unwrap();
let platform = lock_file.platform("osx-arm64").unwrap();
let env = lock_file.environment(DEFAULT_ENVIRONMENT_NAME).unwrap();
for pkg in env
.packages(platform)
.unwrap()
.filter_map(LockedPackage::as_pypi)
{
if let Some(wheel) = pkg.as_wheel() {
assert!(
wheel.index_url.is_none(),
"v5 package {:?} should have no index_url",
wheel.name
);
}
}
}
#[test]
fn v6_pypi_index_url_is_none() {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test-data/conda-lock/v6/numpy-as-pypi-lock.yml");
let lock_file = LockFile::from_path(&path).unwrap();
let platform = lock_file.platform("osx-arm64").unwrap();
let env = lock_file.environment(DEFAULT_ENVIRONMENT_NAME).unwrap();
let numpy = env
.packages(platform)
.unwrap()
.filter_map(LockedPackage::as_pypi)
.find(|p| p.name().as_ref() == "numpy")
.expect("numpy package");
assert!(numpy.as_wheel().unwrap().index_url.is_none());
}
#[test]
fn v3_pypi_index_url_is_none() {
let lock_file_str = "\
version: 3
metadata:
content_hash:
linux-64: abc123
channels:
- url: https://conda.anaconda.org/conda-forge/
used_env_vars: []
platforms:
- linux-64
sources: []
package:
- platform: linux-64
name: requests
version: '2.31.0'
category: main
manager: pip
dependencies: []
url: https://files.pythonhosted.org/packages/requests-2.31.0-py3-none-any.whl
hash:
sha256: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
";
let lock_file = LockFile::from_str_with_base_directory(lock_file_str, None).unwrap();
let linux = lock_file.platform("linux-64").unwrap();
let pkg = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(linux)
.unwrap()
.find_map(LockedPackage::as_pypi)
.expect("expected a pypi package");
assert!(pkg.as_wheel().unwrap().index_url.is_none());
}
#[test]
fn v7_pypi_no_indexes_falls_back_to_pypi() {
let lock_file_str = "\
version: 7
platforms:
- name: linux-64
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
packages:
linux-64:
- pypi: https://files.pythonhosted.org/packages/requests-2.31.0-py3-none-any.whl
packages:
- pypi: https://files.pythonhosted.org/packages/requests-2.31.0-py3-none-any.whl
name: requests
version: 2.31.0
sha256: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
";
let lock_file = LockFile::from_str_with_base_directory(lock_file_str, None).unwrap();
let linux = lock_file.platform("linux-64").unwrap();
let pkg = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(linux)
.unwrap()
.find_map(LockedPackage::as_pypi)
.expect("expected a pypi package");
assert_eq!(
pkg.as_wheel()
.unwrap()
.index_url
.as_ref()
.map(url::Url::as_str),
Some("https://pypi.org/simple"),
);
}
#[test]
fn test_is_empty() {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test-data/conda-lock")
.join("v6/empty-lock.yml");
let conda_lock = LockFile::from_path(&path).unwrap();
assert!(conda_lock.is_empty());
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test-data/conda-lock")
.join("v6/python-from-conda-only-lock.yml");
let conda_lock = LockFile::from_path(&path).unwrap();
assert!(!conda_lock.is_empty());
}
#[test]
fn solve_roundtrip() {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test-data/repodata-records/_libgcc_mutex-0.1-conda_forge.json");
let content = std::fs::read_to_string(&path).unwrap();
let repodata_record: RepoDataRecord = serde_json::from_str(&content).unwrap();
assert_eq!(repodata_record.package_record.arch, None);
assert_eq!(repodata_record.package_record.platform, None);
let lock_file = LockFile::builder()
.with_platforms(vec![crate::PlatformData {
name: PlatformName::try_from("linux-64").unwrap(),
subdir: rattler_conda_types::Platform::Linux64,
virtual_packages: Vec::new(),
}])
.unwrap()
.with_conda_package(
DEFAULT_ENVIRONMENT_NAME,
"linux-64",
repodata_record.clone().into(),
)
.unwrap()
.finish();
let rendered_lock_file = lock_file.render_to_string().unwrap();
let parsed_lock_file =
LockFile::from_str_with_base_directory(&rendered_lock_file, None).unwrap();
let linux = parsed_lock_file.platform("linux-64").unwrap();
let repodata_records = parsed_lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.conda_repodata_records(linux)
.unwrap()
.unwrap();
let repodata_record_two = repodata_records[0].clone();
assert_eq!(
repodata_record_two.package_record.arch,
Some("x86_64".to_string())
);
assert_eq!(
repodata_record_two.package_record.platform,
Some("linux".to_string())
);
let rerendered_lock_file_two = parsed_lock_file.render_to_string().unwrap();
assert_eq!(rendered_lock_file, rerendered_lock_file_two);
}
#[test]
fn test_partial_metadata_roundtrip() {
let lock_file_str = "\
version: 7
platforms:
- name: linux-64
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
packages:
linux-64:
- conda_source: \"my-partial-pkg[abcd1234] @ my-partial-pkg\"
packages:
- conda_source: \"my-partial-pkg[abcd1234] @ my-partial-pkg\"
depends:
- python >=3.10
";
let lock_file = LockFile::from_str_with_base_directory(lock_file_str, None).unwrap();
let platform = lock_file.platform("linux-64").unwrap();
let source_data = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(platform)
.unwrap()
.find_map(LockedPackage::as_source_conda)
.expect("expected a source package");
assert!(source_data.record().is_none());
assert_eq!(source_data.name().as_source(), "my-partial-pkg");
assert_eq!(source_data.depends(), &["python >=3.10".to_string()]);
let rendered = lock_file.render_to_string().unwrap();
let reparsed = LockFile::from_str_with_base_directory(&rendered, None).unwrap();
let rerendered = reparsed.render_to_string().unwrap();
similar_asserts::assert_eq!(rendered, rerendered);
}
#[test]
fn test_source_identifier_hash_is_preserved() {
let lock_file_str = "\
version: 7
platforms:
- name: linux-64
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
packages:
linux-64:
- conda_source: \"my-package[deadbeef] @ my-package\"
packages:
- conda_source: \"my-package[deadbeef] @ my-package\"
";
let lock_file = LockFile::from_str_with_base_directory(lock_file_str, None).unwrap();
let platform = lock_file.platform("linux-64").unwrap();
let source_data = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(platform)
.unwrap()
.find_map(LockedPackage::as_source_conda)
.expect("expected a source package");
assert_eq!(source_data.identifier_hash.as_deref(), Some("deadbeef"));
let rendered = lock_file.render_to_string().unwrap();
assert!(rendered.contains("my-package[deadbeef]"));
}
#[test]
fn test_identical_source_packages_different_hashes_roundtrip() {
let lock_file_str = "\
version: 7
platforms:
- name: linux-64
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
packages:
linux-64:
- conda_source: \"my-package[aaaaaaaa] @ my-package\"
- conda_source: \"my-package[bbbbbbbb] @ my-package\"
packages:
- conda_source: \"my-package[aaaaaaaa] @ my-package\"
- conda_source: \"my-package[bbbbbbbb] @ my-package\"
";
let lock_file = LockFile::from_str_with_base_directory(lock_file_str, None).unwrap();
let platform = lock_file.platform("linux-64").unwrap();
let source_packages: Vec<_> = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(platform)
.unwrap()
.filter_map(LockedPackage::as_source_conda)
.collect();
assert_eq!(source_packages.len(), 2);
let hashes: Vec<_> = source_packages
.iter()
.map(|s| s.identifier_hash.as_deref())
.collect();
assert!(hashes.contains(&Some("aaaaaaaa")));
assert!(hashes.contains(&Some("bbbbbbbb")));
let rendered = lock_file.render_to_string().unwrap();
assert!(rendered.contains("my-package[aaaaaaaa]"));
assert!(rendered.contains("my-package[bbbbbbbb]"));
let reparsed = LockFile::from_str_with_base_directory(&rendered, None).unwrap();
let rerendered = reparsed.render_to_string().unwrap();
similar_asserts::assert_eq!(rendered, rerendered);
}
#[test]
fn test_pypi_relative_source_packages_roundtrip() {
let lock_file_str = "\
version: 7
platforms:
- name: linux-64
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://pypi.org/simple
packages:
linux-64:
- pypi: ./
- pypi: ./my_subdir
- pypi: ../external_pkg
packages:
- pypi: ../external_pkg
name: external-pkg
- pypi: ./my_subdir
name: my-subdir
- pypi: ./
name: my-project
requires_dist:
- my-subdir @ file:my_subdir
- external-pkg @ file:../external_pkg
";
let base_dir = test_path();
let lock_file =
LockFile::from_str_with_base_directory(lock_file_str, Some(&base_dir)).unwrap();
let platform = lock_file.platform("linux-64").unwrap();
let env = lock_file.environment(DEFAULT_ENVIRONMENT_NAME).unwrap();
let pypi_packages: Vec<_> = env
.packages(platform)
.unwrap()
.filter_map(LockedPackage::as_pypi)
.collect();
assert_eq!(pypi_packages.len(), 3);
let root_pkg = pypi_packages
.iter()
.find(|p| p.name().as_ref() == "my-project")
.expect("my-project package");
let root_source = root_pkg.as_source().unwrap();
assert_eq!(
root_source.location.given(),
Some("./"),
"verbatim relative path for root should be preserved"
);
assert!(
root_pkg.as_source().is_some(),
"local-path pypi packages must be the Source variant"
);
assert_eq!(root_source.requires_dist.len(), 2);
let subdir_pkg = pypi_packages
.iter()
.find(|p| p.name().as_ref() == "my-subdir")
.expect("my-subdir package");
let subdir_source = subdir_pkg.as_source().unwrap();
assert_eq!(subdir_source.location.given(), Some("./my_subdir"));
assert!(
subdir_pkg.as_source().is_some(),
"local-path pypi packages must be the Source variant"
);
let external_pkg = pypi_packages
.iter()
.find(|p| p.name().as_ref() == "external-pkg")
.expect("external-pkg package");
let external_source = external_pkg.as_source().unwrap();
assert_eq!(external_source.location.given(), Some("../external_pkg"));
assert!(
external_pkg.as_source().is_some(),
"local-path pypi packages must be the Source variant"
);
let rendered = lock_file.render_to_string().unwrap();
let yaml: serde_yaml::Value = serde_yaml::from_str(&rendered).unwrap();
let package_pypi_keys: std::collections::HashSet<&str> = yaml["packages"]
.as_sequence()
.unwrap()
.iter()
.filter_map(|pkg| pkg["pypi"].as_str())
.collect();
let selector_pypi_keys: Vec<&str> = yaml["environments"]["default"]["packages"]["linux-64"]
.as_sequence()
.unwrap()
.iter()
.filter_map(|sel| sel["pypi"].as_str())
.collect();
assert_eq!(
selector_pypi_keys.len(),
3,
"expected 3 pypi selectors in environment"
);
for selector_key in &selector_pypi_keys {
assert!(
package_pypi_keys.contains(selector_key),
"environment selector `pypi: {selector_key}` has no matching \
entry in the packages section (available: {package_pypi_keys:?})"
);
}
let reparsed = LockFile::from_str_with_base_directory(&rendered, Some(&base_dir)).unwrap();
let rerendered = reparsed.render_to_string().unwrap();
similar_asserts::assert_eq!(rendered, rerendered);
}
#[test]
fn test_pypi_file_url_selector_matches_package() {
let base_dir = test_path();
let lock_file_str = format!(
r#"version: 7
platforms:
- name: linux-64
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://pypi.org/simple
packages:
linux-64:
- pypi: file://{0}/my_pkg
packages:
- pypi: file://{0}/my_pkg
name: my-pkg
version: 1.0.0
sha256: abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890
"#,
base_dir.display()
);
eprintln!("Lockfile:\n{lock_file_str}");
let lock_file =
LockFile::from_str_with_base_directory(&lock_file_str, Some(&base_dir)).unwrap();
let rendered = lock_file.render_to_string().unwrap();
let yaml: serde_yaml::Value = serde_yaml::from_str(&rendered).unwrap();
let package_pypi_keys: std::collections::HashSet<&str> = yaml["packages"]
.as_sequence()
.unwrap()
.iter()
.filter_map(|pkg| pkg["pypi"].as_str())
.collect();
let selector_pypi_keys: Vec<&str> = yaml["environments"]["default"]["packages"]["linux-64"]
.as_sequence()
.unwrap()
.iter()
.filter_map(|sel| sel["pypi"].as_str())
.collect();
assert_eq!(selector_pypi_keys.len(), 1);
for selector_key in &selector_pypi_keys {
assert!(
package_pypi_keys.contains(selector_key),
"environment selector `pypi: {selector_key}` has no matching \
entry in the packages section (available: {package_pypi_keys:?})"
);
}
}
#[test]
fn v5_local_path_pypi_package_hash_is_stripped() {
let lock_file_str = "\
version: 5
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://pypi.org/simple
packages:
linux-64:
- pypi: ./local-pkg
packages:
- kind: pypi
name: local-pkg
version: '0.1.0'
path: ./local-pkg
sha256: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
";
let base_dir = test_path();
let lock_file =
LockFile::from_str_with_base_directory(lock_file_str, Some(&base_dir)).unwrap();
let linux = lock_file.platform("linux-64").unwrap();
let pkg = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(linux)
.unwrap()
.find_map(LockedPackage::as_pypi)
.expect("expected a pypi package");
let source = pkg
.as_source()
.expect("local-path pypi package must be the Source variant");
assert_eq!(source.name.as_ref(), "local-pkg");
}
#[test]
fn v6_local_path_pypi_package_hash_is_stripped() {
let lock_file_str = "\
version: 6
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://pypi.org/simple
packages:
linux-64:
- pypi: ./local-pkg
packages:
- pypi: ./local-pkg
name: local-pkg
version: '0.1.0'
sha256: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
";
let lock_file = LockFile::from_str_with_base_directory(lock_file_str, None).unwrap();
let linux = lock_file.platform("linux-64").unwrap();
let pkg = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(linux)
.unwrap()
.find_map(LockedPackage::as_pypi)
.expect("expected a pypi package");
let source = pkg
.as_source()
.expect("local-path pypi package must be the Source variant");
assert_eq!(source.name.as_ref(), "local-pkg");
}
#[test]
fn v7_local_path_pypi_package_version_stripped_even_when_present() {
let lock_file_str = "\
version: 7
platforms:
- name: linux-64
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://pypi.org/simple
packages:
linux-64:
- pypi: ./local-pkg
packages:
- pypi: ./local-pkg
name: local-pkg
";
let lock_file = LockFile::from_str_with_base_directory(lock_file_str, None).unwrap();
let linux = lock_file.platform("linux-64").unwrap();
let pkg = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(linux)
.unwrap()
.find_map(LockedPackage::as_pypi)
.expect("expected a pypi package");
let source = pkg
.as_source()
.expect("local-path pypi package must be the Source variant");
assert_eq!(source.name.as_ref(), "local-pkg");
}
#[test]
fn v7_url_pypi_package_hash_is_preserved() {
let lock_file_str = "\
version: 7
platforms:
- name: linux-64
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://pypi.org/simple
packages:
linux-64:
- pypi: https://files.pythonhosted.org/packages/numpy-1.26.0-cp311-cp311-linux_x86_64.whl
packages:
- pypi: https://files.pythonhosted.org/packages/numpy-1.26.0-cp311-cp311-linux_x86_64.whl
name: numpy
version: 1.26.0
sha256: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
";
let lock_file = LockFile::from_str_with_base_directory(lock_file_str, None).unwrap();
let linux = lock_file.platform("linux-64").unwrap();
let pkg = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(linux)
.unwrap()
.find_map(LockedPackage::as_pypi)
.expect("expected a pypi package");
let wheel = pkg
.as_wheel()
.expect("URL-based pypi package must be the Wheel variant");
assert_eq!(wheel.name.as_ref(), "numpy");
assert!(
wheel.hash.is_some(),
"hash must be preserved for URL-based pypi packages"
);
}
#[test]
fn v5_local_path_wheel_preserves_version_and_hash() {
let lock_file_str = "\
version: 5
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://pypi.org/simple
packages:
linux-64:
- pypi: ./dist/my_pkg-1.0.0-py3-none-any.whl
packages:
- kind: pypi
name: my-pkg
version: '1.0.0'
path: ./dist/my_pkg-1.0.0-py3-none-any.whl
sha256: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
";
let base_dir = test_path();
let lock_file =
LockFile::from_str_with_base_directory(lock_file_str, Some(&base_dir)).unwrap();
let linux = lock_file.platform("linux-64").unwrap();
let pkg = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(linux)
.unwrap()
.find_map(LockedPackage::as_pypi)
.expect("expected a pypi package");
let wheel = pkg
.as_wheel()
.expect("local wheel file must be the Wheel variant");
assert_eq!(wheel.name.as_ref(), "my-pkg");
assert_eq!(wheel.version.to_string(), "1.0.0");
assert!(
wheel.hash.is_some(),
"hash must be preserved for local wheel files, got None"
);
}
#[test]
fn v6_local_path_wheel_preserves_version_and_hash() {
let lock_file_str = "\
version: 6
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://pypi.org/simple
packages:
linux-64:
- pypi: ./dist/my_pkg-1.0.0-py3-none-any.whl
packages:
- pypi: ./dist/my_pkg-1.0.0-py3-none-any.whl
name: my-pkg
version: '1.0.0'
sha256: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
";
let lock_file = LockFile::from_str_with_base_directory(lock_file_str, None).unwrap();
let linux = lock_file.platform("linux-64").unwrap();
let pkg = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(linux)
.unwrap()
.find_map(LockedPackage::as_pypi)
.expect("expected a pypi package");
let wheel = pkg
.as_wheel()
.expect("local wheel file must be the Wheel variant");
assert_eq!(wheel.name.as_ref(), "my-pkg");
assert_eq!(wheel.version.to_string(), "1.0.0");
assert!(
wheel.hash.is_some(),
"hash must be preserved for local wheel files, got None"
);
}
#[test]
fn v7_local_path_wheel_preserves_version_and_hash() {
let lock_file_str = "\
version: 7
platforms:
- name: linux-64
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://pypi.org/simple
packages:
linux-64:
- pypi: ./dist/my_pkg-1.0.0-py3-none-any.whl
packages:
- pypi: ./dist/my_pkg-1.0.0-py3-none-any.whl
name: my-pkg
version: '1.0.0'
sha256: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
";
let lock_file = LockFile::from_str_with_base_directory(lock_file_str, None).unwrap();
let linux = lock_file.platform("linux-64").unwrap();
let pkg = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(linux)
.unwrap()
.find_map(LockedPackage::as_pypi)
.expect("expected a pypi package");
let wheel = pkg
.as_wheel()
.expect("local wheel file must be the Wheel variant");
assert_eq!(wheel.name.as_ref(), "my-pkg");
assert_eq!(wheel.version.to_string(), "1.0.0");
assert!(
wheel.hash.is_some(),
"hash must be preserved for local wheel files, got None"
);
}
#[test]
fn v7_file_url_wheel_preserves_version_and_hash() {
let base_dir = test_path();
let lock_file_str = format!(
"\
version: 7
platforms:
- name: linux-64
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://pypi.org/simple
packages:
linux-64:
- pypi: file://{0}/dist/my_pkg-1.0.0-py3-none-any.whl
packages:
- pypi: file://{0}/dist/my_pkg-1.0.0-py3-none-any.whl
name: my-pkg
version: 1.0.0
sha256: abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890
",
base_dir.display()
);
let lock_file =
LockFile::from_str_with_base_directory(&lock_file_str, Some(&base_dir)).unwrap();
let linux = lock_file.platform("linux-64").unwrap();
let pkg = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(linux)
.unwrap()
.find_map(LockedPackage::as_pypi)
.expect("expected a pypi package");
let wheel = pkg
.as_wheel()
.expect("file:// wheel URL must be the Wheel variant");
assert_eq!(wheel.name.as_ref(), "my-pkg");
assert!(
wheel.hash.is_some(),
"hash must be preserved for file:// wheel URLs, got None"
);
}
#[test]
fn v7_file_url_pypi_package_version_is_stripped() {
let base_dir = test_path();
let lock_file_str = format!(
"\
version: 7
platforms:
- name: linux-64
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://pypi.org/simple
packages:
linux-64:
- pypi: file://{0}/my_pkg
packages:
- pypi: file://{0}/my_pkg
name: my-pkg
",
base_dir.display()
);
let lock_file =
LockFile::from_str_with_base_directory(&lock_file_str, Some(&base_dir)).unwrap();
let linux = lock_file.platform("linux-64").unwrap();
let pkg = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(linux)
.unwrap()
.find_map(LockedPackage::as_pypi)
.expect("expected a pypi package");
let source = pkg
.as_source()
.expect("file:// pypi package (local path) must be the Source variant");
assert_eq!(source.name.as_ref(), "my-pkg");
}
#[test]
fn builder_deduplicates_conda_source_packages() {
use std::collections::BTreeMap;
use rattler_conda_types::{PackageName, PackageRecord};
use crate::{
CondaPackageData, CondaSourceData, PlatformData, SourceData, SourceMetadata, UrlOrPath,
};
let source_pkg = CondaPackageData::Source(Box::new(CondaSourceData {
location: UrlOrPath::Path("my-source-pkg".into()),
package_build_source: None,
variants: BTreeMap::from([(
"target_platform".to_string(),
crate::VariantValue::String("noarch".to_string()),
)]),
identifier_hash: None,
sources: BTreeMap::new(),
source_data: SourceData::default(),
metadata: SourceMetadata::Full(Box::new({
let version: rattler_conda_types::Version = "1.0.0".parse().unwrap();
let mut r = PackageRecord::new(
PackageName::new_unchecked("my-source-pkg"),
version,
"py_0".to_string(),
);
r.subdir = "noarch".to_string();
r
})),
}));
let lock_file = LockFile::builder()
.with_platforms(vec![PlatformData {
name: PlatformName::try_from("linux-64").unwrap(),
subdir: rattler_conda_types::Platform::Linux64,
virtual_packages: Vec::new(),
}])
.unwrap()
.with_conda_package("default", "linux-64", source_pkg.clone())
.unwrap()
.with_conda_package("dev", "linux-64", source_pkg)
.unwrap()
.finish();
let rendered = lock_file.render_to_string().unwrap();
let count = rendered.matches("conda_source:").count();
assert_eq!(
count, 3,
"expected 3 conda_source occurrences (2 selectors + 1 package) \
but found {count}:\n{rendered}"
);
let reparsed = LockFile::from_str_with_base_directory(&rendered, None).unwrap();
let rerendered = reparsed.render_to_string().unwrap();
similar_asserts::assert_eq!(rendered, rerendered);
}
#[test]
fn duplicate_pypi_git_entries_collapsed_on_roundtrip() {
let lock_file_str = "\
version: 7
platforms:
- name: linux-64
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://pypi.org/simple
packages:
linux-64:
- pypi: git+https://github.com/example/minimalloc.git?rev=abc123#abc123
dev:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://pypi.org/simple
packages:
linux-64:
- pypi: git+https://github.com/example/minimalloc.git?rev=abc123#abc123
packages:
- pypi: git+https://github.com/example/minimalloc.git?rev=abc123#abc123
name: minimalloc
version: 0.1.0
requires_dist:
- numpy>=1.0
requires_python: '>=3.7'
- pypi: git+https://github.com/example/minimalloc.git?rev=abc123#abc123
name: minimalloc
version: 0.1.0
requires_dist:
- numpy>=1.0
- scipy>=1.0 ; extra == 'test'
requires_python: '>=3.7'
";
let lock_file = LockFile::from_str_with_base_directory(lock_file_str, None).unwrap();
let rendered = lock_file.render_to_string().unwrap();
let url = "git+https://github.com/example/minimalloc.git";
let count = rendered.matches(url).count();
assert_eq!(
count, 3,
"expected 3 occurrences of git URL (2 selectors + 1 package) \
but found {count}:\n{rendered}"
);
let reparsed = LockFile::from_str_with_base_directory(&rendered, None).unwrap();
let rerendered = reparsed.render_to_string().unwrap();
similar_asserts::assert_eq!(rendered, rerendered);
}
#[test]
fn builder_deduplicates_pypi_git_packages() {
use crate::{PlatformData, PypiPackageData, UrlOrPath, Verbatim};
let url: url::Url = "git+https://github.com/example/minimalloc.git?rev=abc123#abc123"
.parse()
.unwrap();
let pkg_a = PypiPackageData::Distribution(Box::new(crate::PypiDistributionData {
name: "minimalloc".parse().unwrap(),
version: "0.1.0".parse().unwrap(),
location: Verbatim::new(UrlOrPath::Url(url.clone())),
index_url: None,
hash: None,
requires_dist: vec!["numpy>=1.0".parse().unwrap()],
requires_python: Some(">=3.7".parse().unwrap()),
}));
let pkg_b = PypiPackageData::Distribution(Box::new(crate::PypiDistributionData {
name: "minimalloc".parse().unwrap(),
version: "0.1.0".parse().unwrap(),
location: Verbatim::new(UrlOrPath::Url(url)),
index_url: None,
hash: None,
requires_dist: vec![
"numpy>=1.0".parse().unwrap(),
"scipy>=1.0 ; extra == 'test'".parse().unwrap(),
],
requires_python: Some(">=3.7".parse().unwrap()),
}));
let lock_file = LockFile::builder()
.with_platforms(vec![PlatformData {
name: PlatformName::try_from("linux-64").unwrap(),
subdir: rattler_conda_types::Platform::Linux64,
virtual_packages: Vec::new(),
}])
.unwrap()
.with_pypi_package("default", "linux-64", pkg_a)
.unwrap()
.with_pypi_package("dev", "linux-64", pkg_b)
.unwrap()
.finish();
let rendered = lock_file.render_to_string().unwrap();
let url_str = "git+https://github.com/example/minimalloc.git";
let count = rendered.matches(url_str).count();
assert_eq!(
count, 3,
"expected 3 occurrences of git URL (2 selectors + 1 package) \
but found {count}:\n{rendered}"
);
let reparsed = LockFile::from_str_with_base_directory(&rendered, None).unwrap();
let rerendered = reparsed.render_to_string().unwrap();
similar_asserts::assert_eq!(rendered, rerendered);
}
mod environment_packages_hash {
use std::{
hash::{Hash, Hasher},
str::FromStr,
};
use rattler_conda_types::{
PackageName, PackageRecord, Version, package::DistArchiveIdentifier,
};
use url::Url;
use crate::{
CondaBinaryData, CondaPackageData, EnvironmentPackages, LockedPackage, PackageHandle,
PackageIndex,
};
fn hash_of(value: &impl Hash) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
value.hash(&mut hasher);
hasher.finish()
}
fn make_package(name: &str, version: &str) -> LockedPackage {
LockedPackage::Conda(CondaPackageData::Binary(Box::new(CondaBinaryData {
package_record: PackageRecord {
subdir: "linux-64".into(),
..PackageRecord::new(
PackageName::new_unchecked(name),
Version::from_str(version).unwrap(),
"build0".into(),
)
},
location: Url::parse(&format!(
"https://example.com/{name}-{version}-build0.tar.bz2"
))
.unwrap()
.into(),
file_name: format!("{name}-{version}-build0.tar.bz2")
.parse::<DistArchiveIdentifier>()
.unwrap(),
channel: None,
})))
}
fn packages() -> Vec<LockedPackage> {
vec![
make_package("alpha", "1.0.0"),
make_package("beta", "2.0.0"),
make_package("gamma", "3.0.0"),
]
}
fn env_with(indices: &[usize], packages: &[LockedPackage]) -> EnvironmentPackages {
EnvironmentPackages::from_indices(
indices.iter().map(|&index| PackageIndex(index)),
packages,
)
.unwrap()
}
#[test]
fn empty_sets_are_equal() {
let packages = packages();
let a = env_with(&[], &packages);
let b = env_with(&[], &packages);
assert_eq!(a, b);
assert_eq!(hash_of(&a), hash_of(&b));
}
#[test]
fn same_packages_same_order() {
let packages = packages();
let a = env_with(&[0, 1], &packages);
let b = env_with(&[0, 1], &packages);
assert_eq!(a, b);
assert_eq!(hash_of(&a), hash_of(&b));
}
#[test]
fn same_packages_different_order() {
let packages = packages();
let a = env_with(&[0, 1], &packages);
let b = env_with(&[1, 0], &packages);
assert_eq!(a, b);
assert_eq!(hash_of(&a), hash_of(&b));
}
#[test]
fn different_packages_differ() {
let packages = packages();
let a = env_with(&[0, 1], &packages);
let b = env_with(&[0, 2], &packages);
assert_ne!(a, b);
assert_ne!(hash_of(&a), hash_of(&b));
}
#[test]
fn duplicates_are_deduplicated() {
let packages = packages();
let a = env_with(&[0, 0, 1], &packages);
let b = env_with(&[0, 1], &packages);
assert_eq!(a, b);
assert_eq!(hash_of(&a), hash_of(&b));
}
#[test]
fn insert_preserves_sorted_invariant() {
let packages = packages();
let mut env = env_with(&[2], &packages);
env.insert(PackageHandle::new(PackageIndex(0), &packages[0]))
.unwrap();
let ids = env.to_selector_ids();
let mut sorted = ids.clone();
sorted.sort();
assert_eq!(ids, sorted);
}
#[test]
fn insert_duplicate_pair_returns_false() {
let packages = packages();
let mut env = env_with(&[0, 1], &packages);
assert!(
!env.insert(PackageHandle::new(PackageIndex(0), &packages[0]))
.unwrap()
);
}
#[test]
fn insert_reused_selector_with_different_index_errors() {
let packages = packages();
let mut env = env_with(&[0], &packages);
let result = env.insert(PackageHandle::new(PackageIndex(1), &packages[0]));
assert!(result.is_err());
}
}
}