Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: Add integration tests #2

Merged
merged 7 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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