Skip to content

Commit

Permalink
test: Add integration tests (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelzw authored Nov 22, 2024
1 parent af43ba3 commit b632e49
Show file tree
Hide file tree
Showing 16 changed files with 368 additions and 237 deletions.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,6 @@ reqwest = { version = "0.12.9", default-features = false, features = [
"macos-system-configuration",
] }
tempfile = "3.13.0"

[dev-dependencies]
rstest = "0.23.0"
241 changes: 241 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
use anyhow::Result;
use rattler::install::{link_package, InstallDriver, InstallOptions};
use rattler_conda_types::{prefix_record::PathsEntry, PackageRecord, PrefixRecord, RepoDataRecord};
use rattler_package_streaming::fs::extract;
use std::{
fmt::{Display, Formatter},
path::{Path, PathBuf},
};

use std::collections::HashSet;

use anyhow::Context;
use rattler_conda_types::{package::ArchiveType, Platform};
use rattler_index::{package_record_from_conda, package_record_from_tar_bz2};
use reqwest::Url;

pub struct PackageRecordVec(pub Vec<PackageRecord>);

impl Display for PackageRecordVec {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"[{}]",
self.0
.iter()
.map(|p| format!("{}", p))
.collect::<Vec<_>>()
.join(", ")
)
}
}

pub async fn pixi_inject(target_prefix: PathBuf, packages: Vec<PathBuf>) -> Result<()> {
if packages.is_empty() {
return Err(anyhow::anyhow!("No packages were provided."));
}

let installed_packages = PrefixRecord::collect_from_prefix(&target_prefix)?;
let installed_package_records = installed_packages
.iter()
.map(|e| e.repodata_record.clone().package_record)
.collect::<Vec<_>>();

let injected_packages = packages
.iter()
.map(|p| {
let record = package_record_from_archive(p)?;
anyhow::Ok((p.clone(), record))
})
.collect::<Result<Vec<_>>>()?;

tracing::debug!(
"Installed packages: {}",
PackageRecordVec(
installed_packages
.iter()
.map(|p| p.repodata_record.package_record.clone())
.collect::<Vec<_>>()
)
);
tracing::debug!(
"Injected packages: {}",
PackageRecordVec(
injected_packages
.iter()
.map(|p| p.1.clone())
.collect::<Vec<_>>()
)
);

let not_matching_platform = injected_packages
.iter()
.map(|p| &p.1)
.filter(|p| {
p.subdir != Platform::NoArch.to_string() && p.subdir != Platform::current().to_string()
})
.collect::<Vec<_>>();
if !not_matching_platform.is_empty() {
return Err(anyhow::anyhow!(
"Packages with platform not matching the current platform ({}) were found: {}",
Platform::current().to_string(),
not_matching_platform
.into_iter()
.map(|p| format!("{} ({})", p, p.subdir.clone()))
.collect::<Vec<_>>()
.join(", ")
));
}

tracing::debug!("Validating package compatibility with prefix.");
let all_records = installed_package_records
.iter()
.chain(injected_packages.iter().map(|p| &p.1))
.collect();
PackageRecord::validate(all_records)?;
tracing::debug!("All packages are compatible with the prefix.");

// check whether the package is already installed
let injected_package_names = injected_packages
.iter()
.map(|p| p.1.name.as_normalized())
.collect::<HashSet<_>>();
let installed_package_names = installed_packages
.iter()
.map(|p| p.repodata_record.package_record.name.as_normalized())
.collect::<HashSet<_>>();
if !injected_package_names.is_disjoint(&installed_package_names) {
return Err(anyhow::anyhow!(
"Some of the packages are already installed: {}",
injected_package_names
.intersection(&injected_package_names)
.map(|p| p.to_string())
.collect::<Vec<_>>()
.join(", ")
));
}

eprintln!(
"⏳ Extracting and installing {} package{} to {}...",
packages.len(),
if packages.len() == 1 { "" } else { "s" },
target_prefix.display()
);

let driver = InstallDriver::default();
let options = InstallOptions::default();

for (path, package_record) in injected_packages.iter() {
let repodata_record = RepoDataRecord {
package_record: package_record.clone(),
file_name: path
.to_str()
.context("Could not create file name from path")?
.to_string(),
url: Url::from_file_path(path.canonicalize()?)
.map_err(|_| anyhow::anyhow!("Could not convert path to URL"))?,
channel: "".to_string(),
};
install_package_to_environment_from_archive(
target_prefix.as_path(),
path.clone(),
repodata_record,
&driver,
&options,
)
.await?;
tracing::debug!("Installed package: {}", path.display());
}

eprintln!("✅ Finished installing packages to prefix.");

tracing::debug!("Finished running pixi-inject");
Ok(())
}

fn package_record_from_archive(file: &Path) -> Result<PackageRecord> {
let archive_type = ArchiveType::split_str(file.to_string_lossy().as_ref())
.context("Could not create ArchiveType")?
.1;
match archive_type {
ArchiveType::TarBz2 => package_record_from_tar_bz2(file),
ArchiveType::Conda => package_record_from_conda(file),
}
.map_err(|e| anyhow::anyhow!("Could not read package record from archive: {}", e))
}

/// Install a package into the environment and write a `conda-meta` file that
/// contains information about how the file was linked.
async fn install_package_to_environment_from_archive(
target_prefix: &Path,
package_path: PathBuf,
repodata_record: RepoDataRecord,
install_driver: &InstallDriver,
install_options: &InstallOptions,
) -> anyhow::Result<()> {
// Link the contents of the package into our environment. This returns all the
// paths that were linked.
let paths = link_package_from_archive(
&package_path,
target_prefix,
install_driver,
install_options.clone(),
)
.await?;

// Construct a PrefixRecord for the package
let prefix_record = PrefixRecord {
repodata_record,
package_tarball_full_path: None,
extracted_package_dir: None,
files: paths
.iter()
.map(|entry| entry.relative_path.clone())
.collect(),
paths_data: paths.into(),
requested_spec: None,
link: None,
};

// Create the conda-meta directory if it doesnt exist yet.
let target_prefix = target_prefix.to_path_buf();
let result = tokio::task::spawn_blocking(move || {
let conda_meta_path = target_prefix.join("conda-meta");
std::fs::create_dir_all(&conda_meta_path)?;

// Write the conda-meta information
let pkg_meta_path = conda_meta_path.join(prefix_record.file_name());
prefix_record.write_to_path(pkg_meta_path, true)
})
.await;
match result {
Ok(result) => Ok(result?),
Err(err) => {
if let Ok(panic) = err.try_into_panic() {
std::panic::resume_unwind(panic);
}
// The operation has been cancelled, so we can also just ignore everything.
Ok(())
}
}
}

// https://github.com/conda/rattler/pull/937
async fn link_package_from_archive(
package_path: &Path,
target_dir: &Path,
driver: &InstallDriver,
options: InstallOptions,
) -> Result<Vec<PathsEntry>> {
let temp_dir = tempfile::tempdir()?;

tracing::debug!(
"extracting {} to temporary directory {}",
package_path.display(),
temp_dir.path().display()
);
extract(package_path, temp_dir.path())?;
link_package(temp_dir.path(), target_dir, driver, options)
.await
.map_err(|e| anyhow::anyhow!("Could not create temporary directory: {}", e))
}
Loading

0 comments on commit b632e49

Please sign in to comment.