Skip to content

Commit

Permalink
feat: add pixi global expose command (#2030)
Browse files Browse the repository at this point in the history
Some missing things:

- [x] : public doc strings
- [x] : tests
- [x] : better errors
- [x] : running pixi global sync at the end

it is also should be merged after
#1975 lands first

---------

Co-authored-by: Hofer-Julian <30049909+Hofer-Julian@users.noreply.github.com>
Co-authored-by: Julian Hofer <julianhofer@gnome.org>
  • Loading branch information
3 people authored Sep 24, 2024
1 parent 2a4c451 commit c17fd80
Show file tree
Hide file tree
Showing 22 changed files with 808 additions and 319 deletions.
2 changes: 2 additions & 0 deletions crates/pixi_manifest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ mod validation;
pub use dependencies::{CondaDependencies, Dependencies, PyPiDependencies};

pub use manifests::manifest::{Manifest, ManifestKind};
pub use manifests::TomlManifest;

pub use crate::environments::Environments;
pub use crate::parsed_manifest::{deserialize_package_map, ParsedManifest};
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;
Expand Down
10 changes: 9 additions & 1 deletion crates/pixi_manifest/src/manifests/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::fmt;

use toml_edit::{self, Array, Item, Table, Value};

pub mod project;
Expand All @@ -23,7 +25,7 @@ impl TomlManifest {
/// 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.
fn get_or_insert_nested_table<'a>(
pub fn get_or_insert_nested_table<'a>(
&'a mut self,
table_name: &str,
) -> Result<&'a mut Table, TomlError> {
Expand Down Expand Up @@ -75,3 +77,9 @@ impl TomlManifest {
Ok(array)
}
}

impl fmt::Display for TomlManifest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
146 changes: 146 additions & 0 deletions src/cli/global/expose.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
use std::str::FromStr;

use clap::Parser;
use miette::Context;
use pixi_config::{Config, ConfigCli};

use crate::global::{self, EnvironmentName, ExposedName};

#[derive(Parser, Debug)]
pub struct AddArgs {
/// 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<global::Mapping>,

#[clap(short, long)]
environment: EnvironmentName,

/// Answer yes to all questions.
#[clap(short = 'y', long = "yes", long = "assume-yes")]
assume_yes: bool,

#[clap(flatten)]
config: ConfigCli,
}

/// Parse mapping between exposed name and executable name
fn parse_mapping(input: &str) -> miette::Result<global::Mapping> {
input
.split_once('=')
.ok_or_else(|| {
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(),
))
})
}
#[derive(Parser, Debug)]
pub struct RemoveArgs {
/// The exposed names that should be removed
exposed_names: Vec<ExposedName>,

#[clap(short, long)]
environment: EnvironmentName,

/// Answer yes to all questions.
#[clap(short = 'y', long = "yes", long = "assume-yes")]
assume_yes: bool,

#[clap(flatten)]
config: ConfigCli,
}

#[derive(Parser, Debug)]
#[clap(group(clap::ArgGroup::new("command")))]
pub enum SubCommand {
#[clap(name = "add")]
Add(AddArgs),
#[clap(name = "remove")]
Remove(RemoveArgs),
}

/// Expose some binaries
pub async fn execute(args: SubCommand) -> miette::Result<()> {
match args {
SubCommand::Add(args) => add(args).await?,
SubCommand::Remove(args) => remove(args).await?,
}
Ok(())
}

async fn revert_after_error(
mut project_original: global::Project,
config: &Config,
) -> miette::Result<()> {
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 = 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;

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(())
}

if let Err(err) = apply_changes(args, project_original.clone(), &config).await {
revert_after_error(project_original, &config)
.await
.wrap_err("Could not add exposed mappings. Reverting also failed.")?;
return Err(err);
}
Ok(())
}

pub async fn remove(args: RemoveArgs) -> miette::Result<()> {
let config = Config::with_cli_config(&args.config);
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;

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(())
}

if let Err(err) = apply_changes(args, project_original.clone(), &config).await {
revert_after_error(project_original, &config)
.await
.wrap_err("Could not remove exposed name. Reverting also failed.")?;
return Err(err);
}
Ok(())
}
5 changes: 5 additions & 0 deletions src/cli/global/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use clap::Parser;

mod expose;
mod install;
mod list;
mod remove;
Expand All @@ -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
Expand All @@ -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(())
}
8 changes: 5 additions & 3 deletions src/cli/global/sync.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::global::{self};
use crate::global;
use clap::Parser;
use pixi_config::{Config, ConfigCli};

Expand All @@ -15,8 +15,10 @@ 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 updated_env = global::sync(&config, args.assume_yes).await?;
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 {
eprintln!(
Expand Down
66 changes: 16 additions & 50 deletions src/global/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ use std::{
path::{Path, PathBuf},
};

use itertools::Itertools;
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)]
pub struct BinDir(PathBuf);

impl BinDir {
Expand Down Expand Up @@ -59,53 +59,16 @@ 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");
}
executable_script_path
}

pub async fn print_executables_available(
&self,
executables: Vec<PathBuf>,
) -> 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`
#[derive(Debug, Clone)]
pub struct EnvRoot(PathBuf);

Expand All @@ -116,7 +79,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))
}

Expand All @@ -128,7 +91,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))
}

Expand Down Expand Up @@ -189,16 +152,16 @@ impl EnvRoot {

/// A global environment directory
pub(crate) struct EnvDir {
path: PathBuf,
pub(crate) path: PathBuf,
}

impl EnvDir {
/// Create a global environment directory
pub(crate) async fn new(
root: EnvRoot,
/// Create a global environment directory based on passed global environment root
pub(crate) async fn from_env_root(
env_root: EnvRoot,
environment_name: EnvironmentName,
) -> miette::Result<Self> {
let path = root.path().join(environment_name.as_str());
let path = env_root.path().join(environment_name.as_str());
tokio::fs::create_dir_all(&path).await.into_diagnostic()?;

Ok(Self { path })
Expand Down Expand Up @@ -234,6 +197,7 @@ pub(crate) fn is_text(file_path: impl AsRef<Path>) -> miette::Result<bool> {
#[cfg(test)]
mod tests {
use super::*;
use itertools::Itertools;

use tempfile::tempdir;

Expand All @@ -249,7 +213,9 @@ mod tests {
let environment_name = "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());
Expand All @@ -267,7 +233,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();
}
Expand Down
Loading

0 comments on commit c17fd80

Please sign in to comment.