Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: add pixi global expose command #2030

Open
wants to merge 32 commits into
base: feature/pixi-global
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
daf1e50
misc: work on pixi global expose
nichmor Sep 10, 2024
0254be7
feat: add pixi global expose functionality
nichmor Sep 10, 2024
230f936
Merge branch 'main' into feat/add-pixi-global-expose-command
nichmor Sep 10, 2024
d48a41b
misc: fix some stuff after merging
nichmor Sep 10, 2024
4285980
Merge branch 'feature/pixi-global' into feat/add-pixi-global-expose-c…
nichmor Sep 10, 2024
0450962
Merge branch 'feat/add-pixi-global-expose-command' of https://github.…
nichmor Sep 10, 2024
e549a1c
feat: add also global expose remove
nichmor Sep 13, 2024
4f74022
Merge branch 'feature/pixi-global' into feat/add-pixi-global-expose-c…
Hofer-Julian Sep 19, 2024
f65716e
Merge branch 'feature/pixi-global' into feat/add-pixi-global-expose-c…
Hofer-Julian Sep 19, 2024
799b3c5
misc: some WIP
nichmor Sep 20, 2024
75dfe65
Merge branch 'feature/pixi-global' into feat/add-pixi-global-expose-c…
nichmor Sep 20, 2024
a5018ed
wip: some conflicts unresolved
nichmor Sep 23, 2024
898383f
Refactor EVERYTHING
Hofer-Julian Sep 23, 2024
0be2070
Adapt environment flag
Hofer-Julian Sep 23, 2024
611bad7
First mostly working version
Hofer-Julian Sep 23, 2024
e5a63dd
Improve code somewhat
Hofer-Julian Sep 23, 2024
ffde8ff
Adapt docs
Hofer-Julian Sep 23, 2024
bf1508a
Merge branch 'feature/pixi-global' into feat/add-pixi-global-expose-c…
Hofer-Julian Sep 23, 2024
5a89120
Update to latest main
Hofer-Julian Sep 23, 2024
8cd895b
Update tests
Hofer-Julian Sep 23, 2024
db1d269
Remove comments
Hofer-Julian Sep 23, 2024
3ca2021
Update `find_executables`
Hofer-Julian Sep 23, 2024
ea3b905
Simplify expose a bit
Hofer-Julian Sep 24, 2024
6c46e60
Add tests based on dummy channel
Hofer-Julian Sep 24, 2024
e8b00d1
Simplify errors
Hofer-Julian Sep 24, 2024
3c11674
Add exposed tests
Hofer-Julian Sep 24, 2024
a044bf6
Add tests
Hofer-Julian Sep 24, 2024
aa452a0
Convert more tests to dummy channel
Hofer-Julian Sep 24, 2024
f6ce494
Add docs
Hofer-Julian Sep 24, 2024
cb2865b
Remove unused methods
Hofer-Julian Sep 24, 2024
010fb48
Move import
Hofer-Julian Sep 24, 2024
057bd76
Also update parsed and tests
Hofer-Julian Sep 24, 2024
File filter

Filter by extension

Filter by extension

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