From daf1e506967b1012729dd4bb6fad02c919ee7c8c Mon Sep 17 00:00:00 2001 From: nichmor Date: Tue, 10 Sep 2024 15:36:16 +0300 Subject: [PATCH 01/26] misc: work on pixi global expose --- src/cli/global/expose.rs | 101 +++++++++++++++++++++++++++++++++ src/global/common.rs | 14 +++-- src/global/install.rs | 29 +++------- src/global/project/manifest.rs | 4 ++ src/prefix.rs | 82 ++++++++++++++++++++++++++ 5 files changed, 205 insertions(+), 25 deletions(-) create mode 100644 src/cli/global/expose.rs diff --git a/src/cli/global/expose.rs b/src/cli/global/expose.rs new file mode 100644 index 000000000..9d31fada6 --- /dev/null +++ b/src/cli/global/expose.rs @@ -0,0 +1,101 @@ +use std::{collections::HashMap, path::PathBuf}; + +use clap::Parser; +use itertools::Itertools; +use rattler_shell::shell::ShellEnum; + +use crate::{ + global::{ + self, create_executable_scripts, script_exec_mapping, BinDir, EnvDir, EnvRoot, + EnvironmentName, ExposedKey, + }, + prefix::{create_activation_script, Prefix}, +}; + +#[derive(Parser, Debug)] +pub struct AddArgs { + /// The binary to add as executable in the form of key=value (e.g. python=python3.10) + #[arg(value_parser = parse_key_val)] + name: HashMap, + + #[clap(long)] + environment_name: String, +} + +/// Custom parser to split the input into a key-value pair +fn parse_key_val(s: &str) -> Result<(String, String), String> { + let parts: Vec<&str> = s.splitn(2, '=').collect(); + if parts.len() != 2 { + return Err(format!("Invalid format: {}", s)); + } + Ok((parts[0].to_string(), parts[1].to_string())) +} + +#[derive(Parser, Debug)] +#[clap(group(clap::ArgGroup::new("command")))] +pub enum Command { + #[clap(name = "add")] + Add(AddArgs), +} + + + +/// Expose some binaries +pub async fn execute(args: Command) -> miette::Result<()> { + match args { + Command::Add(args) => { + let mut project = global::Project::discover()?; + + let exposed_by_env = project.environment(args.environment_name.clone()); + + if let None = exposed_by_env{ + miette::bail!("Environment not found"); + } else { + exposed_by_env.expect("we checked this above"); + } + + + let bin_env_dir = EnvDir::new(args.environment_name.clone()).await?; + + let prefix = Prefix::new(bin_env_dir.path()); + + let prefix_records = prefix.find_installed_packages(None).await?; + + let all_executables: Vec<(String, PathBuf)> = + prefix.find_executables(prefix_records.as_slice()); + + + // let exposed = exposed_by_env.exposed; + + // add the new executable + let exposed_key = ExposedKey::try_from(args.name.clone()).unwrap(); + let env_name = EnvironmentName::from(args.environment_name.clone()); + + let script_mapping = script_exec_mapping( + &exposed_key, + &args.name.clone(), + all_executables, + &bin_env_dir.bin_dir, + &env_name, + )?; + + + // Determine the shell to use for the invocation script + let shell: ShellEnum = if cfg!(windows) { + rattler_shell::shell::CmdExe.into() + } else { + rattler_shell::shell::Bash.into() + }; + + let activation_script = create_activation_script(&prefix, shell.clone())?; + + create_executable_scripts(&[script_mapping], &prefix, &shell, activation_script) + .await?; + + // Add the new binary to the manifest + // project.manifest.expose_binary(args.environment_name, args.name).unwrap(); + } + } + Ok(()) + +} diff --git a/src/global/common.rs b/src/global/common.rs index 6a23a13dc..5b6a6104d 100644 --- a/src/global/common.rs +++ b/src/global/common.rs @@ -119,6 +119,7 @@ impl BinDir { } } +/// Global environoments directory, default to `$HOME/.pixi/envs` #[derive(Debug, Clone)] pub struct EnvRoot(PathBuf); @@ -182,20 +183,23 @@ impl EnvRoot { /// Global binary environments directory pub(crate) struct EnvDir { - root: EnvRoot, + pub(crate) root: EnvRoot, + pub(crate) bin_dir: BinDir, path: PathBuf, } impl EnvDir { /// Create the Binary Environment directory - pub(crate) async fn new( - root: EnvRoot, - environment_name: EnvironmentName, + pub(crate) async fn new>( + environment_name: T, ) -> miette::Result { + let root = EnvRoot::from_env().await?; + let bin_dir = BinDir::from_env().await?; + let environment_name = environment_name.into(); let path = root.path().join(environment_name.as_str()); tokio::fs::create_dir_all(&path).await.into_diagnostic()?; - Ok(Self { root, path }) + Ok(Self { root, bin_dir, path }) } /// Construct the path to the env directory for the environment diff --git a/src/global/install.rs b/src/global/install.rs index 3a8144edc..e172238bd 100644 --- a/src/global/install.rs +++ b/src/global/install.rs @@ -55,8 +55,8 @@ pub(crate) async fn install_environment( try_increase_rlimit_to_sensible(); // Create the binary environment prefix where we install or update the package - let env_root = EnvRoot::from_env().await?; - let bin_env_dir = EnvDir::new(env_root, environment_name.clone()).await?; + let bin_env_dir = EnvDir::new(environment_name.clone()).await?; + let prefix = Prefix::new(bin_env_dir.path()); // Install the environment @@ -96,23 +96,11 @@ pub(crate) async fn install_environment( let prefix_records = prefix.find_installed_packages(None).await?; - /// Processes prefix records to filter and collect executable files. - /// It performs the following steps: - /// 1. Filters records to only include direct dependencies - /// 2. Finds executables for each filtered record. - /// 3. Maps executables to a tuple of file name (as a string) and file path. - /// 4. Filters tuples to include only those whose names are in the `exposed` values. - /// 5. Collects the resulting tuples into a vector of executables. - let executables: Vec<(String, PathBuf)> = prefix_records + let all_executables = prefix.find_executables(prefix_records.as_slice()); + + let exposed_executables: Vec<_> = all_executables .into_iter() - .filter(|record| packages.contains(&record.repodata_record.package_record.name)) - .flat_map(|record| find_executables(&prefix, record)) - .filter_map(|path| { - path.file_name() - .and_then(|name| name.to_str()) - .map(|name| (name.to_string(), path.clone())) - }) - .filter(|(name, path)| exposed.values().contains(&name)) + .filter(|(name, _)| exposed.values().contains(name)) .collect(); let script_mapping = exposed @@ -121,8 +109,8 @@ pub(crate) async fn install_environment( script_exec_mapping( exposed_name, entry_point, - executables.clone(), - bin_dir, + exposed_executables.clone(), + &bin_env_dir.bin_dir, environment_name, ) }) @@ -153,6 +141,7 @@ fn script_exec_mapping( bin_dir: &BinDir, environment_name: &EnvironmentName, ) -> miette::Result { + executables .into_iter() .find(|(executable_name, _)| *executable_name == entry_point) diff --git a/src/global/project/manifest.rs b/src/global/project/manifest.rs index b8328dcba..af8fe9cb4 100644 --- a/src/global/project/manifest.rs +++ b/src/global/project/manifest.rs @@ -82,4 +82,8 @@ impl Manifest { pub fn remove_dependency(&mut self, _dep: &PackageName) -> miette::Result<()> { todo!() } + + pub fn expose_binary(&mut self, env_name: &str, bin_name: &str) -> miette::Result<()> { + todo!() + } } diff --git a/src/prefix.rs b/src/prefix.rs index f626d373f..145263e8e 100644 --- a/src/prefix.rs +++ b/src/prefix.rs @@ -105,4 +105,86 @@ impl Prefix { Ok(result) } + + /// Processes prefix records (that you can get by using `find_installed_packages`) + /// to filter and collect executable files. + /// It performs the following steps: + /// 1. Filters records to only include direct dependencies + /// 2. Finds executables for each filtered record. + /// 3. Maps executables to a tuple of file name (as a string) and file path. + /// 4. Filters tuples to include only those whose names are in the `exposed` values. + /// 5. Collects the resulting tuples into a vector of executables. + pub fn find_executables(&self, prefix_packages: &[PrefixRecord]) -> Vec<(String, PathBuf)> { + prefix_packages + .iter() + .flat_map(|record| { + record + .files + .iter() + .filter(|relative_path| is_executable(self, relative_path)) + .filter_map(|path| { + path.file_name() + .and_then(|name| name.to_str()) + .map(|name| (name.to_string(), path.clone())) + }) + }) + .collect() + } +} + +pub(crate) fn is_executable(prefix: &Prefix, relative_path: &Path) -> bool { + // Check if the file is in a known executable directory. + let binary_folders = if cfg!(windows) { + &([ + "", + "Library/mingw-w64/bin/", + "Library/usr/bin/", + "Library/bin/", + "Scripts/", + "bin/", + ][..]) + } else { + &(["bin"][..]) + }; + + let parent_folder = match relative_path.parent() { + Some(dir) => dir, + None => return false, + }; + + if !binary_folders + .iter() + .any(|bin_path| Path::new(bin_path) == parent_folder) + { + return false; + } + + // Check if the file is executable + let absolute_path = prefix.root().join(relative_path); + is_executable::is_executable(absolute_path) +} + +/// Create the environment activation script +pub(crate) fn create_activation_script( + prefix: &Prefix, + shell: ShellEnum, +) -> miette::Result { + let activator = + Activator::from_path(prefix.root(), shell, Platform::current()).into_diagnostic()?; + let result = activator + .activation(ActivationVariables { + conda_prefix: None, + path: None, + path_modification_behavior: PathModificationBehavior::Prepend, + }) + .into_diagnostic()?; + + // Add a shebang on unix based platforms + let script = if cfg!(unix) { + format!("#!/bin/sh\n{}", result.script.contents().into_diagnostic()?) + } else { + result.script.contents().into_diagnostic()? + }; + + Ok(script) } From 0254be7a0319a87c167568782b4e030fcfd6b0f7 Mon Sep 17 00:00:00 2001 From: nichmor Date: Tue, 10 Sep 2024 17:21:53 +0300 Subject: [PATCH 02/26] feat: add pixi global expose functionality --- crates/pixi_manifest/src/lib.rs | 4 +- crates/pixi_manifest/src/manifests/mod.rs | 81 ++++++++++++++ src/cli/global/expose.rs | 125 +++++++++++++--------- src/global/common.rs | 34 ++++-- src/global/install.rs | 12 +-- src/global/project/document.rs | 11 -- src/global/project/manifest.rs | 49 +++++++-- src/global/project/mod.rs | 3 +- src/global/project/parsed_manifest.rs | 18 +++- 9 files changed, 247 insertions(+), 90 deletions(-) create mode 100644 crates/pixi_manifest/src/manifests/mod.rs delete mode 100644 src/global/project/document.rs diff --git a/crates/pixi_manifest/src/lib.rs b/crates/pixi_manifest/src/lib.rs index a972e7497..088780fc8 100644 --- a/crates/pixi_manifest/src/lib.rs +++ b/crates/pixi_manifest/src/lib.rs @@ -24,7 +24,9 @@ mod validation; pub use dependencies::{CondaDependencies, Dependencies, PyPiDependencies}; -pub use manifest::{Manifest, ManifestKind}; + +pub use manifests::manifest::{Manifest, ManifestKind}; +pub use manifests::TomlManifest; pub use crate::environments::Environments; pub use crate::parsed_manifest::{deserialize_package_map, ParsedManifest}; diff --git a/crates/pixi_manifest/src/manifests/mod.rs b/crates/pixi_manifest/src/manifests/mod.rs new file mode 100644 index 000000000..0e714fb9c --- /dev/null +++ b/crates/pixi_manifest/src/manifests/mod.rs @@ -0,0 +1,81 @@ +use toml_edit::{self, Array, Item, Table, Value}; + +pub mod project; + +pub mod manifest; + +pub use project::ManifestSource; + +use crate::error::TomlError; + +/// Represents a wrapper around a TOML document. +/// This struct is exposed to other crates +/// to allow for easy manipulation of the TOML document. +#[derive(Debug, Clone, Default)] +pub struct TomlManifest(toml_edit::DocumentMut); + +impl TomlManifest { + /// Create a new `TomlManifest` from a `toml_edit::DocumentMut` document. + pub fn new(document: toml_edit::DocumentMut) -> Self { + Self(document) + } + + /// Retrieve a mutable reference to a target table `table_name` + /// in dotted form (e.g. `table1.table2`) from the root of the document. + /// If the table is not found, it is inserted into the document. + pub fn get_or_insert_nested_table<'a>( + &'a mut self, + table_name: &str, + ) -> Result<&'a mut Table, TomlError> { + let parts: Vec<&str> = table_name.split('.').collect(); + + let mut current_table = self.0.as_table_mut(); + + for part in parts { + let entry = current_table.entry(part); + let item = entry.or_insert(Item::Table(Table::new())); + current_table = item + .as_table_mut() + .ok_or_else(|| TomlError::table_error(part, table_name))?; + // Avoid creating empty tables + current_table.set_implicit(true); + } + Ok(current_table) + } + + /// Retrieves a mutable reference to a target array `array_name` + /// in table `table_name` in dotted form (e.g. `table1.table2.array`). + /// + /// If the array is not found, it is inserted into the document. + pub fn get_or_insert_toml_array<'a>( + &'a mut self, + table_name: &str, + array_name: &str, + ) -> Result<&'a mut Array, TomlError> { + self.get_or_insert_nested_table(table_name)? + .entry(array_name) + .or_insert(Item::Value(Value::Array(Array::new()))) + .as_array_mut() + .ok_or_else(|| TomlError::array_error(array_name, table_name.to_string().as_str())) + } + + /// Retrieves a mutable reference to a target array `array_name` + /// in table `table_name` in dotted form (e.g. `table1.table2.array`). + /// + /// If the array is not found, returns None. + pub fn get_toml_array<'a>( + &'a mut self, + table_name: &str, + array_name: &str, + ) -> Result, TomlError> { + let array = self + .get_or_insert_nested_table(table_name)? + .get_mut(array_name) + .and_then(|a| a.as_array_mut()); + Ok(array) + } + + pub fn to_string(&self) -> String { + self.0.to_string() + } +} diff --git a/src/cli/global/expose.rs b/src/cli/global/expose.rs index 9d31fada6..e5500fee8 100644 --- a/src/cli/global/expose.rs +++ b/src/cli/global/expose.rs @@ -1,13 +1,13 @@ -use std::{collections::HashMap, path::PathBuf}; +use std::{error::Error, path::PathBuf}; use clap::Parser; use itertools::Itertools; +use pixi_config::ConfigCli; use rattler_shell::shell::ShellEnum; use crate::{ global::{ - self, create_executable_scripts, script_exec_mapping, BinDir, EnvDir, EnvRoot, - EnvironmentName, ExposedKey, + self, create_executable_scripts, script_exec_mapping, EnvDir, ExposedKey, }, prefix::{create_activation_script, Prefix}, }; @@ -16,19 +16,23 @@ use crate::{ pub struct AddArgs { /// The binary to add as executable in the form of key=value (e.g. python=python3.10) #[arg(value_parser = parse_key_val)] - name: HashMap, + name: Vec<(String, String)>, #[clap(long)] environment_name: String, + + #[clap(flatten)] + config: ConfigCli, } -/// Custom parser to split the input into a key-value pair -fn parse_key_val(s: &str) -> Result<(String, String), String> { - let parts: Vec<&str> = s.splitn(2, '=').collect(); - if parts.len() != 2 { - return Err(format!("Invalid format: {}", s)); - } - Ok((parts[0].to_string(), parts[1].to_string())) +/// Parse a single key-value pair +fn parse_key_val(s: &str) -> Result<(String, String), Box> { + let pos = s + .find('=') + .ok_or_else(|| format!("invalid KEY=value: no `=` found in `{}`", s))?; + let key = s[..pos].to_string(); + let value = s[pos + 1..].to_string(); + Ok((key, value)) } #[derive(Parser, Debug)] @@ -38,64 +42,85 @@ pub enum Command { Add(AddArgs), } - - /// Expose some binaries pub async fn execute(args: Command) -> miette::Result<()> { match args { - Command::Add(args) => { - let mut project = global::Project::discover()?; + Command::Add(args) => add(args).await?, + } + Ok(()) +} - let exposed_by_env = project.environment(args.environment_name.clone()); +pub async fn add(args: AddArgs) -> miette::Result<()> { + // should we do a sync first? + let mut project = global::Project::discover()?; - if let None = exposed_by_env{ - miette::bail!("Environment not found"); - } else { - exposed_by_env.expect("we checked this above"); - } + let exposed_by_env = project.environment(args.environment_name.clone()); + if exposed_by_env.is_none() { + miette::bail!("Environment not found"); + } else { + exposed_by_env.expect("we checked this above"); + } - let bin_env_dir = EnvDir::new(args.environment_name.clone()).await?; + let bin_env_dir = EnvDir::new(args.environment_name.clone()).await?; - let prefix = Prefix::new(bin_env_dir.path()); + let prefix = Prefix::new(bin_env_dir.path()); - let prefix_records = prefix.find_installed_packages(None).await?; + let prefix_records = prefix.find_installed_packages(None).await?; - let all_executables: Vec<(String, PathBuf)> = - prefix.find_executables(prefix_records.as_slice()); + let all_executables: Vec<(String, PathBuf)> = + prefix.find_executables(prefix_records.as_slice()); + let binary_to_be_exposed: Vec<&String> = args + .name + .iter() + .map(|(_, actual_binary)| actual_binary) + .collect(); - // let exposed = exposed_by_env.exposed; + // Check if all binaries that are to be exposed are present in the environment + let all_binaries_present = args + .name + .iter() + .all(|(_, binary_name)| binary_to_be_exposed.contains(&binary_name)); - // add the new executable - let exposed_key = ExposedKey::try_from(args.name.clone()).unwrap(); - let env_name = EnvironmentName::from(args.environment_name.clone()); + if !all_binaries_present { + miette::bail!("Not all binaries are present in the environment"); + } - let script_mapping = script_exec_mapping( - &exposed_key, - &args.name.clone(), - all_executables, - &bin_env_dir.bin_dir, - &env_name, - )?; + let env_name = args.environment_name.clone().into(); + for (name_to_exposed, real_binary_to_be_exposed) in args.name.iter() { + let exposed_key = ExposedKey::try_from(name_to_exposed.clone()).unwrap(); - // Determine the shell to use for the invocation script - let shell: ShellEnum = if cfg!(windows) { - rattler_shell::shell::CmdExe.into() - } else { - rattler_shell::shell::Bash.into() - }; + let script_mapping = script_exec_mapping( + &exposed_key, + real_binary_to_be_exposed, + all_executables.iter(), + &bin_env_dir.bin_dir, + &env_name, + )?; - let activation_script = create_activation_script(&prefix, shell.clone())?; + // Determine the shell to use for the invocation script + let shell: ShellEnum = if cfg!(windows) { + rattler_shell::shell::CmdExe.into() + } else { + rattler_shell::shell::Bash.into() + }; - create_executable_scripts(&[script_mapping], &prefix, &shell, activation_script) - .await?; + let activation_script = create_activation_script(&prefix, shell.clone())?; - // Add the new binary to the manifest - // project.manifest.expose_binary(args.environment_name, args.name).unwrap(); - } + create_executable_scripts(&[script_mapping], &prefix, &shell, activation_script).await?; + + // Add the new binary to the manifest + project + .manifest + .expose_binary( + &env_name, + exposed_key, + real_binary_to_be_exposed.to_string(), + ) + .unwrap(); + project.manifest.save()?; } Ok(()) - } diff --git a/src/global/common.rs b/src/global/common.rs index 5b6a6104d..8dd58117b 100644 --- a/src/global/common.rs +++ b/src/global/common.rs @@ -189,17 +189,33 @@ pub(crate) struct EnvDir { } impl EnvDir { - /// Create the Binary Environment directory - pub(crate) async fn new>( - environment_name: T, - ) -> miette::Result { - let root = EnvRoot::from_env().await?; + /// Create the default Binary Environment directory + pub(crate) async fn new>(environment_name: T) -> miette::Result { + let root = EnvRoot::from_env().await?; let bin_dir = BinDir::from_env().await?; let environment_name = environment_name.into(); let path = root.path().join(environment_name.as_str()); tokio::fs::create_dir_all(&path).await.into_diagnostic()?; - Ok(Self { root, bin_dir, path }) + Ok(Self { + root, + bin_dir, + path, + }) + } + + /// Create the Binary Environment based on passed global environment root + pub(crate) async fn from_env_root>(env_root: EnvRoot, environment_name: T) -> miette::Result { + let bin_dir = BinDir::from_env().await?; + let environment_name = environment_name.into(); + let path = env_root.path().join(environment_name.as_str()); + tokio::fs::create_dir_all(&path).await.into_diagnostic()?; + + Ok(Self { + root: env_root, + bin_dir, + path, + }) } /// Construct the path to the env directory for the environment @@ -254,10 +270,10 @@ mod tests { let env_root = EnvRoot::new(temp_dir.path().to_owned()).await.unwrap(); // Define a test environment name - let environment_name = "test-env".parse().unwrap(); + let environment_name: EnvironmentName = "test-env".parse().unwrap(); // Create a new binary env dir - let bin_env_dir = EnvDir::new(env_root, environment_name).await.unwrap(); + let bin_env_dir = EnvDir::from_env_root(env_root, environment_name).await.unwrap(); // Verify that the directory was created assert!(bin_env_dir.path().exists()); @@ -275,7 +291,7 @@ mod tests { // Create some directories in the temporary directory let envs = ["env1", "env2", "env3"]; for env in &envs { - EnvDir::new(env_root.clone(), env.parse().unwrap()) + EnvDir::from_env_root(env_root.clone(), env.parse::().unwrap()) .await .unwrap(); } diff --git a/src/global/install.rs b/src/global/install.rs index e172238bd..9c201b440 100644 --- a/src/global/install.rs +++ b/src/global/install.rs @@ -109,7 +109,7 @@ pub(crate) async fn install_environment( script_exec_mapping( exposed_name, entry_point, - exposed_executables.clone(), + exposed_executables.iter(), &bin_env_dir.bin_dir, environment_name, ) @@ -134,20 +134,18 @@ pub(crate) async fn install_environment( /// # Errors /// /// Returns an error if the entry point is not found in the list of executable names. -fn script_exec_mapping( +pub(crate) fn script_exec_mapping<'a>( exposed_name: &ExposedKey, entry_point: &str, - executables: impl IntoIterator, + mut executables: impl Iterator, bin_dir: &BinDir, environment_name: &EnvironmentName, ) -> miette::Result { - executables - .into_iter() .find(|(executable_name, _)| *executable_name == entry_point) .map(|(_, executable_path)| ScriptExecMapping { global_script_path: bin_dir.executable_script_path(exposed_name), - original_executable: executable_path, + original_executable: executable_path.clone(), }) .ok_or_else(|| miette::miette!("Could not find {entry_point} in {environment_name}")) } @@ -480,7 +478,7 @@ pub(crate) async fn sync( let packages = specs.keys().cloned().collect(); install_environment( - &environment_name, + environment_name, &environment.exposed, packages, solved_records.clone(), diff --git a/src/global/project/document.rs b/src/global/project/document.rs deleted file mode 100644 index 55b824194..000000000 --- a/src/global/project/document.rs +++ /dev/null @@ -1,11 +0,0 @@ -use std::fmt; - -#[derive(Debug, Clone)] -/// Represents a mutable pixi global TOML. -pub(crate) struct ManifestSource(pub(crate) toml_edit::DocumentMut); - -impl fmt::Display for ManifestSource { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.0) - } -} diff --git a/src/global/project/manifest.rs b/src/global/project/manifest.rs index af8fe9cb4..eee7cbd5a 100644 --- a/src/global/project/manifest.rs +++ b/src/global/project/manifest.rs @@ -2,12 +2,14 @@ use std::path::{Path, PathBuf}; use miette::IntoDiagnostic; use rattler_conda_types::{MatchSpec, PackageName}; -use toml_edit::DocumentMut; +use toml_edit::{DocumentMut, Item}; use super::error::ManifestError; -use super::MANIFEST_DEFAULT_NAME; -use super::{document::ManifestSource, parsed_manifest::ParsedManifest}; +use super::parsed_manifest::ParsedManifest; +use super::{EnvironmentName, ExposedKey, MANIFEST_DEFAULT_NAME}; + +use pixi_manifest::TomlManifest; /// Handles the global project's manifest file. /// This struct is responsible for reading, parsing, editing, and saving the @@ -23,7 +25,7 @@ pub struct Manifest { pub contents: String, /// Editable toml document - pub document: ManifestSource, + pub document: TomlManifest, /// The parsed manifest pub parsed: ParsedManifest, @@ -52,11 +54,10 @@ impl Manifest { Err(e) => e.to_fancy(MANIFEST_DEFAULT_NAME, &contents)?, }; - let source = ManifestSource(document); let manifest = Self { path: manifest_path.to_path_buf(), contents, - document: source, + document: TomlManifest::new(document), parsed: manifest, }; @@ -83,7 +84,39 @@ impl Manifest { todo!() } - pub fn expose_binary(&mut self, env_name: &str, bin_name: &str) -> miette::Result<()> { - todo!() + pub fn expose_binary( + &mut self, + env_name: &EnvironmentName, + exposed_name: ExposedKey, + actual_bin: String, + ) -> miette::Result<()> { + let table_name = format!("envs.{}.exposed", env_name); + + // let bin_name = bin_name.to_string(); + self.document + .get_or_insert_nested_table(&table_name)? + .insert( + exposed_name.as_str(), + Item::Value(toml_edit::Value::from(actual_bin.clone())), + ); + + let mut envs = self.parsed.get_mut_environment(env_name); + let envs = if envs.is_none() { + miette::bail!("Environment {env_name} not found"); + } else { + envs.expect("we checked this above") + }; + + envs.exposed.insert(exposed_name.clone(), actual_bin.clone()); + + tracing::debug!("added {}={} in toml document", exposed_name, actual_bin); + Ok(()) + } + + /// Save the manifest to the file and update the contents + pub fn save(&mut self) -> miette::Result<()> { + self.contents = self.document.to_string(); + std::fs::write(&self.path, self.contents.clone()).into_diagnostic()?; + Ok(()) } } diff --git a/src/global/project/mod.rs b/src/global/project/mod.rs index 5a503bbcd..1d4d1eee4 100644 --- a/src/global/project/mod.rs +++ b/src/global/project/mod.rs @@ -17,7 +17,6 @@ use rattler_repodata_gateway::Gateway; use reqwest_middleware::ClientWithMiddleware; use std::fmt::Debug; -mod document; mod environment; mod error; mod manifest; @@ -123,7 +122,7 @@ impl Project { } /// Returns the environments in this project. - pub(crate) fn environments(&self) -> IndexMap { + pub(crate) fn environments(&self) -> &IndexMap { self.manifest.parsed.environments() } } diff --git a/src/global/project/parsed_manifest.rs b/src/global/project/parsed_manifest.rs index 22b532df6..8ba11f764 100644 --- a/src/global/project/parsed_manifest.rs +++ b/src/global/project/parsed_manifest.rs @@ -14,6 +14,7 @@ use super::error::ManifestError; use pixi_spec::PixiSpec; /// Describes the contents of a parsed global project manifest. +/// #[derive(Debug, Clone)] pub struct ParsedManifest { /// The environments the project can create. @@ -26,8 +27,15 @@ impl ParsedManifest { toml_edit::de::from_str(source).map_err(ManifestError::from) } - pub(crate) fn environments(&self) -> IndexMap { - self.environments.clone() + pub(crate) fn environments(&self) -> &IndexMap { + &self.environments + } + + pub(crate) fn get_mut_environment( + &mut self, + key: &EnvironmentName, + ) -> Option<&mut ParsedEnvironment> { + self.environments.get_mut(key) } } @@ -101,6 +109,12 @@ impl ParsedEnvironment { #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub(crate) struct ExposedKey(String); +impl ExposedKey { + pub fn as_str(&self) -> &str { + &self.0 + } +} + impl fmt::Display for ExposedKey { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) From d48a41b3554a009d91285a96961d708f985eb17f Mon Sep 17 00:00:00 2001 From: nichmor Date: Tue, 10 Sep 2024 17:34:47 +0300 Subject: [PATCH 03/26] misc: fix some stuff after merging --- tests/integration/test_main_cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/test_main_cli.py b/tests/integration/test_main_cli.py index bc8379583..397497fcf 100644 --- a/tests/integration/test_main_cli.py +++ b/tests/integration/test_main_cli.py @@ -1,6 +1,7 @@ from enum import IntEnum from pathlib import Path import subprocess +import pytest PIXI_VERSION = "0.29.0" From e549a1ca4fce8c4ab0252e1d13f6c76b8e364e25 Mon Sep 17 00:00:00 2001 From: nichmor Date: Fri, 13 Sep 2024 09:35:18 +0300 Subject: [PATCH 04/26] feat: add also global expose remove --- src/cli/global/expose.rs | 107 +++++++-------------- src/cli/global/mod.rs | 5 + src/global/common.rs | 9 +- src/global/expose.rs | 131 ++++++++++++++++++++++++++ src/global/install.rs | 2 +- src/global/mod.rs | 5 +- src/global/project/manifest.rs | 38 ++++++-- src/global/project/mod.rs | 5 + src/global/project/parsed_manifest.rs | 17 +++- src/prefix.rs | 2 +- 10 files changed, 230 insertions(+), 91 deletions(-) create mode 100644 src/global/expose.rs diff --git a/src/cli/global/expose.rs b/src/cli/global/expose.rs index e5500fee8..34970dc1e 100644 --- a/src/cli/global/expose.rs +++ b/src/cli/global/expose.rs @@ -2,12 +2,15 @@ use std::{error::Error, path::PathBuf}; use clap::Parser; use itertools::Itertools; +use miette::IntoDiagnostic; use pixi_config::ConfigCli; use rattler_shell::shell::ShellEnum; +use crate::global::{expose_add, expose_remove}; + use crate::{ global::{ - self, create_executable_scripts, script_exec_mapping, EnvDir, ExposedKey, + self, create_executable_scripts, script_exec_mapping, EnvDir, EnvironmentName, ExposedKey, }, prefix::{create_activation_script, Prefix}, }; @@ -19,7 +22,7 @@ pub struct AddArgs { name: Vec<(String, String)>, #[clap(long)] - environment_name: String, + environment: String, #[clap(flatten)] config: ConfigCli, @@ -35,92 +38,46 @@ fn parse_key_val(s: &str) -> Result<(String, String), Box, + + #[clap(long)] + environment: String, + + #[clap(flatten)] + config: ConfigCli, +} + #[derive(Parser, Debug)] #[clap(group(clap::ArgGroup::new("command")))] -pub enum Command { +pub enum SubCommand { #[clap(name = "add")] Add(AddArgs), + #[clap(name = "remove")] + Remove(RemoveArgs), } /// Expose some binaries -pub async fn execute(args: Command) -> miette::Result<()> { +pub async fn execute(args: SubCommand) -> miette::Result<()> { match args { - Command::Add(args) => add(args).await?, + SubCommand::Add(args) => add(args).await?, + SubCommand::Remove(args) => remove(args).await?, } Ok(()) } pub async fn add(args: AddArgs) -> miette::Result<()> { // should we do a sync first? - let mut project = global::Project::discover()?; - - let exposed_by_env = project.environment(args.environment_name.clone()); - - if exposed_by_env.is_none() { - miette::bail!("Environment not found"); - } else { - exposed_by_env.expect("we checked this above"); - } - - let bin_env_dir = EnvDir::new(args.environment_name.clone()).await?; - - let prefix = Prefix::new(bin_env_dir.path()); - - let prefix_records = prefix.find_installed_packages(None).await?; - - let all_executables: Vec<(String, PathBuf)> = - prefix.find_executables(prefix_records.as_slice()); - - let binary_to_be_exposed: Vec<&String> = args - .name - .iter() - .map(|(_, actual_binary)| actual_binary) - .collect(); - - // Check if all binaries that are to be exposed are present in the environment - let all_binaries_present = args - .name - .iter() - .all(|(_, binary_name)| binary_to_be_exposed.contains(&binary_name)); - - if !all_binaries_present { - miette::bail!("Not all binaries are present in the environment"); - } - - let env_name = args.environment_name.clone().into(); - - for (name_to_exposed, real_binary_to_be_exposed) in args.name.iter() { - let exposed_key = ExposedKey::try_from(name_to_exposed.clone()).unwrap(); - - let script_mapping = script_exec_mapping( - &exposed_key, - real_binary_to_be_exposed, - all_executables.iter(), - &bin_env_dir.bin_dir, - &env_name, - )?; - - // Determine the shell to use for the invocation script - let shell: ShellEnum = if cfg!(windows) { - rattler_shell::shell::CmdExe.into() - } else { - rattler_shell::shell::Bash.into() - }; - - let activation_script = create_activation_script(&prefix, shell.clone())?; - - create_executable_scripts(&[script_mapping], &prefix, &shell, activation_script).await?; + let project = global::Project::discover()?.with_cli_config(args.config); + let env_name: EnvironmentName = args.environment.parse()?; + expose_add(project, env_name, args.name).await +} - // Add the new binary to the manifest - project - .manifest - .expose_binary( - &env_name, - exposed_key, - real_binary_to_be_exposed.to_string(), - ) - .unwrap(); - project.manifest.save()?; - } - Ok(()) +pub async fn remove(args: RemoveArgs) -> miette::Result<()> { + // should we do a sync first? + let project = global::Project::discover()?.with_cli_config(args.config); + let env_name: EnvironmentName = args.environment.parse()?; + expose_remove(project, env_name, args.name).await } diff --git a/src/cli/global/mod.rs b/src/cli/global/mod.rs index fcc901b43..87f895b45 100644 --- a/src/cli/global/mod.rs +++ b/src/cli/global/mod.rs @@ -1,5 +1,6 @@ use clap::Parser; +mod expose; mod install; mod list; mod remove; @@ -18,6 +19,9 @@ pub enum Command { List(list::Args), #[clap(visible_alias = "s")] Sync(sync::Args), + #[clap(visible_alias = "e")] + #[command(subcommand)] + Expose(expose::SubCommand), } /// Subcommand for global package management actions @@ -38,6 +42,7 @@ pub async fn execute(cmd: Args) -> miette::Result<()> { Command::Remove(args) => remove::execute(args).await?, Command::List(args) => list::execute(args).await?, Command::Sync(args) => sync::execute(args).await?, + Command::Expose(subcommand) => expose::execute(subcommand).await?, }; Ok(()) } diff --git a/src/global/common.rs b/src/global/common.rs index 8dd58117b..4bef255b3 100644 --- a/src/global/common.rs +++ b/src/global/common.rs @@ -205,7 +205,10 @@ impl EnvDir { } /// Create the Binary Environment based on passed global environment root - pub(crate) async fn from_env_root>(env_root: EnvRoot, environment_name: T) -> miette::Result { + pub(crate) async fn from_env_root>( + env_root: EnvRoot, + environment_name: T, + ) -> miette::Result { let bin_dir = BinDir::from_env().await?; let environment_name = environment_name.into(); let path = env_root.path().join(environment_name.as_str()); @@ -273,7 +276,9 @@ mod tests { let environment_name: EnvironmentName = "test-env".parse().unwrap(); // Create a new binary env dir - let bin_env_dir = EnvDir::from_env_root(env_root, environment_name).await.unwrap(); + let bin_env_dir = EnvDir::from_env_root(env_root, environment_name) + .await + .unwrap(); // Verify that the directory was created assert!(bin_env_dir.path().exists()); diff --git a/src/global/expose.rs b/src/global/expose.rs new file mode 100644 index 000000000..cb50b83fd --- /dev/null +++ b/src/global/expose.rs @@ -0,0 +1,131 @@ +use std::path::PathBuf; + +use pixi_config::Config; +use rattler_shell::shell::ShellEnum; +use tokio::fs; + +use crate::{ + global::{self, BinDir, EnvRoot}, + prefix::{create_activation_script, Prefix}, +}; + +use miette::{Error, IntoDiagnostic, Report}; + +use super::{create_executable_scripts, script_exec_mapping, EnvDir, EnvironmentName, ExposedKey}; + +pub(crate) async fn expose_add( + mut project: global::Project, + env_name: EnvironmentName, + bin_names_to_expose: Vec<(String, String)>, +) -> miette::Result<()> { + // verify that environment exist + let exposed_by_env = project + .environments() + .get(&env_name) + .ok_or_else(|| miette::miette!("Environment {env_name} not found"))?; + + let bin_env_dir = EnvDir::new(env_name.clone()).await?; + + let prefix = Prefix::new(bin_env_dir.path()); + + let prefix_records = prefix.find_installed_packages(None).await?; + + let all_executables: Vec<(String, PathBuf)> = + prefix.find_executables(prefix_records.as_slice()); + + let installed_binaries: Vec<&String> = all_executables + .iter() + .map(|(binary_name, _)| binary_name) + .collect(); + + // Check if all binaries that are to be exposed are present in the environment + tracing::debug!("installed binaries : {installed_binaries:?}"); + tracing::debug!("binary to expose: {bin_names_to_expose:?}"); + + bin_names_to_expose + .iter() + .try_for_each(|(_, binary_name)| { + installed_binaries + .contains(&binary_name) + .then(|| { + println!( + "binary name to check {}", + installed_binaries.contains(&binary_name) + ); + () + }) + .ok_or_else(|| miette::miette!("Not all binaries are present in the environment")) + })?; + + for (name_to_exposed, real_binary_to_be_exposed) in bin_names_to_expose.iter() { + let exposed_key: ExposedKey = name_to_exposed.parse().into_diagnostic()?; + + let script_mapping = script_exec_mapping( + &exposed_key, + real_binary_to_be_exposed, + all_executables.iter(), + &bin_env_dir.bin_dir, + &env_name, + )?; + + // Determine the shell to use for the invocation script + let shell: ShellEnum = if cfg!(windows) { + rattler_shell::shell::CmdExe.into() + } else { + rattler_shell::shell::Bash.into() + }; + + let activation_script = create_activation_script(&prefix, shell.clone())?; + + create_executable_scripts(&[script_mapping], &prefix, &shell, activation_script).await?; + + // Add the new binary to the manifest + project + .manifest + .add_exposed_binary( + &env_name, + exposed_key, + real_binary_to_be_exposed.to_string(), + ) + .unwrap(); + project.manifest.save()?; + } + Ok(()) +} + +pub(crate) async fn expose_remove( + mut project: global::Project, + environment_name: EnvironmentName, + bin_names_to_remove: Vec, +) -> miette::Result<()> { + // verify that environment exist + let exposed_by_env = project + .environments() + .get(&environment_name) + .ok_or_else(|| miette::miette!("Environment {environment_name} not found"))?; + + bin_names_to_remove.iter().try_for_each(|binary_name| { + let exposed_key = ExposedKey::from_str(binary_name).into_diagnostic()?; + if !exposed_by_env.exposed.contains_key(&exposed_key) { + miette::bail!("Binary {binary_name} not found in the {environment_name} environment"); + } + Ok(()) + })?; + + let bin_env_dir = EnvDir::new(environment_name.clone()).await?; + + for binary_name in bin_names_to_remove.iter() { + let exposed_key = ExposedKey::from_str(binary_name).into_diagnostic()?; + // remove from filesystem + let bin_path = bin_env_dir.bin_dir.executable_script_path(&exposed_key); + tracing::debug!("removing binary {bin_path:?}"); + fs::remove_file(bin_path).await.into_diagnostic()?; + // remove from map + project + .manifest + .remove_exposed_binary(&environment_name, &exposed_key)?; + } + project.manifest.save()?; + + Ok(()) +} diff --git a/src/global/install.rs b/src/global/install.rs index 9c201b440..1c8747705 100644 --- a/src/global/install.rs +++ b/src/global/install.rs @@ -292,7 +292,7 @@ async fn map_executables_to_global_bin_scripts( /// Create the executable scripts by modifying the activation script /// to activate the environment and run the executable. -async fn create_executable_scripts( +pub(crate) async fn create_executable_scripts( mapped_executables: &[ScriptExecMapping], prefix: &Prefix, shell: &ShellEnum, diff --git a/src/global/mod.rs b/src/global/mod.rs index 34bfb8a47..96d117bb7 100644 --- a/src/global/mod.rs +++ b/src/global/mod.rs @@ -2,11 +2,14 @@ #![allow(unused)] mod common; +mod expose; mod install; mod project; +pub(crate) use expose::{expose_add, expose_remove}; + pub(crate) use common::{ channel_name_from_prefix, find_designated_package, BinDir, EnvDir, EnvRoot, }; -pub(crate) use install::sync; +pub(crate) use install::{create_executable_scripts, script_exec_mapping, sync}; pub(crate) use project::{EnvironmentName, ExposedKey, Project, MANIFEST_DEFAULT_NAME}; diff --git a/src/global/project/manifest.rs b/src/global/project/manifest.rs index eee7cbd5a..4d1185a58 100644 --- a/src/global/project/manifest.rs +++ b/src/global/project/manifest.rs @@ -84,7 +84,7 @@ impl Manifest { todo!() } - pub fn expose_binary( + pub fn add_exposed_binary( &mut self, env_name: &EnvironmentName, exposed_name: ExposedKey, @@ -92,7 +92,6 @@ impl Manifest { ) -> miette::Result<()> { let table_name = format!("envs.{}.exposed", env_name); - // let bin_name = bin_name.to_string(); self.document .get_or_insert_nested_table(&table_name)? .insert( @@ -100,19 +99,40 @@ impl Manifest { Item::Value(toml_edit::Value::from(actual_bin.clone())), ); - let mut envs = self.parsed.get_mut_environment(env_name); - let envs = if envs.is_none() { - miette::bail!("Environment {env_name} not found"); - } else { - envs.expect("we checked this above") - }; + let mut envs = self + .parsed + .get_mut_environment(env_name) + .ok_or_else(|| miette::miette!("Environment {env_name} not found"))?; - envs.exposed.insert(exposed_name.clone(), actual_bin.clone()); + envs.exposed + .insert(exposed_name.clone(), actual_bin.clone()); tracing::debug!("added {}={} in toml document", exposed_name, actual_bin); Ok(()) } + pub fn remove_exposed_binary( + &mut self, + env_name: &EnvironmentName, + exposed_name: &ExposedKey, + ) -> miette::Result<()> { + let table_name = format!("envs.{}.exposed", env_name); + + self.document + .get_or_insert_nested_table(&table_name)? + .remove(exposed_name.as_str()); + + let mut envs = self + .parsed + .get_mut_environment(env_name) + .ok_or_else(|| miette::miette!("Environment {env_name} not found"))?; + + envs.exposed.swap_remove(exposed_name); + + tracing::debug!("removed {} from manifest", exposed_name); + Ok(()) + } + /// Save the manifest to the file and update the contents pub fn save(&mut self) -> miette::Result<()> { self.contents = self.document.to_string(); diff --git a/src/global/project/mod.rs b/src/global/project/mod.rs index 1d4d1eee4..3f99f3071 100644 --- a/src/global/project/mod.rs +++ b/src/global/project/mod.rs @@ -125,6 +125,11 @@ impl Project { pub(crate) fn environments(&self) -> &IndexMap { self.manifest.parsed.environments() } + + /// Returns a specific environment based by name. + pub(crate) fn environment(&self, name: &EnvironmentName) -> Option<&ParsedEnvironment> { + self.manifest.parsed.environments().get(name) + } } #[cfg(test)] diff --git a/src/global/project/parsed_manifest.rs b/src/global/project/parsed_manifest.rs index 8ba11f764..19b0a334c 100644 --- a/src/global/project/parsed_manifest.rs +++ b/src/global/project/parsed_manifest.rs @@ -3,10 +3,12 @@ use std::str::FromStr; use indexmap::{IndexMap, IndexSet}; use itertools::Itertools; +use miette::Diagnostic; use pixi_manifest::PrioritizedChannel; use rattler_conda_types::{NamedChannelOrUrl, PackageName, Platform}; use serde::de::{Deserialize, DeserializeSeed, Deserializer, MapAccess, Visitor}; use serde_with::{serde_as, serde_derive::Deserialize}; +use thiserror::Error; use super::environment::EnvironmentName; @@ -113,6 +115,10 @@ impl ExposedKey { pub fn as_str(&self) -> &str { &self.0 } + + pub fn from_str(value: &str) -> Result { + value.parse() + } } impl fmt::Display for ExposedKey { @@ -122,11 +128,11 @@ impl fmt::Display for ExposedKey { } impl FromStr for ExposedKey { - type Err = String; + type Err = ParseExposedKeyError; fn from_str(value: &str) -> Result { if value == "pixi" { - Err("The key 'pixi' is not allowed in the exposed map".to_string()) + Err(ParseExposedKeyError {}) } else { Ok(ExposedKey(value.to_string())) } @@ -159,6 +165,13 @@ impl<'de> Deserialize<'de> for ExposedKey { } } +/// Represents an error that occurs when parsing an binary exposed key. +/// +/// This error is returned when a string fails to be parsed as an environment name. +#[derive(Debug, Clone, Error, Diagnostic, PartialEq)] +#[error("pixi is not allowed as exposed name in the map")] +pub struct ParseExposedKeyError {} + #[cfg(test)] mod tests { use insta::assert_snapshot; diff --git a/src/prefix.rs b/src/prefix.rs index 145263e8e..b12bf81a9 100644 --- a/src/prefix.rs +++ b/src/prefix.rs @@ -7,7 +7,7 @@ use futures::{stream::FuturesUnordered, StreamExt}; use miette::{Context, IntoDiagnostic}; use rattler_conda_types::{Platform, PrefixRecord}; use rattler_shell::{ - activation::{ActivationVariables, Activator}, + activation::{ActivationVariables, Activator, PathModificationBehavior}, shell::ShellEnum, }; use tokio::task::JoinHandle; From 799b3c501a328c8b4583cfaf3164f19e7d9a5c13 Mon Sep 17 00:00:00 2001 From: nichmor Date: Fri, 20 Sep 2024 15:46:13 +0300 Subject: [PATCH 05/26] misc: some WIP --- src/cli/global/expose.rs | 40 ++++++- src/global/expose.rs | 160 ++++++++++++++++++++------ src/global/project/mod.rs | 8 +- src/global/project/parsed_manifest.rs | 6 +- 4 files changed, 168 insertions(+), 46 deletions(-) diff --git a/src/cli/global/expose.rs b/src/cli/global/expose.rs index 34970dc1e..c4c62c8bc 100644 --- a/src/cli/global/expose.rs +++ b/src/cli/global/expose.rs @@ -4,9 +4,10 @@ use clap::Parser; use itertools::Itertools; use miette::IntoDiagnostic; use pixi_config::ConfigCli; +use pixi_utils::reqwest::build_reqwest_clients; use rattler_shell::shell::ShellEnum; -use crate::global::{expose_add, expose_remove}; +use crate::global::{expose_add, expose_remove, BinDir, EnvRoot}; use crate::{ global::{ @@ -70,14 +71,43 @@ pub async fn execute(args: SubCommand) -> miette::Result<()> { pub async fn add(args: AddArgs) -> miette::Result<()> { // should we do a sync first? - let project = global::Project::discover()?.with_cli_config(args.config); + let mut project = global::Project::discover()?.with_cli_config(args.config); let env_name: EnvironmentName = args.environment.parse()?; - expose_add(project, env_name, args.name).await + expose_add(&mut project, env_name, args.name).await?; + + let config = project.config(); + + // after https://github.com/prefix-dev/pixi/pull/1975 PR lands + // this can be simplified by just passing the project + let (_, auth_client) = build_reqwest_clients(Some(&config)); + + let gateway = config.gateway(auth_client.clone()); + + let env_root = EnvRoot::from_env().await?; + let bin_dir = BinDir::from_env().await?; + + + global::sync(&env_root, &project, &bin_dir, config, &gateway, &auth_client).await } pub async fn remove(args: RemoveArgs) -> miette::Result<()> { // should we do a sync first? - let project = global::Project::discover()?.with_cli_config(args.config); + let mut project = global::Project::discover()?.with_cli_config(args.config); let env_name: EnvironmentName = args.environment.parse()?; - expose_remove(project, env_name, args.name).await + expose_remove(&mut project, env_name, args.name).await?; + + let config = project.config(); + + // after https://github.com/prefix-dev/pixi/pull/1975 PR lands + // this can be simplified by just passing the project + let (_, auth_client) = build_reqwest_clients(Some(&config)); + + let gateway = config.gateway(auth_client.clone()); + + let env_root = EnvRoot::from_env().await?; + let bin_dir = BinDir::from_env().await?; + + + global::sync(&env_root, &project, &bin_dir, config, &gateway, &auth_client).await + } diff --git a/src/global/expose.rs b/src/global/expose.rs index cb50b83fd..974956765 100644 --- a/src/global/expose.rs +++ b/src/global/expose.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::{path::PathBuf, str::FromStr}; use pixi_config::Config; use rattler_shell::shell::ShellEnum; @@ -14,7 +14,7 @@ use miette::{Error, IntoDiagnostic, Report}; use super::{create_executable_scripts, script_exec_mapping, EnvDir, EnvironmentName, ExposedKey}; pub(crate) async fn expose_add( - mut project: global::Project, + project: &mut global::Project, env_name: EnvironmentName, bin_names_to_expose: Vec<(String, String)>, ) -> miette::Result<()> { @@ -30,9 +30,12 @@ pub(crate) async fn expose_add( let prefix_records = prefix.find_installed_packages(None).await?; + eprintln!("installed packages: {prefix_records:?}"); let all_executables: Vec<(String, PathBuf)> = prefix.find_executables(prefix_records.as_slice()); + eprintln!("all execs : {all_executables:?}"); + let installed_binaries: Vec<&String> = all_executables .iter() .map(|(binary_name, _)| binary_name) @@ -47,39 +50,13 @@ pub(crate) async fn expose_add( .try_for_each(|(_, binary_name)| { installed_binaries .contains(&binary_name) - .then(|| { - println!( - "binary name to check {}", - installed_binaries.contains(&binary_name) - ); - () - }) - .ok_or_else(|| miette::miette!("Not all binaries are present in the environment")) + .then(|| ()) + .ok_or_else(|| miette::miette!("Binary for exposure {binary_name} is not present in the environment {env_name}")) })?; for (name_to_exposed, real_binary_to_be_exposed) in bin_names_to_expose.iter() { - let exposed_key: ExposedKey = name_to_exposed.parse().into_diagnostic()?; - - let script_mapping = script_exec_mapping( - &exposed_key, - real_binary_to_be_exposed, - all_executables.iter(), - &bin_env_dir.bin_dir, - &env_name, - )?; - - // Determine the shell to use for the invocation script - let shell: ShellEnum = if cfg!(windows) { - rattler_shell::shell::CmdExe.into() - } else { - rattler_shell::shell::Bash.into() - }; + let exposed_key = ExposedKey::from_str(&name_to_exposed).into_diagnostic()?; - let activation_script = create_activation_script(&prefix, shell.clone())?; - - create_executable_scripts(&[script_mapping], &prefix, &shell, activation_script).await?; - - // Add the new binary to the manifest project .manifest .add_exposed_binary( @@ -94,7 +71,7 @@ pub(crate) async fn expose_add( } pub(crate) async fn expose_remove( - mut project: global::Project, + project: &mut global::Project, environment_name: EnvironmentName, bin_names_to_remove: Vec, ) -> miette::Result<()> { @@ -116,10 +93,6 @@ pub(crate) async fn expose_remove( for binary_name in bin_names_to_remove.iter() { let exposed_key = ExposedKey::from_str(binary_name).into_diagnostic()?; - // remove from filesystem - let bin_path = bin_env_dir.bin_dir.executable_script_path(&exposed_key); - tracing::debug!("removing binary {bin_path:?}"); - fs::remove_file(bin_path).await.into_diagnostic()?; // remove from map project .manifest @@ -129,3 +102,118 @@ pub(crate) async fn expose_remove( Ok(()) } + + +mod tests { + use std::{env, fs}; + use std::fs::{set_permissions, File}; + use std::os::unix::fs::PermissionsExt; + use std::path::Path; + use std::str::FromStr; + + use minijinja::Environment; + use pixi_manifest::ParsedManifest; + + use crate::global::{self, expose_add, EnvDir, EnvironmentName, ExposedKey}; + use crate::global::project::Manifest; + + + fn create_empty_executable_file(dir: &Path, executable_name: &str) -> miette::Result<()> { + // Define the path to the empty executable file + let file_path = dir.join("bin").join(executable_name); + + fs::create_dir_all(file_path.parent().unwrap()).unwrap(); + + // Create the empty file + let _file = File::create(file_path.clone()).unwrap(); + + // Set executable permissions (Unix-like systems) + #[cfg(unix)] + { + let mut permissions = std::fs::metadata(file_path.clone()).unwrap().permissions(); + // Set the executable bit (0o755 means read/write/execute for owner, read/execute for group and others) + permissions.set_mode(0o755); + set_permissions(file_path, permissions).unwrap(); + } + + Ok(()) + } + + + #[tokio::test] + async fn test_expose_add_when_binary_exist() { + let contents = r#" +[envs.python-3-10] +channels = ["conda-forge"] +[envs.python-3-10.dependencies] +python = "3.10" +[envs.python-3-10.exposed] +python = "python" +python3 = "python" +"#; + + let tmp_dir = tempfile::tempdir().unwrap(); + + let manifest_path = tmp_dir.path().join("pixi-global-manifest.toml"); + + let manifest = Manifest::from_str(&manifest_path, contents).unwrap(); + + let mut project = global::Project::from_manifest(manifest); + + // inject a fake environment + let env_name = EnvironmentName::from_str("python-3-10").unwrap(); + + let env_dir = EnvDir::new(env_name.clone()).await.unwrap(); + + let test_folder_path = format!( + "{}/{}", + env!("CARGO_MANIFEST_DIR"), + "tests/data/conda-meta/atuin.json" + ); + + let content = std::fs::read_to_string(test_folder_path).unwrap(); + + let atuin_content = env_dir.path().join("conda-meta").join("atuin-18.3.0-h6e96688_0.json"); + std::fs::create_dir_all(atuin_content.parent().unwrap()).unwrap(); + std::fs::write(atuin_content, content).unwrap(); + + + // create also an empty executable + create_empty_executable_file(env_dir.path(), "atuin").unwrap(); + + + expose_add(&mut project, "python-3-10".parse().unwrap(), vec![("atuin".to_string(), "atuin".to_string())]).await.unwrap(); + + let exposed_key = ExposedKey::from_str("atuin").unwrap(); + assert!(project.manifest.parsed.environments().get(&env_name).unwrap().exposed.contains_key(&exposed_key)); + + insta::assert_snapshot!(project.manifest.document.to_string()); + } + + #[tokio::test] + async fn test_expose_add_when_exposing_non_existing_binary() { + let contents = r#" +[envs.python-3-10] +channels = ["conda-forge"] +[envs.python-3-10.dependencies] +python = "3.10" +[envs.python-3-10.exposed] +python = "python" +python3 = "python" +"#; + + let tmp_dir = tempfile::tempdir().unwrap(); + + let manifest_path = tmp_dir.path().join("pixi-global-manifest.toml"); + + let manifest = Manifest::from_str(&manifest_path, contents).unwrap(); + + let mut project = global::Project::from_manifest(manifest); + + let result = expose_add(&mut project, "python-3-10".parse().unwrap(), vec![("non-existing-library".to_string(), "non-existing-library".to_string())]).await.unwrap(); + insta::assert_snapshot!(result.unwrap_err().to_string()); + + + + } +} diff --git a/src/global/project/mod.rs b/src/global/project/mod.rs index 3f99f3071..7f0e78dba 100644 --- a/src/global/project/mod.rs +++ b/src/global/project/mod.rs @@ -8,7 +8,7 @@ use std::{ pub(crate) use environment::EnvironmentName; use indexmap::IndexMap; -use manifest::Manifest; +pub(crate) use manifest::Manifest; use miette::IntoDiagnostic; pub(crate) use parsed_manifest::ExposedKey; use parsed_manifest::ParsedEnvironment; @@ -55,7 +55,7 @@ impl Debug for Project { impl Project { /// Constructs a new instance from an internal manifest representation - fn from_manifest(manifest: Manifest) -> Self { + pub(crate) fn from_manifest(manifest: Manifest) -> Self { let root = manifest .path .parent() @@ -130,6 +130,10 @@ impl Project { pub(crate) fn environment(&self, name: &EnvironmentName) -> Option<&ParsedEnvironment> { self.manifest.parsed.environments().get(name) } + + pub(crate) fn config(&self) -> &Config { + &self.config + } } #[cfg(test)] diff --git a/src/global/project/parsed_manifest.rs b/src/global/project/parsed_manifest.rs index 19b0a334c..d94c051aa 100644 --- a/src/global/project/parsed_manifest.rs +++ b/src/global/project/parsed_manifest.rs @@ -116,9 +116,9 @@ impl ExposedKey { &self.0 } - pub fn from_str(value: &str) -> Result { - value.parse() - } + // pub fn from_str(value: &str) -> Result { + // value.parse() + // } } impl fmt::Display for ExposedKey { From a5018ed8cdd81ff18bd9b39a32580d047152b2bd Mon Sep 17 00:00:00 2001 From: nichmor Date: Mon, 23 Sep 2024 11:27:58 +0300 Subject: [PATCH 06/26] wip: some conflicts unresolved --- src/global/expose.rs | 164 +++++++++++++------------- src/global/install.rs | 16 ++- src/global/mod.rs | 9 +- src/global/project/manifest.rs | 10 +- src/global/project/mod.rs | 15 +-- src/global/project/parsed_manifest.rs | 12 +- 6 files changed, 109 insertions(+), 117 deletions(-) diff --git a/src/global/expose.rs b/src/global/expose.rs index 974956765..7bb392300 100644 --- a/src/global/expose.rs +++ b/src/global/expose.rs @@ -55,7 +55,7 @@ pub(crate) async fn expose_add( })?; for (name_to_exposed, real_binary_to_be_exposed) in bin_names_to_expose.iter() { - let exposed_key = ExposedKey::from_str(&name_to_exposed).into_diagnostic()?; + let exposed_key = ExposedKey::from_str(&name_to_exposed)?; project .manifest @@ -82,7 +82,7 @@ pub(crate) async fn expose_remove( .ok_or_else(|| miette::miette!("Environment {environment_name} not found"))?; bin_names_to_remove.iter().try_for_each(|binary_name| { - let exposed_key = ExposedKey::from_str(binary_name).into_diagnostic()?; + let exposed_key = ExposedKey::from_str(binary_name)?; if !exposed_by_env.exposed.contains_key(&exposed_key) { miette::bail!("Binary {binary_name} not found in the {environment_name} environment"); } @@ -92,7 +92,7 @@ pub(crate) async fn expose_remove( let bin_env_dir = EnvDir::new(environment_name.clone()).await?; for binary_name in bin_names_to_remove.iter() { - let exposed_key = ExposedKey::from_str(binary_name).into_diagnostic()?; + let exposed_key = ExposedKey::from_str(binary_name)?; // remove from map project .manifest @@ -104,116 +104,116 @@ pub(crate) async fn expose_remove( } -mod tests { - use std::{env, fs}; - use std::fs::{set_permissions, File}; - use std::os::unix::fs::PermissionsExt; - use std::path::Path; - use std::str::FromStr; +// mod tests { +// use std::{env, fs}; +// use std::fs::{set_permissions, File}; +// use std::os::unix::fs::PermissionsExt; +// use std::path::Path; +// use std::str::FromStr; - use minijinja::Environment; - use pixi_manifest::ParsedManifest; +// use minijinja::Environment; +// use pixi_manifest::ParsedManifest; - use crate::global::{self, expose_add, EnvDir, EnvironmentName, ExposedKey}; - use crate::global::project::Manifest; +// use crate::global::{self, expose_add, EnvDir, EnvironmentName, ExposedKey}; +// use crate::global::project::Manifest; - fn create_empty_executable_file(dir: &Path, executable_name: &str) -> miette::Result<()> { - // Define the path to the empty executable file - let file_path = dir.join("bin").join(executable_name); +// fn create_empty_executable_file(dir: &Path, executable_name: &str) -> miette::Result<()> { +// // Define the path to the empty executable file +// let file_path = dir.join("bin").join(executable_name); - fs::create_dir_all(file_path.parent().unwrap()).unwrap(); +// fs::create_dir_all(file_path.parent().unwrap()).unwrap(); - // Create the empty file - let _file = File::create(file_path.clone()).unwrap(); +// // Create the empty file +// let _file = File::create(file_path.clone()).unwrap(); - // Set executable permissions (Unix-like systems) - #[cfg(unix)] - { - let mut permissions = std::fs::metadata(file_path.clone()).unwrap().permissions(); - // Set the executable bit (0o755 means read/write/execute for owner, read/execute for group and others) - permissions.set_mode(0o755); - set_permissions(file_path, permissions).unwrap(); - } +// // Set executable permissions (Unix-like systems) +// #[cfg(unix)] +// { +// let mut permissions = std::fs::metadata(file_path.clone()).unwrap().permissions(); +// // Set the executable bit (0o755 means read/write/execute for owner, read/execute for group and others) +// permissions.set_mode(0o755); +// set_permissions(file_path, permissions).unwrap(); +// } - Ok(()) - } +// Ok(()) +// } - #[tokio::test] - async fn test_expose_add_when_binary_exist() { - let contents = r#" -[envs.python-3-10] -channels = ["conda-forge"] -[envs.python-3-10.dependencies] -python = "3.10" -[envs.python-3-10.exposed] -python = "python" -python3 = "python" -"#; +// #[tokio::test] +// async fn test_expose_add_when_binary_exist() { +// let contents = r#" +// [envs.python-3-10] +// channels = ["conda-forge"] +// [envs.python-3-10.dependencies] +// python = "3.10" +// [envs.python-3-10.exposed] +// python = "python" +// python3 = "python" +// "#; - let tmp_dir = tempfile::tempdir().unwrap(); +// let tmp_dir = tempfile::tempdir().unwrap(); - let manifest_path = tmp_dir.path().join("pixi-global-manifest.toml"); +// let manifest_path = tmp_dir.path().join("pixi-global-manifest.toml"); - let manifest = Manifest::from_str(&manifest_path, contents).unwrap(); +// let manifest = Manifest::from_str(&manifest_path, contents).unwrap(); - let mut project = global::Project::from_manifest(manifest); +// let mut project = global::Project::from_manifest(manifest); - // inject a fake environment - let env_name = EnvironmentName::from_str("python-3-10").unwrap(); +// // inject a fake environment +// let env_name = EnvironmentName::from_str("python-3-10").unwrap(); - let env_dir = EnvDir::new(env_name.clone()).await.unwrap(); +// let env_dir = EnvDir::new(env_name.clone()).await.unwrap(); - let test_folder_path = format!( - "{}/{}", - env!("CARGO_MANIFEST_DIR"), - "tests/data/conda-meta/atuin.json" - ); +// let test_folder_path = format!( +// "{}/{}", +// env!("CARGO_MANIFEST_DIR"), +// "tests/data/conda-meta/atuin.json" +// ); - let content = std::fs::read_to_string(test_folder_path).unwrap(); +// let content = std::fs::read_to_string(test_folder_path).unwrap(); - let atuin_content = env_dir.path().join("conda-meta").join("atuin-18.3.0-h6e96688_0.json"); - std::fs::create_dir_all(atuin_content.parent().unwrap()).unwrap(); - std::fs::write(atuin_content, content).unwrap(); +// let atuin_content = env_dir.path().join("conda-meta").join("atuin-18.3.0-h6e96688_0.json"); +// std::fs::create_dir_all(atuin_content.parent().unwrap()).unwrap(); +// std::fs::write(atuin_content, content).unwrap(); - // create also an empty executable - create_empty_executable_file(env_dir.path(), "atuin").unwrap(); +// // create also an empty executable +// create_empty_executable_file(env_dir.path(), "atuin").unwrap(); - expose_add(&mut project, "python-3-10".parse().unwrap(), vec![("atuin".to_string(), "atuin".to_string())]).await.unwrap(); +// expose_add(&mut project, "python-3-10".parse().unwrap(), vec![("atuin".to_string(), "atuin".to_string())]).await.unwrap(); - let exposed_key = ExposedKey::from_str("atuin").unwrap(); - assert!(project.manifest.parsed.environments().get(&env_name).unwrap().exposed.contains_key(&exposed_key)); +// let exposed_key = ExposedKey::from_str("atuin").unwrap(); +// assert!(project.manifest.parsed.envs().get(&env_name).unwrap().exposed.contains_key(&exposed_key)); - insta::assert_snapshot!(project.manifest.document.to_string()); - } +// insta::assert_snapshot!(project.manifest.document.to_string()); +// } - #[tokio::test] - async fn test_expose_add_when_exposing_non_existing_binary() { - let contents = r#" -[envs.python-3-10] -channels = ["conda-forge"] -[envs.python-3-10.dependencies] -python = "3.10" -[envs.python-3-10.exposed] -python = "python" -python3 = "python" -"#; +// #[tokio::test] +// async fn test_expose_add_when_exposing_non_existing_binary() { +// let contents = r#" +// [envs.python-3-10] +// channels = ["conda-forge"] +// [envs.python-3-10.dependencies] +// python = "3.10" +// [envs.python-3-10.exposed] +// python = "python" +// python3 = "python" +// "#; - let tmp_dir = tempfile::tempdir().unwrap(); +// let tmp_dir = tempfile::tempdir().unwrap(); - let manifest_path = tmp_dir.path().join("pixi-global-manifest.toml"); +// let manifest_path = tmp_dir.path().join("pixi-global-manifest.toml"); - let manifest = Manifest::from_str(&manifest_path, contents).unwrap(); +// let manifest = Manifest::from_str(&manifest_path, contents).unwrap(); - let mut project = global::Project::from_manifest(manifest); +// let mut project = global::Project::from_manifest(manifest); - let result = expose_add(&mut project, "python-3-10".parse().unwrap(), vec![("non-existing-library".to_string(), "non-existing-library".to_string())]).await.unwrap(); - insta::assert_snapshot!(result.unwrap_err().to_string()); +// let result = expose_add(&mut project, "python-3-10".parse().unwrap(), vec![("non-existing-library".to_string(), "non-existing-library".to_string())]).await.unwrap(); +// insta::assert_snapshot!(result.unwrap_err().to_string()); - } -} +// } +// } diff --git a/src/global/install.rs b/src/global/install.rs index f556cfb19..e4f66c483 100644 --- a/src/global/install.rs +++ b/src/global/install.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, ffi::OsStr, path::PathBuf, str::FromStr}; +use std::{collections::{HashMap, HashSet}, ffi::OsStr, path::PathBuf, str::FromStr}; use indexmap::IndexMap; use itertools::Itertools; @@ -45,10 +45,6 @@ pub(crate) async fn install_environment( .map(|channel| channel.clone().into_channel(config.global_channel_config())) .collect_vec(); - // Create the binary environment prefix where we install or update the package - let bin_env_dir = EnvDir::new(environment_name.clone()).await?; - - let prefix = Prefix::new(bin_env_dir.path()); let platform = parsed_environment .platform() @@ -143,9 +139,11 @@ pub(crate) async fn expose_executables( let all_executables = prefix.find_executables(prefix_records.as_slice()); + let exposed: HashSet<&String> = parsed_environment.exposed.values().collect(); + let exposed_executables: Vec<_> = all_executables .into_iter() - .filter(|(name, _)| exposed.values().contains(name)) + .filter(|(name, _)| exposed.contains(name)) .collect(); let script_mapping = parsed_environment @@ -156,8 +154,8 @@ pub(crate) async fn expose_executables( exposed_name, entry_point, exposed_executables.iter(), - &bin_env_dir.bin_dir, - environment_name, + bin_dir, + env_name, ) }) .collect::>>()?; @@ -486,7 +484,7 @@ pub(crate) async fn sync(config: &Config, assume_yes: bool) -> Result<(), miette }) .collect::, miette::Report>>()?; - let env_dir = EnvDir::new(env_root.clone(), env_name.clone()).await?; + let env_dir = EnvDir::from_env_root(env_root.clone(), env_name.clone()).await?; let prefix = Prefix::new(env_dir.path()); let prefix_records = prefix.find_installed_packages(Some(50)).await?; diff --git a/src/global/mod.rs b/src/global/mod.rs index f31cdd402..846ac1459 100644 --- a/src/global/mod.rs +++ b/src/global/mod.rs @@ -2,19 +2,16 @@ mod common; mod expose; mod install; mod project; +// mod document; pub(crate) use expose::{expose_add, expose_remove}; -pub(crate) use common::{ - channel_name_from_prefix, find_designated_package, BinDir, EnvDir, EnvRoot, -}; +pub(crate) use common::{BinDir, EnvDir, EnvRoot,}; pub(crate) use install::{create_executable_scripts, script_exec_mapping, sync}; pub(crate) use project::{EnvironmentName, ExposedKey, Project, MANIFEST_DEFAULT_NAME}; +// pub(crate) use document::ManifestSource; use crate::prefix::Prefix; -pub(crate) use common::{BinDir, EnvDir, EnvRoot}; -pub(crate) use install::sync; -pub(crate) use project::{EnvironmentName, ExposedKey, Project}; use rattler_conda_types::PrefixRecord; use std::path::{Path, PathBuf}; diff --git a/src/global/project/manifest.rs b/src/global/project/manifest.rs index eb28cff68..83ee34802 100644 --- a/src/global/project/manifest.rs +++ b/src/global/project/manifest.rs @@ -1,15 +1,17 @@ use std::path::{Path, PathBuf}; use miette::IntoDiagnostic; +use pixi_manifest::TomlManifest; use rattler_conda_types::{MatchSpec, PackageName}; use toml_edit::{DocumentMut, Item}; +// use crate::global::document::ManifestSource; + use super::error::ManifestError; use super::parsed_manifest::ParsedManifest; use super::{EnvironmentName, ExposedKey, MANIFEST_DEFAULT_NAME}; - -use pixi_manifest::TomlManifest; +// use super::document::ManifestSource; // TODO: remove #[allow(unused)] @@ -104,7 +106,7 @@ impl Manifest { let mut envs = self .parsed - .get_mut_environment(env_name) + .get_mut_env(env_name) .ok_or_else(|| miette::miette!("Environment {env_name} not found"))?; envs.exposed @@ -127,7 +129,7 @@ impl Manifest { let mut envs = self .parsed - .get_mut_environment(env_name) + .get_mut_env(env_name) .ok_or_else(|| miette::miette!("Environment {env_name} not found"))?; envs.exposed.swap_remove(exposed_name); diff --git a/src/global/project/mod.rs b/src/global/project/mod.rs index 247a0e2f0..7c0fdce3b 100644 --- a/src/global/project/mod.rs +++ b/src/global/project/mod.rs @@ -8,18 +8,13 @@ use std::{ pub(crate) use environment::EnvironmentName; use indexmap::IndexMap; pub(crate) use manifest::Manifest; -use miette::IntoDiagnostic; +use miette::{Context, IntoDiagnostic}; pub(crate) use parsed_manifest::ExposedKey; -use parsed_manifest::ParsedEnvironment; -use pixi_config::Config; +pub(crate) use parsed_manifest::ParsedEnvironment; use rattler_repodata_gateway::Gateway; use reqwest_middleware::ClientWithMiddleware; -use std::fmt::Debug; -use manifest::Manifest; -use miette::{Context, IntoDiagnostic}; use once_cell::sync::Lazy; use parsed_manifest::ParsedManifest; -pub(crate) use parsed_manifest::{ExposedKey, ParsedEnvironment}; use pixi_config::{home_path, Config}; use pixi_manifest::PrioritizedChannel; use rattler_conda_types::{NamedChannelOrUrl, PackageName, Platform, PrefixRecord}; @@ -107,7 +102,7 @@ impl ExposedData { let conda_meta = env_path.join("conda-meta"); - let bin_env_dir = EnvDir::new(env_root.clone(), env_name.clone()).await?; + let bin_env_dir = EnvDir::from_env_root(env_root.clone(), env_name.clone()).await?; let prefix = Prefix::new(bin_env_dir.path()); let (platform, channel, package) = @@ -359,12 +354,12 @@ impl Project { /// Returns the environments in this project. pub(crate) fn environments(&self) -> &IndexMap { - self.manifest.parsed.environments() + self.manifest.parsed.envs() } /// Returns a specific environment based by name. pub(crate) fn environment(&self, name: &EnvironmentName) -> Option<&ParsedEnvironment> { - self.manifest.parsed.environments().get(name) + self.manifest.parsed.envs().get(name) } pub(crate) fn config(&self) -> &Config { diff --git a/src/global/project/parsed_manifest.rs b/src/global/project/parsed_manifest.rs index 50d4b6ce1..6c19e796c 100644 --- a/src/global/project/parsed_manifest.rs +++ b/src/global/project/parsed_manifest.rs @@ -58,15 +58,15 @@ impl ParsedManifest { toml_edit::de::from_str(source).map_err(ManifestError::from) } - pub(crate) fn environments(&self) -> &IndexMap { - &self.environments + pub(crate) fn envs(&self) -> &IndexMap { + &self.envs } - pub(crate) fn get_mut_environment( + pub(crate) fn get_mut_env( &mut self, key: &EnvironmentName, ) -> Option<&mut ParsedEnvironment> { - self.environments.get_mut(key) + self.envs.get_mut(key) } } @@ -153,11 +153,11 @@ impl fmt::Display for ExposedKey { } impl FromStr for ExposedKey { - type Err = ParseExposedKeyError; + type Err = miette::Report; fn from_str(value: &str) -> Result { if value == "pixi" { - Err(ParseExposedKeyError {}) + miette::bail!("The key 'pixi' is not allowed in the exposed map"); } else { Ok(ExposedKey(value.to_string())) } From 898383f7d041823a18eaa7cfbefde3346702ecf4 Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Mon, 23 Sep 2024 12:04:00 +0200 Subject: [PATCH 07/26] Refactor EVERYTHING --- crates/pixi_manifest/src/manifests/mod.rs | 8 +- src/cli/global/expose.rs | 121 ++++++------ src/cli/global/sync.rs | 10 +- src/global/common.rs | 25 +-- src/global/expose.rs | 219 ---------------------- src/global/install.rs | 41 ++-- src/global/mod.rs | 9 +- src/global/project/manifest.rs | 31 +-- src/global/project/mod.rs | 83 ++++---- src/global/project/parsed_manifest.rs | 7 - 10 files changed, 154 insertions(+), 400 deletions(-) delete mode 100644 src/global/expose.rs diff --git a/crates/pixi_manifest/src/manifests/mod.rs b/crates/pixi_manifest/src/manifests/mod.rs index 0e714fb9c..e0be90baa 100644 --- a/crates/pixi_manifest/src/manifests/mod.rs +++ b/crates/pixi_manifest/src/manifests/mod.rs @@ -1,3 +1,5 @@ +use std::fmt; + use toml_edit::{self, Array, Item, Table, Value}; pub mod project; @@ -74,8 +76,10 @@ impl TomlManifest { .and_then(|a| a.as_array_mut()); Ok(array) } +} - pub fn to_string(&self) -> String { - self.0.to_string() +impl fmt::Display for TomlManifest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) } } diff --git a/src/cli/global/expose.rs b/src/cli/global/expose.rs index c4c62c8bc..f167a5cb4 100644 --- a/src/cli/global/expose.rs +++ b/src/cli/global/expose.rs @@ -1,51 +1,63 @@ -use std::{error::Error, path::PathBuf}; +use std::str::FromStr; +use crate::global::{BinDir, EnvRoot}; use clap::Parser; -use itertools::Itertools; -use miette::IntoDiagnostic; -use pixi_config::ConfigCli; -use pixi_utils::reqwest::build_reqwest_clients; -use rattler_shell::shell::ShellEnum; +use pixi_config::{Config, ConfigCli}; -use crate::global::{expose_add, expose_remove, BinDir, EnvRoot}; +use crate::global::{self, EnvironmentName, ExposedKey}; -use crate::{ - global::{ - self, create_executable_scripts, script_exec_mapping, EnvDir, EnvironmentName, ExposedKey, - }, - prefix::{create_activation_script, Prefix}, -}; +#[derive(Debug, Clone)] +struct Mapping { + exposed_key: ExposedKey, + executable_name: String, +} + +impl Mapping { + pub fn new(exposed_key: ExposedKey, executable_name: String) -> Self { + Self { + exposed_key, + executable_name, + } + } +} #[derive(Parser, Debug)] pub struct AddArgs { /// The binary to add as executable in the form of key=value (e.g. python=python3.10) - #[arg(value_parser = parse_key_val)] - name: Vec<(String, String)>, + #[arg(value_parser = parse_mapping)] + mapping: Vec, #[clap(long)] - environment: String, + environment: EnvironmentName, + + /// Answer yes to all questions. + #[clap(short = 'y', long = "yes", long = "assume-yes")] + assume_yes: bool, #[clap(flatten)] config: ConfigCli, } -/// Parse a single key-value pair -fn parse_key_val(s: &str) -> Result<(String, String), Box> { - let pos = s - .find('=') - .ok_or_else(|| format!("invalid KEY=value: no `=` found in `{}`", s))?; - let key = s[..pos].to_string(); - let value = s[pos + 1..].to_string(); - Ok((key, value)) +/// Parse mapping between exposed key and executable name +fn parse_mapping(input: &str) -> miette::Result { + input + .split_once('=') + .ok_or_else(|| { + miette::miette!("Could not parse mapping `exposed_key=executable_name` from {input}") + }) + .and_then(|(key, value)| Ok(Mapping::new(ExposedKey::from_str(key)?, value.to_string()))) } - #[derive(Parser, Debug)] pub struct RemoveArgs { /// The binary or binaries to remove as executable (e.g. python atuin) - name: Vec, + exposed_keys: Vec, #[clap(long)] - environment: String, + environment: EnvironmentName, + + /// Answer yes to all questions. + #[clap(short = 'y', long = "yes", long = "assume-yes")] + assume_yes: bool, #[clap(flatten)] config: ConfigCli, @@ -70,44 +82,43 @@ pub async fn execute(args: SubCommand) -> miette::Result<()> { } pub async fn add(args: AddArgs) -> miette::Result<()> { - // should we do a sync first? - let mut project = global::Project::discover()?.with_cli_config(args.config); - let env_name: EnvironmentName = args.environment.parse()?; - expose_add(&mut project, env_name, args.name).await?; - - let config = project.config(); - - // after https://github.com/prefix-dev/pixi/pull/1975 PR lands - // this can be simplified by just passing the project - let (_, auth_client) = build_reqwest_clients(Some(&config)); - - let gateway = config.gateway(auth_client.clone()); + let config = Config::with_cli_config(&args.config); - let env_root = EnvRoot::from_env().await?; let bin_dir = BinDir::from_env().await?; + let env_root = EnvRoot::from_env().await?; + let mut project = global::Project::discover_or_create(env_root, bin_dir, args.assume_yes) + .await? + .with_cli_config(config.clone()); + for mapping in args.mapping { + project.manifest.add_exposed_binary( + &args.environment, + mapping.exposed_key, + mapping.executable_name, + )?; + } - global::sync(&env_root, &project, &bin_dir, config, &gateway, &auth_client).await + project.manifest.save()?; + + global::sync(&project, &config).await } pub async fn remove(args: RemoveArgs) -> miette::Result<()> { - // should we do a sync first? - let mut project = global::Project::discover()?.with_cli_config(args.config); - let env_name: EnvironmentName = args.environment.parse()?; - expose_remove(&mut project, env_name, args.name).await?; - - let config = project.config(); + let config = Config::with_cli_config(&args.config); - // after https://github.com/prefix-dev/pixi/pull/1975 PR lands - // this can be simplified by just passing the project - let (_, auth_client) = build_reqwest_clients(Some(&config)); - - let gateway = config.gateway(auth_client.clone()); - - let env_root = EnvRoot::from_env().await?; let bin_dir = BinDir::from_env().await?; + let env_root = EnvRoot::from_env().await?; + let mut project = global::Project::discover_or_create(env_root, bin_dir, args.assume_yes) + .await? + .with_cli_config(config.clone()); + for exposed_key in args.exposed_keys { + project + .manifest + .remove_exposed_binary(&args.environment, &exposed_key)?; + } - global::sync(&env_root, &project, &bin_dir, config, &gateway, &auth_client).await + project.manifest.save()?; + global::sync(&project, &config).await } diff --git a/src/cli/global/sync.rs b/src/cli/global/sync.rs index 85b0edf50..edf478028 100644 --- a/src/cli/global/sync.rs +++ b/src/cli/global/sync.rs @@ -1,4 +1,4 @@ -use crate::global::{self}; +use crate::global::{self, BinDir, EnvRoot}; use clap::Parser; use pixi_config::{Config, ConfigCli}; @@ -15,6 +15,12 @@ pub struct Args { /// Sync global manifest with installed environments pub async fn execute(args: Args) -> miette::Result<()> { let config = Config::with_cli_config(&args.config); + let bin_dir = BinDir::from_env().await?; + let env_root = EnvRoot::from_env().await?; - global::sync(&config, args.assume_yes).await + let project = global::Project::discover_or_create(env_root, bin_dir, args.assume_yes) + .await? + .with_cli_config(config.clone()); + + global::sync(&project, &config).await } diff --git a/src/global/common.rs b/src/global/common.rs index 052ae2efc..fe5bbcd45 100644 --- a/src/global/common.rs +++ b/src/global/common.rs @@ -11,6 +11,7 @@ use pixi_config::home_path; use super::{EnvironmentName, ExposedKey}; /// Global binaries directory, default to `$HOME/.pixi/bin` +#[derive(Debug, Clone)] pub struct BinDir(PathBuf); impl BinDir { @@ -190,42 +191,20 @@ impl EnvRoot { /// A global environment directory pub(crate) struct EnvDir { - pub(crate) root: EnvRoot, - pub(crate) bin_dir: BinDir, pub(crate) path: PathBuf, } impl EnvDir { - /// Create a global environment directory - pub(crate) async fn new>(environment_name: T) -> miette::Result { - let root = EnvRoot::from_env().await?; - let bin_dir = BinDir::from_env().await?; - let environment_name = environment_name.into(); - let path = root.path().join(environment_name.as_str()); - tokio::fs::create_dir_all(&path).await.into_diagnostic()?; - - Ok(Self { - root, - bin_dir, - path, - }) - } - /// Create a global environment directory based on passed global environment root pub(crate) async fn from_env_root>( env_root: EnvRoot, environment_name: T, ) -> miette::Result { - let bin_dir = BinDir::from_env().await?; let environment_name = environment_name.into(); let path = env_root.path().join(environment_name.as_str()); tokio::fs::create_dir_all(&path).await.into_diagnostic()?; - Ok(Self { - root: env_root, - bin_dir, - path, - }) + Ok(Self { path }) } /// Construct the path to the env directory for the environment diff --git a/src/global/expose.rs b/src/global/expose.rs deleted file mode 100644 index 7bb392300..000000000 --- a/src/global/expose.rs +++ /dev/null @@ -1,219 +0,0 @@ -use std::{path::PathBuf, str::FromStr}; - -use pixi_config::Config; -use rattler_shell::shell::ShellEnum; -use tokio::fs; - -use crate::{ - global::{self, BinDir, EnvRoot}, - prefix::{create_activation_script, Prefix}, -}; - -use miette::{Error, IntoDiagnostic, Report}; - -use super::{create_executable_scripts, script_exec_mapping, EnvDir, EnvironmentName, ExposedKey}; - -pub(crate) async fn expose_add( - project: &mut global::Project, - env_name: EnvironmentName, - bin_names_to_expose: Vec<(String, String)>, -) -> miette::Result<()> { - // verify that environment exist - let exposed_by_env = project - .environments() - .get(&env_name) - .ok_or_else(|| miette::miette!("Environment {env_name} not found"))?; - - let bin_env_dir = EnvDir::new(env_name.clone()).await?; - - let prefix = Prefix::new(bin_env_dir.path()); - - let prefix_records = prefix.find_installed_packages(None).await?; - - eprintln!("installed packages: {prefix_records:?}"); - let all_executables: Vec<(String, PathBuf)> = - prefix.find_executables(prefix_records.as_slice()); - - eprintln!("all execs : {all_executables:?}"); - - let installed_binaries: Vec<&String> = all_executables - .iter() - .map(|(binary_name, _)| binary_name) - .collect(); - - // Check if all binaries that are to be exposed are present in the environment - tracing::debug!("installed binaries : {installed_binaries:?}"); - tracing::debug!("binary to expose: {bin_names_to_expose:?}"); - - bin_names_to_expose - .iter() - .try_for_each(|(_, binary_name)| { - installed_binaries - .contains(&binary_name) - .then(|| ()) - .ok_or_else(|| miette::miette!("Binary for exposure {binary_name} is not present in the environment {env_name}")) - })?; - - for (name_to_exposed, real_binary_to_be_exposed) in bin_names_to_expose.iter() { - let exposed_key = ExposedKey::from_str(&name_to_exposed)?; - - project - .manifest - .add_exposed_binary( - &env_name, - exposed_key, - real_binary_to_be_exposed.to_string(), - ) - .unwrap(); - project.manifest.save()?; - } - Ok(()) -} - -pub(crate) async fn expose_remove( - project: &mut global::Project, - environment_name: EnvironmentName, - bin_names_to_remove: Vec, -) -> miette::Result<()> { - // verify that environment exist - let exposed_by_env = project - .environments() - .get(&environment_name) - .ok_or_else(|| miette::miette!("Environment {environment_name} not found"))?; - - bin_names_to_remove.iter().try_for_each(|binary_name| { - let exposed_key = ExposedKey::from_str(binary_name)?; - if !exposed_by_env.exposed.contains_key(&exposed_key) { - miette::bail!("Binary {binary_name} not found in the {environment_name} environment"); - } - Ok(()) - })?; - - let bin_env_dir = EnvDir::new(environment_name.clone()).await?; - - for binary_name in bin_names_to_remove.iter() { - let exposed_key = ExposedKey::from_str(binary_name)?; - // remove from map - project - .manifest - .remove_exposed_binary(&environment_name, &exposed_key)?; - } - project.manifest.save()?; - - Ok(()) -} - - -// mod tests { -// use std::{env, fs}; -// use std::fs::{set_permissions, File}; -// use std::os::unix::fs::PermissionsExt; -// use std::path::Path; -// use std::str::FromStr; - -// use minijinja::Environment; -// use pixi_manifest::ParsedManifest; - -// use crate::global::{self, expose_add, EnvDir, EnvironmentName, ExposedKey}; -// use crate::global::project::Manifest; - - -// fn create_empty_executable_file(dir: &Path, executable_name: &str) -> miette::Result<()> { -// // Define the path to the empty executable file -// let file_path = dir.join("bin").join(executable_name); - -// fs::create_dir_all(file_path.parent().unwrap()).unwrap(); - -// // Create the empty file -// let _file = File::create(file_path.clone()).unwrap(); - -// // Set executable permissions (Unix-like systems) -// #[cfg(unix)] -// { -// let mut permissions = std::fs::metadata(file_path.clone()).unwrap().permissions(); -// // Set the executable bit (0o755 means read/write/execute for owner, read/execute for group and others) -// permissions.set_mode(0o755); -// set_permissions(file_path, permissions).unwrap(); -// } - -// Ok(()) -// } - - -// #[tokio::test] -// async fn test_expose_add_when_binary_exist() { -// let contents = r#" -// [envs.python-3-10] -// channels = ["conda-forge"] -// [envs.python-3-10.dependencies] -// python = "3.10" -// [envs.python-3-10.exposed] -// python = "python" -// python3 = "python" -// "#; - -// let tmp_dir = tempfile::tempdir().unwrap(); - -// let manifest_path = tmp_dir.path().join("pixi-global-manifest.toml"); - -// let manifest = Manifest::from_str(&manifest_path, contents).unwrap(); - -// let mut project = global::Project::from_manifest(manifest); - -// // inject a fake environment -// let env_name = EnvironmentName::from_str("python-3-10").unwrap(); - -// let env_dir = EnvDir::new(env_name.clone()).await.unwrap(); - -// let test_folder_path = format!( -// "{}/{}", -// env!("CARGO_MANIFEST_DIR"), -// "tests/data/conda-meta/atuin.json" -// ); - -// let content = std::fs::read_to_string(test_folder_path).unwrap(); - -// let atuin_content = env_dir.path().join("conda-meta").join("atuin-18.3.0-h6e96688_0.json"); -// std::fs::create_dir_all(atuin_content.parent().unwrap()).unwrap(); -// std::fs::write(atuin_content, content).unwrap(); - - -// // create also an empty executable -// create_empty_executable_file(env_dir.path(), "atuin").unwrap(); - - -// expose_add(&mut project, "python-3-10".parse().unwrap(), vec![("atuin".to_string(), "atuin".to_string())]).await.unwrap(); - -// let exposed_key = ExposedKey::from_str("atuin").unwrap(); -// assert!(project.manifest.parsed.envs().get(&env_name).unwrap().exposed.contains_key(&exposed_key)); - -// insta::assert_snapshot!(project.manifest.document.to_string()); -// } - -// #[tokio::test] -// async fn test_expose_add_when_exposing_non_existing_binary() { -// let contents = r#" -// [envs.python-3-10] -// channels = ["conda-forge"] -// [envs.python-3-10.dependencies] -// python = "3.10" -// [envs.python-3-10.exposed] -// python = "python" -// python3 = "python" -// "#; - -// let tmp_dir = tempfile::tempdir().unwrap(); - -// let manifest_path = tmp_dir.path().join("pixi-global-manifest.toml"); - -// let manifest = Manifest::from_str(&manifest_path, contents).unwrap(); - -// let mut project = global::Project::from_manifest(manifest); - -// let result = expose_add(&mut project, "python-3-10".parse().unwrap(), vec![("non-existing-library".to_string(), "non-existing-library".to_string())]).await.unwrap(); -// insta::assert_snapshot!(result.unwrap_err().to_string()); - - - -// } -// } diff --git a/src/global/install.rs b/src/global/install.rs index e4f66c483..41b66ce63 100644 --- a/src/global/install.rs +++ b/src/global/install.rs @@ -1,4 +1,9 @@ -use std::{collections::{HashMap, HashSet}, ffi::OsStr, path::PathBuf, str::FromStr}; +use std::{ + collections::{HashMap, HashSet}, + ffi::OsStr, + path::PathBuf, + str::FromStr, +}; use indexmap::IndexMap; use itertools::Itertools; @@ -23,7 +28,7 @@ use rattler_solve::{resolvo::Solver, SolverImpl, SolverTask}; use rattler_virtual_packages::{VirtualPackage, VirtualPackageOverrides}; use reqwest_middleware::ClientWithMiddleware; -use super::{common::EnvRoot, project::ParsedEnvironment, EnvironmentName, ExposedKey}; +use super::{project::ParsedEnvironment, EnvironmentName, ExposedKey}; use crate::{ global::{self, BinDir, EnvDir}, prefix::Prefix, @@ -45,7 +50,6 @@ pub(crate) async fn install_environment( .map(|channel| channel.clone().into_channel(config.global_channel_config())) .collect_vec(); - let platform = parsed_environment .platform() .unwrap_or_else(Platform::current); @@ -119,7 +123,6 @@ pub(crate) async fn install_environment( pub(crate) async fn expose_executables( env_name: &EnvironmentName, parsed_environment: &ParsedEnvironment, - packages: Vec, prefix: &Prefix, bin_dir: &BinDir, ) -> miette::Result { @@ -418,22 +421,15 @@ pub(crate) fn prompt_user_to_continue( Ok(true) } -pub(crate) async fn sync(config: &Config, assume_yes: bool) -> Result<(), miette::Error> { - // Create directories - let bin_dir = BinDir::from_env().await?; - let env_root = EnvRoot::from_env().await?; - - let project = global::Project::discover_or_create(&bin_dir, &env_root, assume_yes) - .await? - .with_cli_config(config.clone()); - +pub(crate) async fn sync(project: &global::Project, config: &Config) -> Result<(), miette::Error> { // Fetch the repodata let (_, auth_client) = build_reqwest_clients(Some(config)); let gateway = config.gateway(auth_client.clone()); // Prune environments that are not listed - env_root + project + .env_root .prune(project.environments().keys().cloned()) .await?; @@ -445,10 +441,10 @@ pub(crate) async fn sync(config: &Config, assume_yes: bool) -> Result<(), miette environment .exposed .keys() - .map(|e| bin_dir.executable_script_path(e)) + .map(|e| project.bin_dir.executable_script_path(e)) }) .collect_vec(); - for file in bin_dir.files().await? { + for file in project.bin_dir.files().await? { let file_name = file .file_stem() .and_then(OsStr::to_str) @@ -484,7 +480,7 @@ pub(crate) async fn sync(config: &Config, assume_yes: bool) -> Result<(), miette }) .collect::, miette::Report>>()?; - let env_dir = EnvDir::from_env_root(env_root.clone(), env_name.clone()).await?; + let env_dir = EnvDir::from_env_root(project.env_root.clone(), env_name.clone()).await?; let prefix = Prefix::new(env_dir.path()); let prefix_records = prefix.find_installed_packages(Some(50)).await?; @@ -492,7 +488,7 @@ pub(crate) async fn sync(config: &Config, assume_yes: bool) -> Result<(), miette if !specs_match_local_environment(&specs, prefix_records, parsed_environment.platform()) { install_environment( &specs, - &parsed_environment, + parsed_environment, auth_client.clone(), &prefix, config, @@ -501,14 +497,7 @@ pub(crate) async fn sync(config: &Config, assume_yes: bool) -> Result<(), miette .await?; } - expose_executables( - &env_name, - &parsed_environment, - specs.keys().cloned().collect(), - &prefix, - &bin_dir, - ) - .await?; + expose_executables(env_name, parsed_environment, &prefix, &project.bin_dir).await?; } Ok(()) diff --git a/src/global/mod.rs b/src/global/mod.rs index 846ac1459..8719b4b22 100644 --- a/src/global/mod.rs +++ b/src/global/mod.rs @@ -1,14 +1,11 @@ mod common; -mod expose; mod install; mod project; // mod document; -pub(crate) use expose::{expose_add, expose_remove}; - -pub(crate) use common::{BinDir, EnvDir, EnvRoot,}; -pub(crate) use install::{create_executable_scripts, script_exec_mapping, sync}; -pub(crate) use project::{EnvironmentName, ExposedKey, Project, MANIFEST_DEFAULT_NAME}; +pub(crate) use common::{BinDir, EnvDir, EnvRoot}; +pub(crate) use install::sync; +pub(crate) use project::{EnvironmentName, ExposedKey, Project}; // pub(crate) use document::ManifestSource; use crate::prefix::Prefix; diff --git a/src/global/project/manifest.rs b/src/global/project/manifest.rs index 83ee34802..9baf9c4f3 100644 --- a/src/global/project/manifest.rs +++ b/src/global/project/manifest.rs @@ -93,26 +93,16 @@ impl Manifest { &mut self, env_name: &EnvironmentName, exposed_name: ExposedKey, - actual_bin: String, + executable_name: String, ) -> miette::Result<()> { - let table_name = format!("envs.{}.exposed", env_name); - self.document - .get_or_insert_nested_table(&table_name)? + .get_or_insert_nested_table(&format!("envs.{env_name}.exposed"))? .insert( exposed_name.as_str(), - Item::Value(toml_edit::Value::from(actual_bin.clone())), + Item::Value(toml_edit::Value::from(executable_name.clone())), ); - let mut envs = self - .parsed - .get_mut_env(env_name) - .ok_or_else(|| miette::miette!("Environment {env_name} not found"))?; - - envs.exposed - .insert(exposed_name.clone(), actual_bin.clone()); - - tracing::debug!("added {}={} in toml document", exposed_name, actual_bin); + tracing::debug!("Added exposed mapping {exposed_name}={executable_name} to toml document"); Ok(()) } @@ -121,20 +111,11 @@ impl Manifest { env_name: &EnvironmentName, exposed_name: &ExposedKey, ) -> miette::Result<()> { - let table_name = format!("envs.{}.exposed", env_name); - self.document - .get_or_insert_nested_table(&table_name)? + .get_or_insert_nested_table(&format!("envs.{env_name}.exposed"))? .remove(exposed_name.as_str()); - let mut envs = self - .parsed - .get_mut_env(env_name) - .ok_or_else(|| miette::miette!("Environment {env_name} not found"))?; - - envs.exposed.swap_remove(exposed_name); - - tracing::debug!("removed {} from manifest", exposed_name); + tracing::debug!("Removed exposed mapping {exposed_name} from toml document"); Ok(()) } diff --git a/src/global/project/mod.rs b/src/global/project/mod.rs index 7c0fdce3b..cc2a95a5c 100644 --- a/src/global/project/mod.rs +++ b/src/global/project/mod.rs @@ -9,11 +9,9 @@ pub(crate) use environment::EnvironmentName; use indexmap::IndexMap; pub(crate) use manifest::Manifest; use miette::{Context, IntoDiagnostic}; +use once_cell::sync::Lazy; pub(crate) use parsed_manifest::ExposedKey; pub(crate) use parsed_manifest::ParsedEnvironment; -use rattler_repodata_gateway::Gateway; -use reqwest_middleware::ClientWithMiddleware; -use once_cell::sync::Lazy; use parsed_manifest::ParsedManifest; use pixi_config::{home_path, Config}; use pixi_manifest::PrioritizedChannel; @@ -46,6 +44,10 @@ pub struct Project { pub(crate) manifest: Manifest, /// The global configuration as loaded from the config file(s) config: Config, + /// Root directory of the global environments + pub(crate) env_root: EnvRoot, + /// Binary directory + pub(crate) bin_dir: BinDir, } impl Debug for Project { @@ -227,7 +229,7 @@ async fn package_from_conda_meta( impl Project { /// Constructs a new instance from an internal manifest representation - pub(crate) fn from_manifest(manifest: Manifest) -> Self { + pub(crate) fn from_manifest(manifest: Manifest, env_root: EnvRoot, bin_dir: BinDir) -> Self { let root = manifest .path .parent() @@ -240,13 +242,20 @@ impl Project { root, manifest, config, + env_root, + bin_dir, } } /// Constructs a project from a manifest. - pub(crate) fn from_str(manifest_path: &Path, content: &str) -> miette::Result { + pub(crate) fn from_str( + manifest_path: &Path, + content: &str, + env_root: EnvRoot, + bin_dir: BinDir, + ) -> miette::Result { let manifest = Manifest::from_str(manifest_path, content)?; - Ok(Self::from_manifest(manifest)) + Ok(Self::from_manifest(manifest, env_root, bin_dir)) } /// Discovers the project manifest file in path at @@ -254,8 +263,8 @@ impl Project { /// yet, and the function will try to create one from the existing /// installation. If that one fails, an empty one will be created. pub(crate) async fn discover_or_create( - bin_dir: &BinDir, - env_root: &EnvRoot, + env_root: EnvRoot, + bin_dir: BinDir, assume_yes: bool, ) -> miette::Result { let manifest_dir = Self::manifest_dir()?; @@ -283,7 +292,7 @@ impl Project { .interact() .into_diagnostic()?) { - return Self::try_from_existing_installation(&manifest_path, bin_dir, env_root) + return Self::try_from_existing_installation(&manifest_path, env_root, bin_dir) .await .wrap_err_with(|| { "Failed to create global manifest from existing installation" @@ -296,13 +305,13 @@ impl Project { .wrap_err_with(|| format!("Couldn't create file {}", manifest_path.display()))?; } - Self::from_path(&manifest_path) + Self::from_path(&manifest_path, env_root, bin_dir) } async fn try_from_existing_installation( manifest_path: &Path, - bin_dir: &BinDir, - env_root: &EnvRoot, + env_root: EnvRoot, + bin_dir: BinDir, ) -> miette::Result { let futures = bin_dir .files() @@ -313,9 +322,9 @@ impl Project { Ok(false) => None, // Success and isn't text, filter out Err(e) => Some(Err(e)), // Failure, continue with error }) - .map(|result| async move { + .map(|result| async { match result { - Ok(path) => ExposedData::from_exposed_path(&path, env_root).await, + Ok(path) => ExposedData::from_exposed_path(&path, &env_root).await, Err(e) => Err(e), } }); @@ -327,7 +336,7 @@ impl Project { tokio::fs::write(&manifest_path, &toml) .await .into_diagnostic()?; - Self::from_str(manifest_path, &toml) + Self::from_str(manifest_path, &toml, env_root, bin_dir) } /// Get default dir for the pixi global manifest @@ -338,9 +347,13 @@ impl Project { } /// Loads a project from manifest file. - pub(crate) fn from_path(manifest_path: &Path) -> miette::Result { + pub(crate) fn from_path( + manifest_path: &Path, + env_root: EnvRoot, + bin_dir: BinDir, + ) -> miette::Result { let manifest = Manifest::from_path(manifest_path)?; - Ok(Project::from_manifest(manifest)) + Ok(Project::from_manifest(manifest, env_root, bin_dir)) } /// Merge config with existing config project @@ -356,15 +369,6 @@ impl Project { pub(crate) fn environments(&self) -> &IndexMap { self.manifest.parsed.envs() } - - /// Returns a specific environment based by name. - pub(crate) fn environment(&self, name: &EnvironmentName) -> Option<&ParsedEnvironment> { - self.manifest.parsed.envs().get(name) - } - - pub(crate) fn config(&self) -> &Config { - &self.config - } } #[cfg(test)] @@ -384,23 +388,29 @@ mod tests { python = "python" "#; - #[test] - fn test_project_from_str() { + #[tokio::test] + async fn test_project_from_str() { let manifest_path: PathBuf = FilePath().fake(); + let env_root = EnvRoot::from_env().await.unwrap(); + let bin_dir = BinDir::from_env().await.unwrap(); - let project = Project::from_str(&manifest_path, SIMPLE_MANIFEST).unwrap(); + let project = + Project::from_str(&manifest_path, SIMPLE_MANIFEST, env_root, bin_dir).unwrap(); assert_eq!(project.root, manifest_path.parent().unwrap()); } - #[test] - fn test_project_from_path() { + #[tokio::test] + async fn test_project_from_path() { let tempdir = tempfile::tempdir().unwrap(); let manifest_path = tempdir.path().join(MANIFEST_DEFAULT_NAME); + let env_root = EnvRoot::from_env().await.unwrap(); + let bin_dir = BinDir::from_env().await.unwrap(); + // Create and write global manifest let mut file = std::fs::File::create(&manifest_path).unwrap(); file.write_all(SIMPLE_MANIFEST.as_bytes()).unwrap(); - let project = Project::from_path(&manifest_path).unwrap(); + let project = Project::from_path(&manifest_path, env_root, bin_dir).unwrap(); // Canonicalize both paths let canonical_root = project.root.canonicalize().unwrap(); @@ -409,12 +419,15 @@ mod tests { assert_eq!(canonical_root, canonical_manifest_parent); } - #[test] - fn test_project_from_manifest() { + #[tokio::test] + async fn test_project_from_manifest() { let manifest_path: PathBuf = FilePath().fake(); + let env_root = EnvRoot::from_env().await.unwrap(); + let bin_dir = BinDir::from_env().await.unwrap(); + let manifest = Manifest::from_str(&manifest_path, SIMPLE_MANIFEST).unwrap(); - let project = Project::from_manifest(manifest); + let project = Project::from_manifest(manifest, env_root, bin_dir); assert_eq!(project.root, manifest_path.parent().unwrap()); } diff --git a/src/global/project/parsed_manifest.rs b/src/global/project/parsed_manifest.rs index 6c19e796c..db1c8d8d9 100644 --- a/src/global/project/parsed_manifest.rs +++ b/src/global/project/parsed_manifest.rs @@ -61,13 +61,6 @@ impl ParsedManifest { pub(crate) fn envs(&self) -> &IndexMap { &self.envs } - - pub(crate) fn get_mut_env( - &mut self, - key: &EnvironmentName, - ) -> Option<&mut ParsedEnvironment> { - self.envs.get_mut(key) - } } impl<'de> serde::Deserialize<'de> for ParsedManifest { From 0be20708c964de1f79b9f2b26629a7989efde22a Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Mon, 23 Sep 2024 13:55:17 +0200 Subject: [PATCH 08/26] Adapt environment flag --- src/cli/global/expose.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/global/expose.rs b/src/cli/global/expose.rs index f167a5cb4..11fdbffe9 100644 --- a/src/cli/global/expose.rs +++ b/src/cli/global/expose.rs @@ -27,7 +27,7 @@ pub struct AddArgs { #[arg(value_parser = parse_mapping)] mapping: Vec, - #[clap(long)] + #[clap(short, long)] environment: EnvironmentName, /// Answer yes to all questions. From 611bad7fb830bbddec8e0b887ca85dfe4dccc372 Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Mon, 23 Sep 2024 17:31:32 +0200 Subject: [PATCH 09/26] First mostly working version --- crates/pixi_manifest/src/error.rs | 3 + src/cli/global/expose.rs | 125 +++++++++++++++++--------- src/global/common.rs | 4 +- src/global/install.rs | 4 +- src/global/mod.rs | 2 +- src/global/project/manifest.rs | 78 ++++++++-------- src/global/project/mod.rs | 10 +-- src/global/project/parsed_manifest.rs | 41 +++------ src/prefix.rs | 1 + 9 files changed, 150 insertions(+), 118 deletions(-) diff --git a/crates/pixi_manifest/src/error.rs b/crates/pixi_manifest/src/error.rs index d169fb46c..fe42d8f84 100644 --- a/crates/pixi_manifest/src/error.rs +++ b/crates/pixi_manifest/src/error.rs @@ -1,3 +1,6 @@ +// TODO: remove +#![allow(dead_code)] + use std::{borrow::Borrow, fmt::Display}; use itertools::Itertools; diff --git a/src/cli/global/expose.rs b/src/cli/global/expose.rs index 11fdbffe9..de6513214 100644 --- a/src/cli/global/expose.rs +++ b/src/cli/global/expose.rs @@ -2,30 +2,16 @@ use std::str::FromStr; use crate::global::{BinDir, EnvRoot}; use clap::Parser; +use miette::Context; use pixi_config::{Config, ConfigCli}; -use crate::global::{self, EnvironmentName, ExposedKey}; - -#[derive(Debug, Clone)] -struct Mapping { - exposed_key: ExposedKey, - executable_name: String, -} - -impl Mapping { - pub fn new(exposed_key: ExposedKey, executable_name: String) -> Self { - Self { - exposed_key, - executable_name, - } - } -} +use crate::global::{self, EnvironmentName, ExposedName}; #[derive(Parser, Debug)] pub struct AddArgs { /// The binary to add as executable in the form of key=value (e.g. python=python3.10) #[arg(value_parser = parse_mapping)] - mapping: Vec, + mappings: Vec, #[clap(short, long)] environment: EnvironmentName, @@ -38,21 +24,26 @@ pub struct AddArgs { config: ConfigCli, } -/// Parse mapping between exposed key and executable name -fn parse_mapping(input: &str) -> miette::Result { +/// Parse mapping between exposed name and executable name +fn parse_mapping(input: &str) -> miette::Result { input .split_once('=') .ok_or_else(|| { - miette::miette!("Could not parse mapping `exposed_key=executable_name` from {input}") + miette::miette!("Could not parse mapping `exposed_name=executable_name` from {input}") + }) + .and_then(|(key, value)| { + Ok(global::Mapping::new( + ExposedName::from_str(key)?, + value.to_string(), + )) }) - .and_then(|(key, value)| Ok(Mapping::new(ExposedKey::from_str(key)?, value.to_string()))) } #[derive(Parser, Debug)] pub struct RemoveArgs { /// The binary or binaries to remove as executable (e.g. python atuin) - exposed_keys: Vec, + exposed_names: Vec, - #[clap(long)] + #[clap(short, long)] environment: EnvironmentName, /// Answer yes to all questions. @@ -87,20 +78,44 @@ pub async fn add(args: AddArgs) -> miette::Result<()> { let bin_dir = BinDir::from_env().await?; let env_root = EnvRoot::from_env().await?; - let mut project = global::Project::discover_or_create(env_root, bin_dir, args.assume_yes) - .await? - .with_cli_config(config.clone()); - for mapping in args.mapping { - project.manifest.add_exposed_binary( - &args.environment, - mapping.exposed_key, - mapping.executable_name, - )?; + let mut project_original = + global::Project::discover_or_create(env_root, bin_dir, args.assume_yes) + .await? + .with_cli_config(config.clone()); + + // Make sure that the manifest is up-to-date with the local installation + global::sync(&project_original, &config).await?; + + if let Err(err) = apply_add_changes(args, project_original.clone(), &config).await { + let err = err.wrap_err("Could not add mappings"); + project_original + .manifest + .save() + .await + .wrap_err_with(|| format!("{}\nReverting also failed", &err))?; + global::sync(&project_original, &config) + .await + .wrap_err_with(|| format!("{}\nReverting also failed", &err))?; + return Err(err); } + Ok(()) +} - project.manifest.save()?; +async fn apply_add_changes( + args: AddArgs, + project_original: global::Project, + config: &Config, +) -> Result<(), miette::Error> { + let mut project_modified = project_original.clone(); - global::sync(&project, &config).await + for mapping in args.mappings { + project_modified + .manifest + .add_exposed_mapping(&args.environment, &mapping)?; + } + project_modified.manifest.save().await?; + global::sync(&project_modified, config).await?; + Ok(()) } pub async fn remove(args: RemoveArgs) -> miette::Result<()> { @@ -109,16 +124,42 @@ pub async fn remove(args: RemoveArgs) -> miette::Result<()> { let bin_dir = BinDir::from_env().await?; let env_root = EnvRoot::from_env().await?; - let mut project = global::Project::discover_or_create(env_root, bin_dir, args.assume_yes) - .await? - .with_cli_config(config.clone()); - for exposed_key in args.exposed_keys { - project + let mut project_original = + global::Project::discover_or_create(env_root, bin_dir, args.assume_yes) + .await? + .with_cli_config(config.clone()); + + // Make sure that the manifest is up-to-date with the local installation + global::sync(&project_original, &config).await?; + + if let Err(err) = apply_remove_changes(args, project_original.clone(), &config).await { + let err = err.wrap_err("Could not remove exposed names"); + project_original .manifest - .remove_exposed_binary(&args.environment, &exposed_key)?; + .save() + .await + .wrap_err_with(|| format!("{}\nReverting also failed", &err))?; + global::sync(&project_original, &config) + .await + .wrap_err_with(|| format!("{}\nReverting also failed", &err))?; + return Err(err); } + Ok(()) +} - project.manifest.save()?; +async fn apply_remove_changes( + args: RemoveArgs, + project_original: global::Project, + config: &Config, +) -> Result<(), miette::Error> { + let mut project_modified = project_original.clone(); - global::sync(&project, &config).await + for exposed_name in args.exposed_names { + project_modified + .manifest + .remove_exposed_name(&args.environment, &exposed_name)?; + } + project_modified.manifest.save().await?; + global::sync(&project_modified, config).await?; + Ok(()) } diff --git a/src/global/common.rs b/src/global/common.rs index fe5bbcd45..64d97dcf4 100644 --- a/src/global/common.rs +++ b/src/global/common.rs @@ -8,7 +8,7 @@ use miette::{Context, IntoDiagnostic}; use pixi_config::home_path; -use super::{EnvironmentName, ExposedKey}; +use super::{EnvironmentName, ExposedName}; /// Global binaries directory, default to `$HOME/.pixi/bin` #[derive(Debug, Clone)] @@ -60,7 +60,7 @@ impl BinDir { /// This function constructs the path to the executable script by joining the /// `bin_dir` with the provided `exposed_name`. If the target platform is /// Windows, it sets the file extension to `.bat`. - pub(crate) fn executable_script_path(&self, exposed_name: &ExposedKey) -> PathBuf { + pub(crate) fn executable_script_path(&self, exposed_name: &ExposedName) -> PathBuf { let mut executable_script_path = self.0.join(exposed_name.to_string()); if cfg!(windows) { executable_script_path.set_extension("bat"); diff --git a/src/global/install.rs b/src/global/install.rs index 41b66ce63..ad245bc1d 100644 --- a/src/global/install.rs +++ b/src/global/install.rs @@ -28,7 +28,7 @@ use rattler_solve::{resolvo::Solver, SolverImpl, SolverTask}; use rattler_virtual_packages::{VirtualPackage, VirtualPackageOverrides}; use reqwest_middleware::ClientWithMiddleware; -use super::{project::ParsedEnvironment, EnvironmentName, ExposedKey}; +use super::{project::ParsedEnvironment, EnvironmentName, ExposedName}; use crate::{ global::{self, BinDir, EnvDir}, prefix::Prefix, @@ -180,7 +180,7 @@ pub(crate) async fn expose_executables( /// /// Returns an error if the entry point is not found in the list of executable names. pub(crate) fn script_exec_mapping<'a>( - exposed_name: &ExposedKey, + exposed_name: &ExposedName, entry_point: &str, mut executables: impl Iterator, bin_dir: &BinDir, diff --git a/src/global/mod.rs b/src/global/mod.rs index 8719b4b22..d4da304c8 100644 --- a/src/global/mod.rs +++ b/src/global/mod.rs @@ -5,7 +5,7 @@ mod project; pub(crate) use common::{BinDir, EnvDir, EnvRoot}; pub(crate) use install::sync; -pub(crate) use project::{EnvironmentName, ExposedKey, Project}; +pub(crate) use project::{EnvironmentName, ExposedName, Mapping, Project}; // pub(crate) use document::ManifestSource; use crate::prefix::Prefix; diff --git a/src/global/project/manifest.rs b/src/global/project/manifest.rs index 9baf9c4f3..a25b74a40 100644 --- a/src/global/project/manifest.rs +++ b/src/global/project/manifest.rs @@ -1,8 +1,8 @@ +use std::fmt; use std::path::{Path, PathBuf}; use miette::IntoDiagnostic; use pixi_manifest::TomlManifest; -use rattler_conda_types::{MatchSpec, PackageName}; use toml_edit::{DocumentMut, Item}; // use crate::global::document::ManifestSource; @@ -10,7 +10,7 @@ use toml_edit::{DocumentMut, Item}; use super::error::ManifestError; use super::parsed_manifest::ParsedManifest; -use super::{EnvironmentName, ExposedKey, MANIFEST_DEFAULT_NAME}; +use super::{EnvironmentName, ExposedName, MANIFEST_DEFAULT_NAME}; // use super::document::ManifestSource; // TODO: remove @@ -26,9 +26,6 @@ pub struct Manifest { /// The path to the manifest file pub path: PathBuf, - /// The raw contents of the manifest file - pub contents: String, - /// Editable toml document pub document: TomlManifest, @@ -61,7 +58,6 @@ impl Manifest { let manifest = Self { path: manifest_path.to_path_buf(), - contents, document: TomlManifest::new(document), parsed: manifest, }; @@ -69,60 +65,64 @@ impl Manifest { Ok(manifest) } - /// Adds an environment to the project. - pub fn add_environment(&mut self, _name: String) -> miette::Result<()> { - todo!() - } - - /// Removes an environment from the project. - pub fn remove_environment(&mut self, _name: &str) -> miette::Result { - todo!() - } - - /// Add a matchspec to the manifest - pub fn add_dependency(&mut self, _spec: &MatchSpec) -> miette::Result { - todo!() - } - - /// Removes a dependency based on `SpecType`. - pub fn remove_dependency(&mut self, _dep: &PackageName) -> miette::Result<()> { - todo!() - } - - pub fn add_exposed_binary( + pub fn add_exposed_mapping( &mut self, env_name: &EnvironmentName, - exposed_name: ExposedKey, - executable_name: String, + mapping: &Mapping, ) -> miette::Result<()> { self.document .get_or_insert_nested_table(&format!("envs.{env_name}.exposed"))? .insert( - exposed_name.as_str(), - Item::Value(toml_edit::Value::from(executable_name.clone())), + &mapping.exposed_name.to_string(), + Item::Value(toml_edit::Value::from(mapping.executable_name.clone())), ); - tracing::debug!("Added exposed mapping {exposed_name}={executable_name} to toml document"); + tracing::debug!("Added exposed mapping {mapping} to toml document"); Ok(()) } - pub fn remove_exposed_binary( + pub fn remove_exposed_name( &mut self, env_name: &EnvironmentName, - exposed_name: &ExposedKey, + exposed_name: &ExposedName, ) -> miette::Result<()> { self.document .get_or_insert_nested_table(&format!("envs.{env_name}.exposed"))? - .remove(exposed_name.as_str()); + .remove(&exposed_name.to_string()) + .ok_or_else(|| miette::miette!("The exposed name {exposed_name} doesn't exist"))?; tracing::debug!("Removed exposed mapping {exposed_name} from toml document"); Ok(()) } - /// Save the manifest to the file and update the contents - pub fn save(&mut self) -> miette::Result<()> { - self.contents = self.document.to_string(); - std::fs::write(&self.path, self.contents.clone()).into_diagnostic()?; + /// Save the manifest to the file and update the parsed_manifest + pub async fn save(&mut self) -> miette::Result<()> { + let contents = self.document.to_string(); + self.parsed = ParsedManifest::from_toml_str(&contents)?; + tokio::fs::write(&self.path, contents) + .await + .into_diagnostic()?; Ok(()) } } + +#[derive(Debug, Clone)] +pub struct Mapping { + exposed_name: ExposedName, + executable_name: String, +} + +impl Mapping { + pub fn new(exposed_name: ExposedName, executable_name: String) -> Self { + Self { + exposed_name, + executable_name, + } + } +} + +impl fmt::Display for Mapping { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}={}", self.exposed_name, self.executable_name) + } +} diff --git a/src/global/project/mod.rs b/src/global/project/mod.rs index cc2a95a5c..3363817d0 100644 --- a/src/global/project/mod.rs +++ b/src/global/project/mod.rs @@ -7,10 +7,10 @@ use std::{ pub(crate) use environment::EnvironmentName; use indexmap::IndexMap; -pub(crate) use manifest::Manifest; +pub(crate) use manifest::{Manifest, Mapping}; use miette::{Context, IntoDiagnostic}; use once_cell::sync::Lazy; -pub(crate) use parsed_manifest::ExposedKey; +pub(crate) use parsed_manifest::ExposedName; pub(crate) use parsed_manifest::ParsedEnvironment; use parsed_manifest::ParsedManifest; use pixi_config::{home_path, Config}; @@ -66,7 +66,7 @@ struct ExposedData { platform: Option, channel: PrioritizedChannel, package: PackageName, - exposed: ExposedKey, + exposed: ExposedName, executable_name: String, } @@ -81,7 +81,7 @@ impl ExposedData { .file_stem() .and_then(OsStr::to_str) .ok_or_else(|| miette::miette!("Could not get file stem of {}", path.display())) - .and_then(ExposedKey::from_str)?; + .and_then(ExposedName::from_str)?; let executable_path = extract_executable_from_script(path)?; let executable = executable_path @@ -367,7 +367,7 @@ impl Project { /// Returns the environments in this project. pub(crate) fn environments(&self) -> &IndexMap { - self.manifest.parsed.envs() + &self.manifest.parsed.envs } } diff --git a/src/global/project/parsed_manifest.rs b/src/global/project/parsed_manifest.rs index db1c8d8d9..d7a44b87b 100644 --- a/src/global/project/parsed_manifest.rs +++ b/src/global/project/parsed_manifest.rs @@ -21,7 +21,7 @@ use pixi_spec::PixiSpec; #[derive(Debug, Clone, Serialize)] pub struct ParsedManifest { /// The environments the project can create. - envs: IndexMap, + pub(crate) envs: IndexMap, } impl From for ParsedManifest @@ -57,10 +57,6 @@ impl ParsedManifest { pub(crate) fn from_toml_str(source: &str) -> Result { toml_edit::de::from_str(source).map_err(ManifestError::from) } - - pub(crate) fn envs(&self) -> &IndexMap { - &self.envs - } } impl<'de> serde::Deserialize<'de> for ParsedManifest { @@ -80,17 +76,17 @@ impl<'de> serde::Deserialize<'de> for ParsedManifest { let manifest = TomlManifest::deserialize(deserializer)?; // Check for duplicate keys in the exposed fields - let mut exposed_keys = IndexSet::new(); + let mut exposed_names = IndexSet::new(); let mut duplicates = IndexMap::new(); for key in manifest.envs.values().flat_map(|env| env.exposed.keys()) { - if !exposed_keys.insert(key) { + if !exposed_names.insert(key) { duplicates.entry(key).or_insert_with(Vec::new).push(key); } } if !duplicates.is_empty() { let duplicate_keys = duplicates.keys().map(|k| k.to_string()).collect_vec(); return Err(serde::de::Error::custom(format!( - "Duplicate exposed keys found: '{}'", + "Duplicate exposed names found: '{}'", duplicate_keys.join(", ") ))); } @@ -111,7 +107,8 @@ pub(crate) struct ParsedEnvironment { platform: Option, #[serde(default, deserialize_with = "pixi_manifest::deserialize_package_map")] pub(crate) dependencies: IndexMap, - pub(crate) exposed: IndexMap, + #[serde(default)] + pub(crate) exposed: IndexMap, } impl ParsedEnvironment { @@ -127,37 +124,27 @@ impl ParsedEnvironment { } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] -pub(crate) struct ExposedKey(String); - -impl ExposedKey { - pub fn as_str(&self) -> &str { - &self.0 - } - - // pub fn from_str(value: &str) -> Result { - // value.parse() - // } -} +pub(crate) struct ExposedName(String); -impl fmt::Display for ExposedKey { +impl fmt::Display for ExposedName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } -impl FromStr for ExposedKey { +impl FromStr for ExposedName { type Err = miette::Report; fn from_str(value: &str) -> Result { if value == "pixi" { miette::bail!("The key 'pixi' is not allowed in the exposed map"); } else { - Ok(ExposedKey(value.to_string())) + Ok(ExposedName(value.to_string())) } } } -impl<'de> Deserialize<'de> for ExposedKey { +impl<'de> Deserialize<'de> for ExposedName { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, @@ -165,7 +152,7 @@ impl<'de> Deserialize<'de> for ExposedKey { struct ExposedKeyVisitor; impl<'de> Visitor<'de> for ExposedKeyVisitor { - type Value = ExposedKey; + type Value = ExposedName; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a string that is not 'pixi'") @@ -175,7 +162,7 @@ impl<'de> Deserialize<'de> for ExposedKey { where E: serde::de::Error, { - ExposedKey::from_str(value).map_err(serde::de::Error::custom) + ExposedName::from_str(value).map_err(serde::de::Error::custom) } } @@ -183,7 +170,7 @@ impl<'de> Deserialize<'de> for ExposedKey { } } -/// Represents an error that occurs when parsing an binary exposed key. +/// Represents an error that occurs when parsing an binary exposed name. /// /// This error is returned when a string fails to be parsed as an environment name. #[derive(Debug, Clone, Error, Diagnostic, PartialEq)] diff --git a/src/prefix.rs b/src/prefix.rs index b12bf81a9..b24817695 100644 --- a/src/prefix.rs +++ b/src/prefix.rs @@ -164,6 +164,7 @@ pub(crate) fn is_executable(prefix: &Prefix, relative_path: &Path) -> bool { is_executable::is_executable(absolute_path) } +#[allow(unused)] /// Create the environment activation script pub(crate) fn create_activation_script( prefix: &Prefix, From e5a63dd17601ac82dc7eab1bfa208e42960bc3d5 Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Mon, 23 Sep 2024 17:47:01 +0200 Subject: [PATCH 10/26] Improve code somewhat --- src/cli/global/expose.rs | 141 +++++++++++++++++--------------------- src/cli/global/sync.rs | 6 +- src/global/project/mod.rs | 9 ++- 3 files changed, 70 insertions(+), 86 deletions(-) diff --git a/src/cli/global/expose.rs b/src/cli/global/expose.rs index de6513214..64f83e9ef 100644 --- a/src/cli/global/expose.rs +++ b/src/cli/global/expose.rs @@ -1,6 +1,5 @@ use std::str::FromStr; -use crate::global::{BinDir, EnvRoot}; use clap::Parser; use miette::Context; use pixi_config::{Config, ConfigCli}; @@ -72,94 +71,82 @@ pub async fn execute(args: SubCommand) -> miette::Result<()> { Ok(()) } -pub async fn add(args: AddArgs) -> miette::Result<()> { - let config = Config::with_cli_config(&args.config); - - let bin_dir = BinDir::from_env().await?; - let env_root = EnvRoot::from_env().await?; - - let mut project_original = - global::Project::discover_or_create(env_root, bin_dir, args.assume_yes) - .await? - .with_cli_config(config.clone()); - - // Make sure that the manifest is up-to-date with the local installation - global::sync(&project_original, &config).await?; - - if let Err(err) = apply_add_changes(args, project_original.clone(), &config).await { - let err = err.wrap_err("Could not add mappings"); - project_original - .manifest - .save() - .await - .wrap_err_with(|| format!("{}\nReverting also failed", &err))?; - global::sync(&project_original, &config) - .await - .wrap_err_with(|| format!("{}\nReverting also failed", &err))?; - return Err(err); - } - Ok(()) +async fn setup_project(config: &Config, assume_yes: bool) -> miette::Result { + let project = global::Project::discover_or_create(assume_yes) + .await? + .with_cli_config(config.clone()); + global::sync(&project, config).await?; + Ok(project) } -async fn apply_add_changes( - args: AddArgs, - project_original: global::Project, +async fn revert_after_error( + err: miette::Error, + mut project_original: global::Project, config: &Config, -) -> Result<(), miette::Error> { - let mut project_modified = project_original.clone(); - - for mapping in args.mappings { - project_modified - .manifest - .add_exposed_mapping(&args.environment, &mapping)?; - } - project_modified.manifest.save().await?; - global::sync(&project_modified, config).await?; - Ok(()) + action: &str, +) -> miette::Result<()> { + let err = err.wrap_err(format!("Could not {}.", action)); + project_original + .manifest + .save() + .await + .wrap_err_with(|| format!("{}\nReverting also failed", &err))?; + global::sync(&project_original, config) + .await + .wrap_err_with(|| format!("{}\nReverting also failed", &err))?; + Err(err) } -pub async fn remove(args: RemoveArgs) -> miette::Result<()> { +pub async fn add(args: AddArgs) -> miette::Result<()> { let config = Config::with_cli_config(&args.config); + let project_original = setup_project(&config, args.assume_yes).await?; + + async fn apply_changes( + args: AddArgs, + project_original: global::Project, + config: &Config, + ) -> Result<(), miette::Error> { + let mut project_modified = project_original.clone(); + + for mapping in args.mappings { + project_modified + .manifest + .add_exposed_mapping(&args.environment, &mapping)?; + } + project_modified.manifest.save().await?; + global::sync(&project_modified, config).await?; + Ok(()) + } - let bin_dir = BinDir::from_env().await?; - let env_root = EnvRoot::from_env().await?; - - let mut project_original = - global::Project::discover_or_create(env_root, bin_dir, args.assume_yes) - .await? - .with_cli_config(config.clone()); - - // Make sure that the manifest is up-to-date with the local installation - global::sync(&project_original, &config).await?; - - if let Err(err) = apply_remove_changes(args, project_original.clone(), &config).await { - let err = err.wrap_err("Could not remove exposed names"); - project_original - .manifest - .save() - .await - .wrap_err_with(|| format!("{}\nReverting also failed", &err))?; - global::sync(&project_original, &config) - .await - .wrap_err_with(|| format!("{}\nReverting also failed", &err))?; - return Err(err); + if let Err(err) = apply_changes(args, project_original.clone(), &config).await { + return revert_after_error(err, project_original, &config, "add mappings").await; } Ok(()) } -async fn apply_remove_changes( - args: RemoveArgs, - project_original: global::Project, - config: &Config, -) -> Result<(), miette::Error> { - let mut project_modified = project_original.clone(); +pub async fn remove(args: RemoveArgs) -> miette::Result<()> { + let config = Config::with_cli_config(&args.config); + let project_original = setup_project(&config, args.assume_yes).await?; + + async fn apply_changes( + args: RemoveArgs, + project_original: global::Project, + config: &Config, + ) -> Result<(), miette::Error> { + let mut project_modified = project_original.clone(); + + for exposed_name in args.exposed_names { + project_modified + .manifest + .remove_exposed_name(&args.environment, &exposed_name)?; + } + project_modified.manifest.save().await?; + global::sync(&project_modified, config).await?; + Ok(()) + } - for exposed_name in args.exposed_names { - project_modified - .manifest - .remove_exposed_name(&args.environment, &exposed_name)?; + if let Err(err) = apply_changes(args, project_original.clone(), &config).await { + return revert_after_error(err, project_original, &config, "remove exposed names").await; } - project_modified.manifest.save().await?; - global::sync(&project_modified, config).await?; Ok(()) } diff --git a/src/cli/global/sync.rs b/src/cli/global/sync.rs index edf478028..bf66fa0ee 100644 --- a/src/cli/global/sync.rs +++ b/src/cli/global/sync.rs @@ -1,4 +1,4 @@ -use crate::global::{self, BinDir, EnvRoot}; +use crate::global; use clap::Parser; use pixi_config::{Config, ConfigCli}; @@ -15,10 +15,8 @@ pub struct Args { /// Sync global manifest with installed environments pub async fn execute(args: Args) -> miette::Result<()> { let config = Config::with_cli_config(&args.config); - let bin_dir = BinDir::from_env().await?; - let env_root = EnvRoot::from_env().await?; - let project = global::Project::discover_or_create(env_root, bin_dir, args.assume_yes) + let project = global::Project::discover_or_create(args.assume_yes) .await? .with_cli_config(config.clone()); diff --git a/src/global/project/mod.rs b/src/global/project/mod.rs index 3363817d0..8008d0643 100644 --- a/src/global/project/mod.rs +++ b/src/global/project/mod.rs @@ -262,11 +262,7 @@ impl Project { /// `~/.pixi/manifests/pixi-global.toml`. If the manifest doesn't exist /// yet, and the function will try to create one from the existing /// installation. If that one fails, an empty one will be created. - pub(crate) async fn discover_or_create( - env_root: EnvRoot, - bin_dir: BinDir, - assume_yes: bool, - ) -> miette::Result { + pub(crate) async fn discover_or_create(assume_yes: bool) -> miette::Result { let manifest_dir = Self::manifest_dir()?; tokio::fs::create_dir_all(&manifest_dir) @@ -276,6 +272,9 @@ impl Project { let manifest_path = manifest_dir.join(MANIFEST_DEFAULT_NAME); + let bin_dir = BinDir::from_env().await?; + let env_root = EnvRoot::from_env().await?; + if !manifest_path.exists() { let prompt = format!( "{} You don't have a global manifest yet.\n\ From ffde8ffd0510d994011a92591243b13fbdadbda2 Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Mon, 23 Sep 2024 17:49:22 +0200 Subject: [PATCH 11/26] Adapt docs --- src/cli/global/expose.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/cli/global/expose.rs b/src/cli/global/expose.rs index 64f83e9ef..b95cba464 100644 --- a/src/cli/global/expose.rs +++ b/src/cli/global/expose.rs @@ -8,7 +8,8 @@ use crate::global::{self, EnvironmentName, ExposedName}; #[derive(Parser, Debug)] pub struct AddArgs { - /// The binary to add as executable in the form of key=value (e.g. python=python3.10) + /// Add one or more `MAPPING` for environment `ENV` which describe which executables are exposed. + /// The syntax for `MAPPING` is `exposed_name=executable_name`, so for example `python3.10=python`. #[arg(value_parser = parse_mapping)] mappings: Vec, @@ -39,7 +40,7 @@ fn parse_mapping(input: &str) -> miette::Result { } #[derive(Parser, Debug)] pub struct RemoveArgs { - /// The binary or binaries to remove as executable (e.g. python atuin) + /// The exposed names that should be removed exposed_names: Vec, #[clap(short, long)] From 5a8912075a050d3cf584110cd7fc4ecaaa4e2f6b Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Mon, 23 Sep 2024 18:07:43 +0200 Subject: [PATCH 12/26] Update to latest main --- src/cli/global/sync.rs | 3 ++ src/global/install.rs | 8 +++- ...anifest__tests__duplicate_exposed.snap.new | 6 +++ tests/integration/test_global.py | 43 +++++++++++++++++++ 4 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 src/global/project/snapshots/pixi__global__project__parsed_manifest__tests__duplicate_exposed.snap.new diff --git a/src/cli/global/sync.rs b/src/cli/global/sync.rs index 2a8257237..521a8d889 100644 --- a/src/cli/global/sync.rs +++ b/src/cli/global/sync.rs @@ -15,6 +15,9 @@ pub struct Args { /// Sync global manifest with installed environments pub async fn execute(args: Args) -> miette::Result<()> { let config = Config::with_cli_config(&args.config); + let project = global::Project::discover_or_create(args.assume_yes) + .await? + .with_cli_config(config.clone()); let updated_env = global::sync(&project, &config).await?; if !updated_env { diff --git a/src/global/install.rs b/src/global/install.rs index d1a0d362d..8d00d262c 100644 --- a/src/global/install.rs +++ b/src/global/install.rs @@ -423,7 +423,10 @@ pub(crate) fn prompt_user_to_continue( // Syncs the manifest with the local environment // Returns true if the global installation had to be updated -pub(crate) async fn sync(project: &global::Project, config: &Config) -> Result<(bool), miette::Error> { +pub(crate) async fn sync( + project: &global::Project, + config: &Config, +) -> Result { // Fetch the repodata let (_, auth_client) = build_reqwest_clients(Some(config)); @@ -514,7 +517,8 @@ pub(crate) async fn sync(project: &global::Project, config: &Config) -> Result<( .await?; } - updated_env |= expose_executables(env_name, parsed_environment, &prefix, &project.bin_dir).await?; + updated_env |= + expose_executables(env_name, parsed_environment, &prefix, &project.bin_dir).await?; } Ok(updated_env) diff --git a/src/global/project/snapshots/pixi__global__project__parsed_manifest__tests__duplicate_exposed.snap.new b/src/global/project/snapshots/pixi__global__project__parsed_manifest__tests__duplicate_exposed.snap.new new file mode 100644 index 000000000..b066dea76 --- /dev/null +++ b/src/global/project/snapshots/pixi__global__project__parsed_manifest__tests__duplicate_exposed.snap.new @@ -0,0 +1,6 @@ +--- +source: src/global/project/parsed_manifest.rs +assertion_line: 224 +expression: manifest.unwrap_err() +--- +Duplicate exposed names found: 'python, python3' diff --git a/tests/integration/test_global.py b/tests/integration/test_global.py index a3ba059ab..c235a94b9 100644 --- a/tests/integration/test_global.py +++ b/tests/integration/test_global.py @@ -188,3 +188,46 @@ def test_global_sync_migrate(pixi: Path, tmp_path: Path) -> None: verify_cli_command([pixi, "global", "sync", "--assume-yes"], ExitCode.SUCCESS, env=env) migrated_manifest = tomllib.loads(manifest.read_text()) assert original_manifest == migrated_manifest + + +def test_global_expose(pixi: Path, tmp_path: Path) -> None: + env = {"PIXI_HOME": str(tmp_path)} + manifests = tmp_path.joinpath("manifests") + manifests.mkdir() + manifest = manifests.joinpath("pixi-global.toml") + toml = """ + [envs.test] + channels = ["conda-forge"] + [envs.test.dependencies] + python = "3.12" + """ + manifest.write_text(toml) + exposed_exec = "python1.bat" if platform.system() == "Windows" else "python1" + python1 = tmp_path / "bin" / exposed_exec + + exposed_exec = "python3.bat" if platform.system() == "Windows" else "python3" + python3 = tmp_path / "bin" / exposed_exec + + # Add Python1 + verify_cli_command( + [pixi, "global", "expose", "add", "--environment=test", "python1=python"], + ExitCode.SUCCESS, + env=env, + ) + verify_cli_command([python1, "--version"], ExitCode.SUCCESS, env=env, stdout_contains="3.12") + + # Add Python3 + verify_cli_command( + [pixi, "global", "expose", "add", "--environment=test", "python3=python"], + ExitCode.SUCCESS, + env=env, + ) + verify_cli_command([python3, "--version"], ExitCode.SUCCESS, env=env, stdout_contains="3.12") + + # Remove Python1 + verify_cli_command( + [pixi, "global", "expose", "remove", "--environment=test", "python1"], + ExitCode.SUCCESS, + env=env, + ) + assert not python1.is_file() From 8cd895b1cc8d482849fc6a7f0c9b3ae82a4d9f61 Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Mon, 23 Sep 2024 18:10:22 +0200 Subject: [PATCH 13/26] Update tests --- ..._parsed_manifest__tests__duplicate_exposed.snap | 2 +- ...sed_manifest__tests__duplicate_exposed.snap.new | 6 ------ tests/integration/test_global.py | 14 +++++++++++--- 3 files changed, 12 insertions(+), 10 deletions(-) delete mode 100644 src/global/project/snapshots/pixi__global__project__parsed_manifest__tests__duplicate_exposed.snap.new diff --git a/src/global/project/snapshots/pixi__global__project__parsed_manifest__tests__duplicate_exposed.snap b/src/global/project/snapshots/pixi__global__project__parsed_manifest__tests__duplicate_exposed.snap index d2ebc752f..c73d2bb8a 100644 --- a/src/global/project/snapshots/pixi__global__project__parsed_manifest__tests__duplicate_exposed.snap +++ b/src/global/project/snapshots/pixi__global__project__parsed_manifest__tests__duplicate_exposed.snap @@ -2,4 +2,4 @@ source: src/global/project/parsed_manifest.rs expression: manifest.unwrap_err() --- -Duplicate exposed keys found: 'python, python3' +Duplicate exposed names found: 'python, python3' diff --git a/src/global/project/snapshots/pixi__global__project__parsed_manifest__tests__duplicate_exposed.snap.new b/src/global/project/snapshots/pixi__global__project__parsed_manifest__tests__duplicate_exposed.snap.new deleted file mode 100644 index b066dea76..000000000 --- a/src/global/project/snapshots/pixi__global__project__parsed_manifest__tests__duplicate_exposed.snap.new +++ /dev/null @@ -1,6 +0,0 @@ ---- -source: src/global/project/parsed_manifest.rs -assertion_line: 224 -expression: manifest.unwrap_err() ---- -Duplicate exposed names found: 'python, python3' diff --git a/tests/integration/test_global.py b/tests/integration/test_global.py index c235a94b9..7ebd5bac1 100644 --- a/tests/integration/test_global.py +++ b/tests/integration/test_global.py @@ -208,7 +208,7 @@ def test_global_expose(pixi: Path, tmp_path: Path) -> None: exposed_exec = "python3.bat" if platform.system() == "Windows" else "python3" python3 = tmp_path / "bin" / exposed_exec - # Add Python1 + # Add python1 verify_cli_command( [pixi, "global", "expose", "add", "--environment=test", "python1=python"], ExitCode.SUCCESS, @@ -216,7 +216,7 @@ def test_global_expose(pixi: Path, tmp_path: Path) -> None: ) verify_cli_command([python1, "--version"], ExitCode.SUCCESS, env=env, stdout_contains="3.12") - # Add Python3 + # Add python3 verify_cli_command( [pixi, "global", "expose", "add", "--environment=test", "python3=python"], ExitCode.SUCCESS, @@ -224,10 +224,18 @@ def test_global_expose(pixi: Path, tmp_path: Path) -> None: ) verify_cli_command([python3, "--version"], ExitCode.SUCCESS, env=env, stdout_contains="3.12") - # Remove Python1 + # Remove python1 verify_cli_command( [pixi, "global", "expose", "remove", "--environment=test", "python1"], ExitCode.SUCCESS, env=env, ) assert not python1.is_file() + + # Attempt to remove python2 + verify_cli_command( + [pixi, "global", "expose", "remove", "--environment=test", "python2"], + ExitCode.FAILURE, + env=env, + stderr_contains="The exposed name python2 doesn't exist", + ) From db1d2690c3f0b9ed872e5d1fb5bb16ff21951067 Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Mon, 23 Sep 2024 20:10:58 +0200 Subject: [PATCH 14/26] Remove comments --- src/global/mod.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/global/mod.rs b/src/global/mod.rs index d4da304c8..603cb4b10 100644 --- a/src/global/mod.rs +++ b/src/global/mod.rs @@ -1,12 +1,10 @@ mod common; mod install; mod project; -// mod document; pub(crate) use common::{BinDir, EnvDir, EnvRoot}; pub(crate) use install::sync; pub(crate) use project::{EnvironmentName, ExposedName, Mapping, Project}; -// pub(crate) use document::ManifestSource; use crate::prefix::Prefix; use rattler_conda_types::PrefixRecord; From 3ca202197e28b92e54201226d45129992f51da70 Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Mon, 23 Sep 2024 20:21:36 +0200 Subject: [PATCH 15/26] Update `find_executables` --- src/prefix.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/prefix.rs b/src/prefix.rs index b24817695..1170549f3 100644 --- a/src/prefix.rs +++ b/src/prefix.rs @@ -1,5 +1,6 @@ use std::{ collections::HashMap, + ffi::OsStr, path::{Path, PathBuf}, }; @@ -108,12 +109,6 @@ impl Prefix { /// Processes prefix records (that you can get by using `find_installed_packages`) /// to filter and collect executable files. - /// It performs the following steps: - /// 1. Filters records to only include direct dependencies - /// 2. Finds executables for each filtered record. - /// 3. Maps executables to a tuple of file name (as a string) and file path. - /// 4. Filters tuples to include only those whose names are in the `exposed` values. - /// 5. Collects the resulting tuples into a vector of executables. pub fn find_executables(&self, prefix_packages: &[PrefixRecord]) -> Vec<(String, PathBuf)> { prefix_packages .iter() @@ -123,8 +118,8 @@ impl Prefix { .iter() .filter(|relative_path| is_executable(self, relative_path)) .filter_map(|path| { - path.file_name() - .and_then(|name| name.to_str()) + path.file_stem() + .and_then(OsStr::to_str) .map(|name| (name.to_string(), path.clone())) }) }) From ea3b905d6ae57ef7ba43310165512748726f48e7 Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Tue, 24 Sep 2024 08:50:05 +0200 Subject: [PATCH 16/26] Simplify expose a bit --- src/cli/global/expose.rs | 45 +++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/src/cli/global/expose.rs b/src/cli/global/expose.rs index b95cba464..fcc112f76 100644 --- a/src/cli/global/expose.rs +++ b/src/cli/global/expose.rs @@ -72,42 +72,27 @@ pub async fn execute(args: SubCommand) -> miette::Result<()> { Ok(()) } -async fn setup_project(config: &Config, assume_yes: bool) -> miette::Result { - let project = global::Project::discover_or_create(assume_yes) - .await? - .with_cli_config(config.clone()); - global::sync(&project, config).await?; - Ok(project) -} - async fn revert_after_error( - err: miette::Error, mut project_original: global::Project, config: &Config, - action: &str, ) -> miette::Result<()> { - let err = err.wrap_err(format!("Could not {}.", action)); - project_original - .manifest - .save() - .await - .wrap_err_with(|| format!("{}\nReverting also failed", &err))?; - global::sync(&project_original, config) - .await - .wrap_err_with(|| format!("{}\nReverting also failed", &err))?; - Err(err) + project_original.manifest.save().await?; + global::sync(&project_original, config).await?; + Ok(()) } pub async fn add(args: AddArgs) -> miette::Result<()> { let config = Config::with_cli_config(&args.config); - let project_original = setup_project(&config, args.assume_yes).await?; + let project_original = global::Project::discover_or_create(args.assume_yes) + .await? + .with_cli_config(config.clone()); async fn apply_changes( args: AddArgs, project_original: global::Project, config: &Config, ) -> Result<(), miette::Error> { - let mut project_modified = project_original.clone(); + let mut project_modified = project_original; for mapping in args.mappings { project_modified @@ -120,21 +105,26 @@ pub async fn add(args: AddArgs) -> miette::Result<()> { } if let Err(err) = apply_changes(args, project_original.clone(), &config).await { - return revert_after_error(err, project_original, &config, "add mappings").await; + revert_after_error(project_original, &config) + .await + .wrap_err_with(|| format!("Reverting of the following error failed:\n {err}"))?; + return Err(err); } Ok(()) } pub async fn remove(args: RemoveArgs) -> miette::Result<()> { let config = Config::with_cli_config(&args.config); - let project_original = setup_project(&config, args.assume_yes).await?; + let project_original = global::Project::discover_or_create(args.assume_yes) + .await? + .with_cli_config(config.clone()); async fn apply_changes( args: RemoveArgs, project_original: global::Project, config: &Config, ) -> Result<(), miette::Error> { - let mut project_modified = project_original.clone(); + let mut project_modified = project_original; for exposed_name in args.exposed_names { project_modified @@ -147,7 +137,10 @@ pub async fn remove(args: RemoveArgs) -> miette::Result<()> { } if let Err(err) = apply_changes(args, project_original.clone(), &config).await { - return revert_after_error(err, project_original, &config, "remove exposed names").await; + revert_after_error(project_original, &config) + .await + .wrap_err_with(|| format!("Reverting of the following error failed:\n {err}"))?; + return Err(err); } Ok(()) } From 6c46e6077da9974cc5e7f828a8db103f612c62f4 Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Tue, 24 Sep 2024 09:44:27 +0200 Subject: [PATCH 17/26] Add tests based on dummy channel --- src/cli/global/expose.rs | 4 +- src/global/common.rs | 4 +- src/global/project/mod.rs | 13 +- tests/integration/common.py | 1 - .../noarch/dummy-a-0.1.0-h4616a5c_0.conda | Bin 0 -> 11520 bytes .../noarch/dummy-b-0.1.0-h4616a5c_0.conda | Bin 0 -> 7632 bytes .../output/noarch/repodata.json | 35 ++++++ .../test_data/dummy_channel_a/recipe.yaml | 27 +++++ tests/integration/test_global.py | 111 +++++++++++++----- 9 files changed, 156 insertions(+), 39 deletions(-) create mode 100644 tests/integration/test_data/dummy_channel_a/output/noarch/dummy-a-0.1.0-h4616a5c_0.conda create mode 100644 tests/integration/test_data/dummy_channel_a/output/noarch/dummy-b-0.1.0-h4616a5c_0.conda create mode 100644 tests/integration/test_data/dummy_channel_a/output/noarch/repodata.json create mode 100644 tests/integration/test_data/dummy_channel_a/recipe.yaml diff --git a/src/cli/global/expose.rs b/src/cli/global/expose.rs index fcc112f76..4062cd08c 100644 --- a/src/cli/global/expose.rs +++ b/src/cli/global/expose.rs @@ -107,7 +107,7 @@ pub async fn add(args: AddArgs) -> miette::Result<()> { if let Err(err) = apply_changes(args, project_original.clone(), &config).await { revert_after_error(project_original, &config) .await - .wrap_err_with(|| format!("Reverting of the following error failed:\n {err}"))?; + .wrap_err("Could not add exposed mappings. Reverting also failed.")?; return Err(err); } Ok(()) @@ -139,7 +139,7 @@ pub async fn remove(args: RemoveArgs) -> miette::Result<()> { if let Err(err) = apply_changes(args, project_original.clone(), &config).await { revert_after_error(project_original, &config) .await - .wrap_err_with(|| format!("Reverting of the following error failed:\n {err}"))?; + .wrap_err("Could not remove exposed name. Reverting also failed.")?; return Err(err); } Ok(()) diff --git a/src/global/common.rs b/src/global/common.rs index 64d97dcf4..16beae270 100644 --- a/src/global/common.rs +++ b/src/global/common.rs @@ -118,7 +118,7 @@ impl EnvRoot { tokio::fs::create_dir_all(&path) .await .into_diagnostic() - .wrap_err_with(|| format!("Couldn't create directory {}", path.display()))?; + .wrap_err_with(|| format!("Could not create directory {}", path.display()))?; Ok(Self(path)) } @@ -130,7 +130,7 @@ impl EnvRoot { tokio::fs::create_dir_all(&path) .await .into_diagnostic() - .wrap_err_with(|| format!("Couldn't create directory {}", path.display()))?; + .wrap_err_with(|| format!("Could not create directory {}", path.display()))?; Ok(Self(path)) } diff --git a/src/global/project/mod.rs b/src/global/project/mod.rs index 8008d0643..a496a3fbd 100644 --- a/src/global/project/mod.rs +++ b/src/global/project/mod.rs @@ -164,7 +164,7 @@ fn determine_env_path(executable_path: &Path, env_root: &Path) -> miette::Result } miette::bail!( - "Couldn't determine environment path: no parent of '{}' has '{}' as its direct parent", + "Could not determine environment path: no parent of '{}' has '{}' as its direct parent", executable_path.display(), env_root.display() ) @@ -183,14 +183,17 @@ async fn package_from_conda_meta( let read_dir = tokio::fs::read_dir(conda_meta) .await .into_diagnostic() - .wrap_err_with(|| format!("Couldn't read directory {}", conda_meta.display()))?; + .wrap_err_with(|| format!("Could not read directory {}", conda_meta.display()))?; let mut entries = ReadDirStream::new(read_dir); while let Some(entry) = entries.next().await { let path = entry .into_diagnostic() .wrap_err_with(|| { - format!("Couldn't read file from directory {}", conda_meta.display()) + format!( + "Could not read file from directory {}", + conda_meta.display() + ) })? .path(); // Check if the entry is a file and has a .json extension @@ -268,7 +271,7 @@ impl Project { tokio::fs::create_dir_all(&manifest_dir) .await .into_diagnostic() - .wrap_err_with(|| format!("Couldn't create directory {}", manifest_dir.display()))?; + .wrap_err_with(|| format!("Could not create directory {}", manifest_dir.display()))?; let manifest_path = manifest_dir.join(MANIFEST_DEFAULT_NAME); @@ -301,7 +304,7 @@ impl Project { tokio::fs::File::create(&manifest_path) .await .into_diagnostic() - .wrap_err_with(|| format!("Couldn't create file {}", manifest_path.display()))?; + .wrap_err_with(|| format!("Could not create file {}", manifest_path.display()))?; } Self::from_path(&manifest_path, env_root, bin_dir) diff --git a/tests/integration/common.py b/tests/integration/common.py index 31d00b3e4..8351246fe 100644 --- a/tests/integration/common.py +++ b/tests/integration/common.py @@ -10,7 +10,6 @@ class ExitCode(IntEnum): SUCCESS = 0 FAILURE = 1 INCORRECT_USAGE = 2 - LIFE = 42 def verify_cli_command( diff --git a/tests/integration/test_data/dummy_channel_a/output/noarch/dummy-a-0.1.0-h4616a5c_0.conda b/tests/integration/test_data/dummy_channel_a/output/noarch/dummy-a-0.1.0-h4616a5c_0.conda new file mode 100644 index 0000000000000000000000000000000000000000..753ef763c6dcbbe3d6d2d2652752e41dd51f07f3 GIT binary patch literal 11520 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 j_lp1SKK`GI+c)8V $PREFIX/bin/dummy-a.com + - chmod +x $PREFIX/bin/dummy-a.com + + - package: + name: dummy-b + version: 0.1.0 + + build: + noarch: generic + script: + - mkdir -p $PREFIX/bin + - echo "clobber-1" > $PREFIX/bin/dummy-b.com + - chmod +x $PREFIX/bin/dummy-b.com diff --git a/tests/integration/test_global.py b/tests/integration/test_global.py index 7ebd5bac1..348a9e968 100644 --- a/tests/integration/test_global.py +++ b/tests/integration/test_global.py @@ -5,6 +5,13 @@ import platform +def exe_extension(exe_name: str) -> str: + if platform.system() == "Windows": + return exe_name + ".bat" + else: + return exe_name + + def test_global_sync_dependencies(pixi: Path, tmp_path: Path) -> None: env = {"PIXI_HOME": str(tmp_path)} manifests = tmp_path.joinpath("manifests") @@ -21,8 +28,7 @@ def test_global_sync_dependencies(pixi: Path, tmp_path: Path) -> None: """ parsed_toml = tomllib.loads(toml) manifest.write_text(toml) - exposed_exec = "python-injected.bat" if platform.system() == "Windows" else "python-injected" - python_injected = tmp_path / "bin" / exposed_exec + python_injected = tmp_path / "bin" / exe_extension("python-injected") # Test basic commands verify_cli_command([pixi, "global", "sync"], ExitCode.SUCCESS, env=env) @@ -99,8 +105,7 @@ def test_global_sync_change_expose(pixi: Path, tmp_path: Path) -> None: """ parsed_toml = tomllib.loads(toml) manifest.write_text(toml) - exposed_exec = "python-injected.bat" if platform.system() == "Windows" else "python-injected" - python_injected = tmp_path / "bin" / exposed_exec + python_injected = tmp_path / "bin" / exe_extension("python-injected") # Test basic commands verify_cli_command([pixi, "global", "sync"], ExitCode.SUCCESS, env=env) @@ -110,9 +115,7 @@ def test_global_sync_change_expose(pixi: Path, tmp_path: Path) -> None: verify_cli_command([python_injected], ExitCode.SUCCESS, env=env) # Add another expose - python_in_disguise_str = ( - "python-in-disguise.bat" if platform.system() == "Windows" else "python-in-disguise" - ) + python_in_disguise_str = exe_extension("python-in-disguise") python_in_disguise = tmp_path / "bin" / python_in_disguise_str parsed_toml["envs"]["test"]["exposed"][python_in_disguise_str] = "python" manifest.write_text(tomli_w.dumps(parsed_toml)) @@ -141,8 +144,7 @@ def test_global_sync_manually_remove_binary(pixi: Path, tmp_path: Path) -> None: "python-injected" = "python" """ manifest.write_text(toml) - exposed_exec = "python-injected.bat" if platform.system() == "Windows" else "python-injected" - python_injected = tmp_path / "bin" / exposed_exec + python_injected = tmp_path / "bin" / exe_extension("python-injected") # Test basic commands verify_cli_command([pixi, "global", "sync"], ExitCode.SUCCESS, env=env) @@ -190,52 +192,103 @@ def test_global_sync_migrate(pixi: Path, tmp_path: Path) -> None: assert original_manifest == migrated_manifest -def test_global_expose(pixi: Path, tmp_path: Path) -> None: +def test_global_expose_basic(pixi: Path, tmp_path: Path, test_data: Path) -> None: env = {"PIXI_HOME": str(tmp_path)} manifests = tmp_path.joinpath("manifests") manifests.mkdir() manifest = manifests.joinpath("pixi-global.toml") - toml = """ + dummy_channel = test_data.joinpath("dummy_channel_a/output").as_uri() + toml = f""" [envs.test] - channels = ["conda-forge"] + channels = ["{dummy_channel}"] [envs.test.dependencies] - python = "3.12" + dummy-a = "*" """ manifest.write_text(toml) - exposed_exec = "python1.bat" if platform.system() == "Windows" else "python1" - python1 = tmp_path / "bin" / exposed_exec - - exposed_exec = "python3.bat" if platform.system() == "Windows" else "python3" - python3 = tmp_path / "bin" / exposed_exec + dummy1 = tmp_path / "bin" / exe_extension("dummy1") + dummy3 = tmp_path / "bin" / exe_extension("dummy3") - # Add python1 + # Add dummy1 verify_cli_command( - [pixi, "global", "expose", "add", "--environment=test", "python1=python"], + [pixi, "global", "expose", "add", "--environment=test", "dummy1=dummy-a"], ExitCode.SUCCESS, env=env, ) - verify_cli_command([python1, "--version"], ExitCode.SUCCESS, env=env, stdout_contains="3.12") + assert dummy1.is_file() - # Add python3 + # Add dummy3 verify_cli_command( - [pixi, "global", "expose", "add", "--environment=test", "python3=python"], + [pixi, "global", "expose", "add", "--environment=test", "dummy3=dummy-a"], ExitCode.SUCCESS, env=env, ) - verify_cli_command([python3, "--version"], ExitCode.SUCCESS, env=env, stdout_contains="3.12") + assert dummy3.is_file() - # Remove python1 + # Remove dummy1 verify_cli_command( - [pixi, "global", "expose", "remove", "--environment=test", "python1"], + [pixi, "global", "expose", "remove", "--environment=test", "dummy1"], ExitCode.SUCCESS, env=env, ) - assert not python1.is_file() + assert not dummy1.is_file() # Attempt to remove python2 verify_cli_command( - [pixi, "global", "expose", "remove", "--environment=test", "python2"], + [pixi, "global", "expose", "remove", "--environment=test", "dummy2"], + ExitCode.FAILURE, + env=env, + stderr_contains="The exposed name dummy2 doesn't exist", + ) + + +def test_global_expose_revert_working(pixi: Path, tmp_path: Path, test_data: Path) -> None: + env = {"PIXI_HOME": str(tmp_path)} + manifests = tmp_path.joinpath("manifests") + manifests.mkdir() + manifest = manifests.joinpath("pixi-global.toml") + dummy_channel = test_data.joinpath("dummy_channel_a/output").as_uri() + original_toml = f""" + [envs.test] + channels = ["{dummy_channel}"] + [envs.test.dependencies] + dummy-a = "*" + """ + manifest.write_text(original_toml) + + # Attempt to add executable dummy-b that is not in our dependencies + verify_cli_command( + [pixi, "global", "expose", "add", "--environment=test", "dummy-b=dummy-b"], + ExitCode.FAILURE, + env=env, + stderr_contains="Could not find dummy-b in test", + ) + + # The TOML has been reverted to the original state + assert original_toml == manifest.read_text() + + +def test_global_expose_revert_failure(pixi: Path, tmp_path: Path, test_data: Path) -> None: + env = {"PIXI_HOME": str(tmp_path)} + manifests = tmp_path.joinpath("manifests") + manifests.mkdir() + manifest = manifests.joinpath("pixi-global.toml") + dummy_channel = test_data.joinpath("dummy_channel_a/output").as_uri() + original_toml = f""" + [envs.test] + channels = ["{dummy_channel}"] + [envs.test.dependencies] + dummy-a = "*" + [envs.test.exposed] + dummy1 = "dummy-b" + """ + manifest.write_text(original_toml) + + # Attempt to add executable dummy-b that isn't in our dependencies + # It should fail since the original manifest contains "dummy-b", + # which is not in our dependencies + verify_cli_command( + [pixi, "global", "expose", "add", "--environment=test", "dummy2=dummyb"], ExitCode.FAILURE, env=env, - stderr_contains="The exposed name python2 doesn't exist", + stderr_contains="Could not add exposed mappings. Reverting also failed", ) From e8b00d142bc60b4c0008b1fc9b49ca55f948f2c3 Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Tue, 24 Sep 2024 10:00:12 +0200 Subject: [PATCH 18/26] Simplify errors --- crates/pixi_manifest/src/error.rs | 3 -- crates/pixi_manifest/src/lib.rs | 1 + src/global/project/error.rs | 63 --------------------------- src/global/project/manifest.rs | 12 +---- src/global/project/mod.rs | 1 - src/global/project/parsed_manifest.rs | 7 ++- 6 files changed, 6 insertions(+), 81 deletions(-) delete mode 100644 src/global/project/error.rs diff --git a/crates/pixi_manifest/src/error.rs b/crates/pixi_manifest/src/error.rs index fe42d8f84..d169fb46c 100644 --- a/crates/pixi_manifest/src/error.rs +++ b/crates/pixi_manifest/src/error.rs @@ -1,6 +1,3 @@ -// TODO: remove -#![allow(dead_code)] - use std::{borrow::Borrow, fmt::Display}; use itertools::Itertools; diff --git a/crates/pixi_manifest/src/lib.rs b/crates/pixi_manifest/src/lib.rs index 0ade5f397..326de2259 100644 --- a/crates/pixi_manifest/src/lib.rs +++ b/crates/pixi_manifest/src/lib.rs @@ -32,6 +32,7 @@ pub use crate::solve_group::{SolveGroup, SolveGroups}; pub use activation::Activation; pub use channel::{PrioritizedChannel, TomlPrioritizedChannelStrOrMap}; pub use environment::{Environment, EnvironmentName}; +pub use error::TomlError; pub use feature::{Feature, FeatureName}; use itertools::Itertools; pub use metadata::ProjectMetadata; diff --git a/src/global/project/error.rs b/src/global/project/error.rs deleted file mode 100644 index 1b28d1108..000000000 --- a/src/global/project/error.rs +++ /dev/null @@ -1,63 +0,0 @@ -use miette::{Diagnostic, IntoDiagnostic, LabeledSpan, NamedSource, Report}; - -use thiserror::Error; - -/// Represents errors that can occur when working with a pixi global manifest -#[derive(Error, Debug, Clone, Diagnostic)] -pub enum ManifestError { - #[error(transparent)] - Error(#[from] toml_edit::TomlError), - #[error("Could not find or access the part '{part}' in the path '[{table_name}]'")] - TableError { part: String, table_name: String }, - #[error("Could not find or access array '{array_name}' in '[{table_name}]'")] - ArrayError { - array_name: String, - table_name: String, - }, -} - -impl ManifestError { - pub fn to_fancy(&self, file_name: &str, contents: impl Into) -> Result { - if let Some(span) = self.span() { - Err(miette::miette!( - labels = vec![LabeledSpan::at(span, self.message())], - "failed to parse project manifest" - ) - .with_source_code(NamedSource::new(file_name, contents.into()))) - } else { - Err(self.clone()).into_diagnostic() - } - } - - fn span(&self) -> Option> { - match self { - ManifestError::Error(e) => e.span(), - _ => None, - } - } - fn message(&self) -> String { - match self { - ManifestError::Error(e) => e.message().to_owned(), - _ => self.to_string(), - } - } - - pub fn table_error(part: &str, table_name: &str) -> Self { - Self::TableError { - part: part.into(), - table_name: table_name.into(), - } - } - - pub fn array_error(array_name: &str, table_name: &str) -> Self { - Self::ArrayError { - array_name: array_name.into(), - table_name: table_name.into(), - } - } -} -impl From for ManifestError { - fn from(e: toml_edit::de::Error) -> Self { - ManifestError::Error(e.into()) - } -} diff --git a/src/global/project/manifest.rs b/src/global/project/manifest.rs index a25b74a40..f01971394 100644 --- a/src/global/project/manifest.rs +++ b/src/global/project/manifest.rs @@ -2,19 +2,11 @@ use std::fmt; use std::path::{Path, PathBuf}; use miette::IntoDiagnostic; -use pixi_manifest::TomlManifest; +use pixi_manifest::{TomlError, TomlManifest}; use toml_edit::{DocumentMut, Item}; -// use crate::global::document::ManifestSource; - -use super::error::ManifestError; - use super::parsed_manifest::ParsedManifest; use super::{EnvironmentName, ExposedName, MANIFEST_DEFAULT_NAME}; -// use super::document::ManifestSource; - -// TODO: remove -#[allow(unused)] /// Handles the global project's manifest file. /// This struct is responsible for reading, parsing, editing, and saving the @@ -50,7 +42,7 @@ impl Manifest { contents .parse::() .map(|doc| (manifest, doc)) - .map_err(ManifestError::from) + .map_err(TomlError::from) }) { Ok(result) => result, Err(e) => e.to_fancy(MANIFEST_DEFAULT_NAME, &contents)?, diff --git a/src/global/project/mod.rs b/src/global/project/mod.rs index a496a3fbd..b6d67bcdf 100644 --- a/src/global/project/mod.rs +++ b/src/global/project/mod.rs @@ -26,7 +26,6 @@ use crate::{ }; mod environment; -mod error; mod manifest; mod parsed_manifest; diff --git a/src/global/project/parsed_manifest.rs b/src/global/project/parsed_manifest.rs index d7a44b87b..bfe5b0de7 100644 --- a/src/global/project/parsed_manifest.rs +++ b/src/global/project/parsed_manifest.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use indexmap::{IndexMap, IndexSet}; use itertools::Itertools; use miette::Diagnostic; -use pixi_manifest::PrioritizedChannel; +use pixi_manifest::{PrioritizedChannel, TomlError}; use rattler_conda_types::{NamedChannelOrUrl, PackageName, Platform}; use serde::de::{Deserialize, Deserializer, Visitor}; use serde::Serialize; @@ -13,7 +13,6 @@ use thiserror::Error; use super::environment::EnvironmentName; -use super::error::ManifestError; use super::ExposedData; use pixi_spec::PixiSpec; @@ -54,8 +53,8 @@ where impl ParsedManifest { /// Parses a toml string into a project manifest. - pub(crate) fn from_toml_str(source: &str) -> Result { - toml_edit::de::from_str(source).map_err(ManifestError::from) + pub(crate) fn from_toml_str(source: &str) -> Result { + toml_edit::de::from_str(source).map_err(TomlError::from) } } From 3c11674a6dd48ece6b891d97607cf3a157a7ae5f Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Tue, 24 Sep 2024 10:30:09 +0200 Subject: [PATCH 19/26] Add exposed tests --- src/global/project/manifest.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/global/project/manifest.rs b/src/global/project/manifest.rs index f01971394..caf2b4bb8 100644 --- a/src/global/project/manifest.rs +++ b/src/global/project/manifest.rs @@ -118,3 +118,32 @@ impl fmt::Display for Mapping { write!(f, "{}={}", self.exposed_name, self.executable_name) } } + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::*; + + #[test] + fn test_add_exposed_mapping_new_env() { + let mut manifest = Manifest::from_str(&PathBuf::from("pixi-global.toml"), "").unwrap(); + let exposed_name = ExposedName::from_str("test_exposed").unwrap(); + let executable_name = "test_executable".to_string(); + let mapping = Mapping::new(exposed_name.clone(), executable_name); + let env_name = EnvironmentName::from_str("test-env").unwrap(); + let result = manifest.add_exposed_mapping(&env_name, &mapping); + assert!(result.is_ok()); + + let expected_value = "test_executable"; + let actual_value = manifest + .document + .get_or_insert_nested_table(&format!("envs.{}.exposed", env_name)) + .unwrap() + .get(&exposed_name.to_string()) + .unwrap() + .as_str() + .unwrap(); + assert_eq!(expected_value, actual_value); + } +} From a044bf610ea3e037037aaa08bb9ccf1a947bce7b Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Tue, 24 Sep 2024 10:35:13 +0200 Subject: [PATCH 20/26] Add tests --- src/global/project/manifest.rs | 72 ++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/src/global/project/manifest.rs b/src/global/project/manifest.rs index caf2b4bb8..22f9779c9 100644 --- a/src/global/project/manifest.rs +++ b/src/global/project/manifest.rs @@ -146,4 +146,76 @@ mod tests { .unwrap(); assert_eq!(expected_value, actual_value); } + + #[test] + fn test_add_exposed_mapping_existing_env() { + let mut manifest = Manifest::from_str(&PathBuf::from("pixi-global.toml"), "").unwrap(); + let exposed_name1 = ExposedName::from_str("test_exposed1").unwrap(); + let executable_name1 = "test_executable1".to_string(); + let mapping1 = Mapping::new(exposed_name1.clone(), executable_name1); + let env_name = EnvironmentName::from_str("test-env").unwrap(); + manifest.add_exposed_mapping(&env_name, &mapping1).unwrap(); + + let exposed_name2 = ExposedName::from_str("test_exposed2").unwrap(); + let executable_name2 = "test_executable2".to_string(); + let mapping2 = Mapping::new(exposed_name2.clone(), executable_name2); + let result = manifest.add_exposed_mapping(&env_name, &mapping2); + assert!(result.is_ok()); + + let expected_value1 = "test_executable1"; + let actual_value1 = manifest + .document + .get_or_insert_nested_table(&format!("envs.{}.exposed", env_name)) + .unwrap() + .get(&exposed_name1.to_string()) + .unwrap() + .as_str() + .unwrap(); + assert_eq!(expected_value1, actual_value1); + + let expected_value2 = "test_executable2"; + let actual_value2 = manifest + .document + .get_or_insert_nested_table(&format!("envs.{}.exposed", env_name)) + .unwrap() + .get(&exposed_name2.to_string()) + .unwrap() + .as_str() + .unwrap(); + assert_eq!(expected_value2, actual_value2); + } + + #[test] + fn test_remove_exposed_mapping() { + let mut manifest = Manifest::from_str(&PathBuf::from("pixi-global.toml"), "").unwrap(); + let exposed_name = ExposedName::from_str("test_exposed").unwrap(); + let executable_name = "test_executable".to_string(); + let mapping = Mapping::new(exposed_name.clone(), executable_name); + let env_name = EnvironmentName::from_str("test-env").unwrap(); + + // Add and remove mapping again + manifest.add_exposed_mapping(&env_name, &mapping).unwrap(); + manifest + .remove_exposed_name(&env_name, &exposed_name) + .unwrap(); + + let actual_value = manifest + .document + .get_or_insert_nested_table(&format!("envs.{env_name}.exposed")) + .unwrap() + .get(&exposed_name.to_string()); + assert!(actual_value.is_none()); + } + + #[test] + fn test_remove_exposed_mapping_nonexistent() { + let mut manifest = Manifest::from_str(&PathBuf::from("pixi-global.toml"), "").unwrap(); + let exposed_name = ExposedName::from_str("test_exposed").unwrap(); + let env_name = EnvironmentName::from_str("test-env").unwrap(); + + // Removing an exposed name that doesn't exist should return an error + manifest + .remove_exposed_name(&env_name, &exposed_name) + .unwrap_err(); + } } From aa452a0d7ce51df3664075f97945d40c6f43ea35 Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Tue, 24 Sep 2024 10:46:55 +0200 Subject: [PATCH 21/26] Convert more tests to dummy channel --- .../noarch/dummy-a-0.1.0-h4616a5c_0.conda | Bin 11520 -> 2834 bytes .../noarch/dummy-b-0.1.0-h4616a5c_0.conda | Bin 7632 -> 5807 bytes .../output/noarch/repodata.json | 16 ++-- .../test_data/dummy_channel_a/recipe.yaml | 4 +- tests/integration/test_global.py | 73 ++++++++---------- 5 files changed, 44 insertions(+), 49 deletions(-) diff --git a/tests/integration/test_data/dummy_channel_a/output/noarch/dummy-a-0.1.0-h4616a5c_0.conda b/tests/integration/test_data/dummy_channel_a/output/noarch/dummy-a-0.1.0-h4616a5c_0.conda index 753ef763c6dcbbe3d6d2d2652752e41dd51f07f3..3a1003351b3c9704ecaa0525c5662fbe58d2cef7 100644 GIT binary patch delta 2555 zcmV@3IG5A006;7kqk!-!9_S(lUAU-UH||9UXfrjf29HdvO^6L zkk0|!+NnU--C;h+Dhk!}9%T$Tgt;oUd^YB#e=glgoukree*b5L)EUY)mbegFN8)1v z3IO!EkN?yS5>Nm(2q=*Fz)i@3IG5A006;7I9VMTL}~B{008g^ z001Qb003!jW^XNIb!}~VlM?|Se_#MpHAeu7SO^g+uxyGq2vLhxn|f3aPatAXtE0_3 z0)mY5(&}rryL-)!wCQZ1py8L8QYJ5AxcyrrAtQlAyF}g6a|gdC`k(H zfhGk-PL(xD3QM}9MTVpmM%7b=Unvw772T3gGB(G3AARU{0CQ@waWW1Wf6G+UB=NFe zlzAGcd9ucQKLt`$QAJr5WJMN)p11Qqao>0hQ!e8D`6bLvJe+4b{(19LFJ^yurr8c) z9xGC}f~u)x>cxyEPD87jqRNjMGo`q%&P=_Sb8E6+6Se&%`*x;!B1wACjBUwpiI}iB z)VeUvlL^VzH{a%tS&NQye{r<;lBr9BdNJJmoNp;IwJs76weH(_ig*wc1x3*FAR-8= ztZA~U2|`d0^gM`Zsvbnv+(FY*9bQM183$#5Ks(O?F5nLrf_cGD18oDgaD14z-*vty zNyd3!2p1#~fS$$Hdq1=IARgysHp>{UwYT5VW{vyZlJ6Niyz}o4l)x2?I+Pz16-@p{ zoiHssmN?%h??CT|lP@+?2hN|mboQk<%mT78EQ8x33kx$y2jfirTzhyy)FmbR?L}4iK}9PJ28s`*9l>e*wP*j}GfPFWbNm+ZYNo zMz=A%Oam7zXy67(0CpY&L8n=ei(n0n5p|KsgIP30IxyljK%ni>;AQsvJV@p}G|Py1 z9|DC$1B^5vW`D#mjL~@uhHis^A2)Af12N9~HtTg$#Vi~dM8Ve3APbR?<1m|X&6{Js zVuG99IUjY#fA8Ymz4A`y>F$XuI(ULQVt;OHSP-IRmiscu2W}rk`yyWVCMJJ!8)^mT z2#7(nPGhj+_x$IZ=!tFDxnUNd0?jULoa56PJO{J*7%;A(9l#p04w58Ej}<+QLhEkN z@BD#hhXy&_rYYth^P~R}uz52k7s`Cfo0iw>DY*CNe>O}RL=mMxQIMi)qUWlnOW^^h zD4}=~{7VTguYHQCyLdfxyfwaTz4+s;yZE)_H(y+@bm1GP4}LH8l4QI)xA*)~4xD=?m z6n_a&f6+wUc$}Qen6*lx_UB7S@fJD%UAHFdkNT{VH2ksN{1w20$)&!yQ_b_2y%KP! z6Dk5E1gH@bwLvBE@@9D6-)JrQ4J)Z{|0)0gi&RxoRPx4tT7lZA!JSfuW<*3%kfe|f zVGv>H(s7EYLw1Oc6v3GK3I92q6MOe*{DzL_|a+5D_VA2LkACtM{*>T$g`i z15DM1*8p1n0pfLBBrl$hq{~tXmjl;0HX!M6K_FWkJLgf{L#ZtTVrw(XS<3>Y}L1sG=`3o6UEj6msQJ z5Iw@CV_y;C1b)4J&hrP0Ldc(Tk@N5Xe^p9_vYHnLv;k<>gGPsaTFPsimXi@VcSh%0 z6^In{oi6XP(@#voW;YzAe>xQKDvV<_3$WdRgOI*B;9Xk~z; zIDRbH*#kI|Ifsq#vsN1PHZPupd+D9kp#(f9wP8knf{`KStvrSX@z*$zXz}*;e+Gkx zJ=TwjMkOF-*nK15TXe5>B>eWBdonSSldJ#1qpp8Wf9=}=^pNR|buuOrK&~!h2iLtg z`(m%JM7ibNHk$qdfyhT2a6}&JBbK{^6f8N<9Li1ke^6c4pZ^_*uTVR`{t<0n1Tj_#;aA@Ic*lSP)L;!@ zUW;)|1i{n@Sw!kH$JLTUgZh{Ox7wMf_>&^ZtEqrcgys&4Eh?prRSzA(u5^&|f0y9F zu;RsujB*nthBfjncih4BkP82q$|~e?jaPV8j4jF(ARN#+!uq5~-C=ZV@U((IcmMd#O*}t*J z?O^FneS~_uL_6h7Wju|XAwa2c#sn;_%j8@t)rLn$DqoZY|DnpXAf)s(z+DE9n#WOo z2g4Y_ade&Z3<|IsRkW&8f4m|eO6E5AhDDouB@0T%0yOtl=zCN$`xzTqcx(P=Bh@V1 z+H3(;TO#`m99G)XuTXIeqF4w|)A4E5Zn{m6I>-JsZ1cv?Lb3cfj8*3tjt2w-!QwZ+ zPe!N_-N*lh6~QhUT_6B|2Q*L~Gy70%F_9kQq=riM(y~5lZ6VIKe+u3W+EpHaF42){ z!`OiMT@=ItNakTKf<#ueKDc`%_1Q45)t? z;JJuG3i(i&PHgYn0W{$y;{1oJ2cKfk0ZAe-gs&N78i3jX7|dFDE&RW?B+zXD5;uZi z>j=X7{2L6m$?e67f2-Rnprc2O=W7veAk3|iOJIrb{F|#4V73Z?M-3=2DcJz{@V3PT z4xpN|$j~d8Z4cI-`&rdjtQUKgv>|Nh8~7Ztedf|U+e3!yI{--%0iU~FmX2ljMM>jA zKw^MA;#+XAN3#+f7yg|j1qTlAD7)r>K-?Lg3s6Kqm;z8s3;_ZP0}22D0002NMUxmT zISs)@I9ZccpuAoH003T-UMxi)!9_S(9T`Mv@CX0^@CX0^B>(^b0000000000q=D&^ R1_3CO>MRfj90~vc005A{h?f8W literal 11520 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 literal 7632 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 j_lp1SKK`GI+c)8V $PREFIX/bin/dummy-a.com + - echo "dummy-a" > $PREFIX/bin/dummy-a.com - chmod +x $PREFIX/bin/dummy-a.com - package: @@ -23,5 +23,5 @@ outputs: noarch: generic script: - mkdir -p $PREFIX/bin - - echo "clobber-1" > $PREFIX/bin/dummy-b.com + - echo "dummy-b" > $PREFIX/bin/dummy-b.com - chmod +x $PREFIX/bin/dummy-b.com diff --git a/tests/integration/test_global.py b/tests/integration/test_global.py index 348a9e968..ae81ab62e 100644 --- a/tests/integration/test_global.py +++ b/tests/integration/test_global.py @@ -89,97 +89,92 @@ def test_global_sync_platform(pixi: Path, tmp_path: Path) -> None: ) -def test_global_sync_change_expose(pixi: Path, tmp_path: Path) -> None: +def test_global_sync_change_expose(pixi: Path, tmp_path: Path, test_data: Path) -> None: env = {"PIXI_HOME": str(tmp_path)} manifests = tmp_path.joinpath("manifests") manifests.mkdir() manifest = manifests.joinpath("pixi-global.toml") - toml = """ + dummy_channel = test_data.joinpath("dummy_channel_a/output").as_uri() + toml = f""" [envs.test] - channels = ["conda-forge"] + channels = ["{dummy_channel}"] [envs.test.dependencies] - python = "3.12" + dummy-a = "*" [envs.test.exposed] - "python-injected" = "python" + "dummy-a" = "dummy-a" """ parsed_toml = tomllib.loads(toml) manifest.write_text(toml) - python_injected = tmp_path / "bin" / exe_extension("python-injected") + dummy_a = tmp_path / "bin" / exe_extension("dummy-a") # Test basic commands verify_cli_command([pixi, "global", "sync"], ExitCode.SUCCESS, env=env) - verify_cli_command( - [python_injected, "--version"], ExitCode.SUCCESS, env=env, stdout_contains="3.12" - ) - verify_cli_command([python_injected], ExitCode.SUCCESS, env=env) + assert dummy_a.is_file() # Add another expose - python_in_disguise_str = exe_extension("python-in-disguise") - python_in_disguise = tmp_path / "bin" / python_in_disguise_str - parsed_toml["envs"]["test"]["exposed"][python_in_disguise_str] = "python" + dummy_in_disguise_str = exe_extension("dummy-in-disguise") + dummy_in_disguise = tmp_path / "bin" / dummy_in_disguise_str + parsed_toml["envs"]["test"]["exposed"][dummy_in_disguise_str] = "dummy-a" manifest.write_text(tomli_w.dumps(parsed_toml)) verify_cli_command([pixi, "global", "sync"], ExitCode.SUCCESS, env=env) - verify_cli_command([python_in_disguise, "--version"], ExitCode.SUCCESS, env=env) + assert dummy_in_disguise.is_file() # Remove expose again - del parsed_toml["envs"]["test"]["exposed"][python_in_disguise_str] + del parsed_toml["envs"]["test"]["exposed"][dummy_in_disguise_str] manifest.write_text(tomli_w.dumps(parsed_toml)) verify_cli_command([pixi, "global", "sync"], ExitCode.SUCCESS, env=env) - assert not python_in_disguise.is_file() + assert not dummy_in_disguise.is_file() -def test_global_sync_manually_remove_binary(pixi: Path, tmp_path: Path) -> None: +def test_global_sync_manually_remove_binary(pixi: Path, tmp_path: Path, test_data: Path) -> None: env = {"PIXI_HOME": str(tmp_path)} manifests = tmp_path.joinpath("manifests") manifests.mkdir() manifest = manifests.joinpath("pixi-global.toml") - toml = """ + dummy_channel = test_data.joinpath("dummy_channel_a/output").as_uri() + toml = f""" [envs.test] - channels = ["conda-forge"] + channels = ["{dummy_channel}"] [envs.test.dependencies] - python = "3.12" + dummy-a = "*" [envs.test.exposed] - "python-injected" = "python" + "dummy-a" = "dummy-a" """ manifest.write_text(toml) - python_injected = tmp_path / "bin" / exe_extension("python-injected") + dummy_a = tmp_path / "bin" / exe_extension("dummy-a") # Test basic commands verify_cli_command([pixi, "global", "sync"], ExitCode.SUCCESS, env=env) - verify_cli_command( - [python_injected, "--version"], ExitCode.SUCCESS, env=env, stdout_contains="3.12" - ) - verify_cli_command([python_injected], ExitCode.SUCCESS, env=env) + assert dummy_a.is_file() # Remove binary manually - python_injected.unlink() + dummy_a.unlink() # Binary is added again verify_cli_command([pixi, "global", "sync"], ExitCode.SUCCESS, env=env) - verify_cli_command( - [python_injected, "--version"], ExitCode.SUCCESS, env=env, stdout_contains="3.12" - ) + assert dummy_a.is_file() -def test_global_sync_migrate(pixi: Path, tmp_path: Path) -> None: +def test_global_sync_migrate(pixi: Path, tmp_path: Path, test_data: Path) -> None: env = {"PIXI_HOME": str(tmp_path)} manifests = tmp_path.joinpath("manifests") manifests.mkdir() manifest = manifests.joinpath("pixi-global.toml") - toml = """ + dummy_channel = test_data.joinpath("dummy_channel_a/output").as_uri() + toml = f""" [envs.test] - channels = ["https://conda.anaconda.org/conda-forge"] + channels = ["{dummy_channel}"] [envs.test.dependencies] - ripgrep = "*" - python = "*" + dummy-a = "*" + dummy-b = "*" [envs.test.exposed] - rg = "rg" - grep = "rg" - python = "python" - python3 = "python" + dummy-1 = "dummy-a" + dummy-2 = "dummy-a" + dummy-3 = "dummy-b" + dummy-4 = "dummy-b" """ manifest.write_text(toml) verify_cli_command([pixi, "global", "sync"], ExitCode.SUCCESS, env=env) From f6ce494aa05ea5fb4b9adb0344ddd15827cb7daa Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Tue, 24 Sep 2024 10:53:08 +0200 Subject: [PATCH 22/26] Add docs --- src/global/common.rs | 9 ++--- src/prefix.rs | 91 ++++++++++++++++---------------------------- 2 files changed, 37 insertions(+), 63 deletions(-) diff --git a/src/global/common.rs b/src/global/common.rs index 16beae270..ee24af35b 100644 --- a/src/global/common.rs +++ b/src/global/common.rs @@ -196,11 +196,10 @@ pub(crate) struct EnvDir { impl EnvDir { /// Create a global environment directory based on passed global environment root - pub(crate) async fn from_env_root>( + pub(crate) async fn from_env_root( env_root: EnvRoot, - environment_name: T, + environment_name: EnvironmentName, ) -> miette::Result { - let environment_name = environment_name.into(); let path = env_root.path().join(environment_name.as_str()); tokio::fs::create_dir_all(&path).await.into_diagnostic()?; @@ -249,7 +248,7 @@ mod tests { let env_root = EnvRoot::new(temp_dir.path().to_owned()).await.unwrap(); // Define a test environment name - let environment_name: EnvironmentName = "test-env".parse().unwrap(); + let environment_name = "test-env".parse().unwrap(); // Create a new binary env dir let bin_env_dir = EnvDir::from_env_root(env_root, environment_name) @@ -272,7 +271,7 @@ mod tests { // Create some directories in the temporary directory let envs = ["env1", "env2", "env3"]; for env in &envs { - EnvDir::from_env_root(env_root.clone(), env.parse::().unwrap()) + EnvDir::from_env_root(env_root.clone(), env.parse().unwrap()) .await .unwrap(); } diff --git a/src/prefix.rs b/src/prefix.rs index 1170549f3..771ddf9f4 100644 --- a/src/prefix.rs +++ b/src/prefix.rs @@ -8,7 +8,7 @@ use futures::{stream::FuturesUnordered, StreamExt}; use miette::{Context, IntoDiagnostic}; use rattler_conda_types::{Platform, PrefixRecord}; use rattler_shell::{ - activation::{ActivationVariables, Activator, PathModificationBehavior}, + activation::{ActivationVariables, Activator}, shell::ShellEnum, }; use tokio::task::JoinHandle; @@ -116,7 +116,7 @@ impl Prefix { record .files .iter() - .filter(|relative_path| is_executable(self, relative_path)) + .filter(|relative_path| self.is_executable(relative_path)) .filter_map(|path| { path.file_stem() .and_then(OsStr::to_str) @@ -125,62 +125,37 @@ impl Prefix { }) .collect() } -} - -pub(crate) fn is_executable(prefix: &Prefix, relative_path: &Path) -> bool { - // Check if the file is in a known executable directory. - let binary_folders = if cfg!(windows) { - &([ - "", - "Library/mingw-w64/bin/", - "Library/usr/bin/", - "Library/bin/", - "Scripts/", - "bin/", - ][..]) - } else { - &(["bin"][..]) - }; - - let parent_folder = match relative_path.parent() { - Some(dir) => dir, - None => return false, - }; - - if !binary_folders - .iter() - .any(|bin_path| Path::new(bin_path) == parent_folder) - { - return false; - } - // Check if the file is executable - let absolute_path = prefix.root().join(relative_path); - is_executable::is_executable(absolute_path) -} + /// Checks if the given relative path points to an executable file. + pub(crate) fn is_executable(&self, relative_path: &Path) -> bool { + // Check if the file is in a known executable directory. + let binary_folders = if cfg!(windows) { + &([ + "", + "Library/mingw-w64/bin/", + "Library/usr/bin/", + "Library/bin/", + "Scripts/", + "bin/", + ][..]) + } else { + &(["bin"][..]) + }; + + let parent_folder = match relative_path.parent() { + Some(dir) => dir, + None => return false, + }; + + if !binary_folders + .iter() + .any(|bin_path| Path::new(bin_path) == parent_folder) + { + return false; + } -#[allow(unused)] -/// Create the environment activation script -pub(crate) fn create_activation_script( - prefix: &Prefix, - shell: ShellEnum, -) -> miette::Result { - let activator = - Activator::from_path(prefix.root(), shell, Platform::current()).into_diagnostic()?; - let result = activator - .activation(ActivationVariables { - conda_prefix: None, - path: None, - path_modification_behavior: PathModificationBehavior::Prepend, - }) - .into_diagnostic()?; - - // Add a shebang on unix based platforms - let script = if cfg!(unix) { - format!("#!/bin/sh\n{}", result.script.contents().into_diagnostic()?) - } else { - result.script.contents().into_diagnostic()? - }; - - Ok(script) + // Check if the file is executable + let absolute_path = self.root().join(relative_path); + is_executable::is_executable(absolute_path) + } } From cb2865b497c64d2153e15eaa0ad94012e27c73b9 Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Tue, 24 Sep 2024 10:58:11 +0200 Subject: [PATCH 23/26] Remove unused methods --- src/global/common.rs | 38 -------------------------------------- 1 file changed, 38 deletions(-) diff --git a/src/global/common.rs b/src/global/common.rs index ee24af35b..f283c7ca6 100644 --- a/src/global/common.rs +++ b/src/global/common.rs @@ -67,44 +67,6 @@ impl BinDir { } executable_script_path } - - pub async fn print_executables_available( - &self, - executables: Vec, - ) -> miette::Result<()> { - let whitespace = console::Emoji(" ", "").to_string(); - let executable = executables - .into_iter() - .map(|path| { - path.strip_prefix(self.path()) - .expect("script paths were constructed by joining onto BinDir") - .to_string_lossy() - .to_string() - }) - .join(&format!("\n{whitespace} - ")); - - if self.is_on_path() { - eprintln!( - "{whitespace}These executables are now globally available:\n{whitespace} - {executable}", - ) - } else { - eprintln!("{whitespace}These executables have been added to {}\n{whitespace} - {executable}\n\n{} To use them, make sure to add {} to your PATH", - console::style(&self.path().display()).bold(), - console::style("!").yellow().bold(), - console::style(&self.path().display()).bold() - ) - } - - Ok(()) - } - - /// Returns true if the bin folder is available on the PATH. - fn is_on_path(&self) -> bool { - let Some(path_content) = std::env::var_os("PATH") else { - return false; - }; - std::env::split_paths(&path_content).contains(&self.path().to_owned()) - } } /// Global environoments directory, default to `$HOME/.pixi/envs` From 010fb4875b77d9123556152b9d61c96de40c774b Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Tue, 24 Sep 2024 11:01:29 +0200 Subject: [PATCH 24/26] Move import --- src/global/common.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/global/common.rs b/src/global/common.rs index f283c7ca6..c3dfc7ac2 100644 --- a/src/global/common.rs +++ b/src/global/common.rs @@ -3,7 +3,6 @@ use std::{ path::{Path, PathBuf}, }; -use itertools::Itertools; use miette::{Context, IntoDiagnostic}; use pixi_config::home_path; @@ -198,6 +197,7 @@ pub(crate) fn is_text(file_path: impl AsRef) -> miette::Result { #[cfg(test)] mod tests { use super::*; + use itertools::Itertools; use tempfile::tempdir; From 057bd76e9445b9ea435300f6d8c25d80d81c8104 Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Tue, 24 Sep 2024 11:42:22 +0200 Subject: [PATCH 25/26] Also update parsed and tests --- src/global/project/manifest.rs | 74 +++++++++++++++++++++++++++++++--- src/global/project/mod.rs | 6 +-- 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/src/global/project/manifest.rs b/src/global/project/manifest.rs index 22f9779c9..3b6161e6f 100644 --- a/src/global/project/manifest.rs +++ b/src/global/project/manifest.rs @@ -62,6 +62,16 @@ impl Manifest { env_name: &EnvironmentName, mapping: &Mapping, ) -> miette::Result<()> { + self.parsed + .envs + .entry(env_name.clone()) + .or_default() + .exposed + .insert( + mapping.exposed_name.clone(), + mapping.executable_name.clone(), + ); + self.document .get_or_insert_nested_table(&format!("envs.{env_name}.exposed"))? .insert( @@ -78,6 +88,13 @@ impl Manifest { env_name: &EnvironmentName, exposed_name: &ExposedName, ) -> miette::Result<()> { + self.parsed + .envs + .get_mut(env_name) + .ok_or_else(|| miette::miette!("[envs.{env_name}] needs to exist"))? + .exposed + .shift_remove(exposed_name); + self.document .get_or_insert_nested_table(&format!("envs.{env_name}.exposed"))? .remove(&exposed_name.to_string()) @@ -136,6 +153,8 @@ mod tests { assert!(result.is_ok()); let expected_value = "test_executable"; + + // Check document let actual_value = manifest .document .get_or_insert_nested_table(&format!("envs.{}.exposed", env_name)) @@ -145,6 +164,17 @@ mod tests { .as_str() .unwrap(); assert_eq!(expected_value, actual_value); + + // Check parsed + let actual_value = manifest + .parsed + .envs + .get(&env_name) + .unwrap() + .exposed + .get(&exposed_name) + .unwrap(); + assert_eq!(expected_value, actual_value) } #[test] @@ -162,10 +192,11 @@ mod tests { let result = manifest.add_exposed_mapping(&env_name, &mapping2); assert!(result.is_ok()); + // Check document for executable1 let expected_value1 = "test_executable1"; let actual_value1 = manifest .document - .get_or_insert_nested_table(&format!("envs.{}.exposed", env_name)) + .get_or_insert_nested_table(&format!("envs.{env_name}.exposed")) .unwrap() .get(&exposed_name1.to_string()) .unwrap() @@ -173,16 +204,39 @@ mod tests { .unwrap(); assert_eq!(expected_value1, actual_value1); + // Check parsed for executable1 + let actual_value1 = manifest + .parsed + .envs + .get(&env_name) + .unwrap() + .exposed + .get(&exposed_name1) + .unwrap(); + assert_eq!(expected_value1, actual_value1); + + // Check document for executable2 let expected_value2 = "test_executable2"; let actual_value2 = manifest .document - .get_or_insert_nested_table(&format!("envs.{}.exposed", env_name)) + .get_or_insert_nested_table(&format!("envs.{env_name}.exposed")) .unwrap() .get(&exposed_name2.to_string()) .unwrap() .as_str() .unwrap(); assert_eq!(expected_value2, actual_value2); + + // Check parsed for executable2 + let actual_value2 = manifest + .parsed + .envs + .get(&env_name) + .unwrap() + .exposed + .get(&exposed_name2) + .unwrap(); + assert_eq!(expected_value2, actual_value2) } #[test] @@ -199,12 +253,23 @@ mod tests { .remove_exposed_name(&env_name, &exposed_name) .unwrap(); + // Check document let actual_value = manifest .document .get_or_insert_nested_table(&format!("envs.{env_name}.exposed")) .unwrap() .get(&exposed_name.to_string()); assert!(actual_value.is_none()); + + // Check parsed + let actual_value = manifest + .parsed + .envs + .get(&env_name) + .unwrap() + .exposed + .get(&exposed_name); + assert!(actual_value.is_none()) } #[test] @@ -214,8 +279,7 @@ mod tests { let env_name = EnvironmentName::from_str("test-env").unwrap(); // Removing an exposed name that doesn't exist should return an error - manifest - .remove_exposed_name(&env_name, &exposed_name) - .unwrap_err(); + let result = manifest.remove_exposed_name(&env_name, &exposed_name); + assert!(result.is_err()) } } diff --git a/src/global/project/mod.rs b/src/global/project/mod.rs index b6d67bcdf..c4b65a776 100644 --- a/src/global/project/mod.rs +++ b/src/global/project/mod.rs @@ -382,11 +382,11 @@ mod tests { const SIMPLE_MANIFEST: &str = r#" [envs.python] - channels = ["conda-forge"] + channels = ["dummy-channel"] [envs.python.dependencies] - python = "3.11.*" + dummy = "3.11.*" [envs.python.exposed] - python = "python" + dummy = "dummy" "#; #[tokio::test] From a7b6078939c0a8a0cf89e23fab9ac5008af5049a Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Tue, 24 Sep 2024 13:49:04 +0200 Subject: [PATCH 26/26] Don't update `parsed` in `save` --- src/global/project/manifest.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/global/project/manifest.rs b/src/global/project/manifest.rs index 3b6161e6f..8a41225ff 100644 --- a/src/global/project/manifest.rs +++ b/src/global/project/manifest.rs @@ -107,7 +107,6 @@ impl Manifest { /// Save the manifest to the file and update the parsed_manifest pub async fn save(&mut self) -> miette::Result<()> { let contents = self.document.to_string(); - self.parsed = ParsedManifest::from_toml_str(&contents)?; tokio::fs::write(&self.path, contents) .await .into_diagnostic()?;