Skip to content

Commit

Permalink
Add installation
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelzw committed Nov 22, 2024
1 parent 95912b0 commit 5a6dc14
Show file tree
Hide file tree
Showing 8 changed files with 2,739 additions and 28 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@ reqwest = { version = "0.12.9", default-features = false, features = [
"http2",
"macos-system-configuration",
] }
tempfile = "3.13.0"
1,238 changes: 1,238 additions & 0 deletions examples/already-installed-different-version/pixi.lock

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions examples/already-installed-different-version/pixi.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[project]
channels = ["conda-forge"]
name = "already-installed"
platforms = ["osx-arm64", "osx-64", "linux-64", "win-64"]

[dependencies]
python = "3.13.*"
typing-extensions = "*"
pydantic-core = "2.27.0"
1,242 changes: 1,242 additions & 0 deletions examples/already-installed/pixi.lock

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions examples/already-installed/pixi.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[project]
channels = ["conda-forge"]
name = "already-installed"
platforms = ["osx-arm64", "osx-64", "linux-64", "win-64"]

[dependencies]
python = "3.13.*"
typing-extensions = "*"
pydantic-core = "2.26.0"
38 changes: 38 additions & 0 deletions examples/simple-example/pixi.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions examples/simple-example/pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ platforms = ["osx-arm64", "osx-64", "linux-64", "win-64"]

[dependencies]
python = "3.13.*"
typing-extensions = "*"
229 changes: 201 additions & 28 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
use std::path::PathBuf;
use std::{
collections::HashSet,
fmt::{Display, Formatter},
path::{Path, PathBuf},
};

use clap::Parser;
use clap_verbosity_flag::Verbosity;

use anyhow::Result;
use rattler_conda_types::{package::ArchiveType, PackageRecord, Platform, PrefixRecord};
use anyhow::{Context, Result};
use rattler::install::{link_package, InstallDriver, InstallOptions};
use rattler_conda_types::{
package::ArchiveType, prefix_record::PathsEntry, PackageRecord, Platform, PrefixRecord,
RepoDataRecord,
};
use rattler_index::{package_record_from_conda, package_record_from_tar_bz2};
use rattler_package_streaming::fs::extract;
use reqwest::Url;
use tracing_log::AsTrace;

/* -------------------------------------------- CLI -------------------------------------------- */
Expand All @@ -15,7 +25,7 @@ use tracing_log::AsTrace;
#[command(version, about, long_about = None)]
struct Cli {
#[arg(long)]
prefix: Option<PathBuf>,
prefix: PathBuf,

#[arg(short, long)]
package: Vec<PathBuf>,
Expand All @@ -26,6 +36,22 @@ struct Cli {

/* -------------------------------------------- MAIN ------------------------------------------- */

struct PackageRecordVec(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(", ")
)
}
}

/// The main entrypoint for the pixi-inject CLI.
#[tokio::main]
async fn main() -> Result<()> {
Expand All @@ -38,47 +64,53 @@ async fn main() -> Result<()> {
tracing::debug!("Starting pixi-inject CLI");
tracing::debug!("Parsed CLI options: {:?}", cli);

let prefix = cli.prefix.unwrap(); // todo: fix unwrap
let target_prefix = cli.prefix;
let packages = cli.package;

if packages.len() == 0 {
if packages.is_empty() {
return Err(anyhow::anyhow!("No packages were provided."));
}

let prefix_package_records = PrefixRecord::collect_from_prefix(&prefix)?
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: Vec<(PathBuf, ArchiveType)> = packages
let injected_packages = packages
.iter()
.filter_map(|e| {
ArchiveType::split_str(e.as_path().to_string_lossy().as_ref())
.map(|(p, t)| (PathBuf::from(format!("{}{}", p, t.extension())), t))
.map(|p| {
let record = package_record_from_archive(p)?;
anyhow::Ok((p.clone(), record))
})
.collect();

let mut package_records = Vec::new();
.collect::<Result<Vec<_>>>()?;

tracing::info!(
"Retrieving metadata of {} injected packages.",
injected_packages.len()
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<_>>()
)
);
for (path, archive_type) in injected_packages.iter() {
let package_record = match archive_type {
ArchiveType::TarBz2 => package_record_from_tar_bz2(path),
ArchiveType::Conda => package_record_from_conda(path),
}?;
package_records.push(package_record);
}

let not_matching_platform = package_records
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: {}",
Expand All @@ -92,13 +124,154 @@ async fn main() -> Result<()> {
}

tracing::debug!("Validating package compatibility with prefix.");
let all_records = prefix_package_records
let all_records = installed_package_records
.iter()
.chain(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
pub 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))
}

0 comments on commit 5a6dc14

Please sign in to comment.