Skip to content

Commit

Permalink
feat: make binary, config folder, and lock file names dynamic
Browse files Browse the repository at this point in the history
Make the pixi binary name dynamic so that it's determined off the actual binary
name when invoked so if it's renamed, it reports correctly.

Make the pixi config folder and default lock file to use overridable in cargo config.

Make the default channels overriable in a build in cargo config. Automatically parse
full URLs in default channels list as URLs and not short names at compile time.

Make some functions and mods accessed externally pub instead of pub(crate).
  • Loading branch information
zbowling committed Dec 29, 2024
1 parent de96ca9 commit d708270
Show file tree
Hide file tree
Showing 13 changed files with 110 additions and 56 deletions.
6 changes: 5 additions & 1 deletion .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ rustflags = [
"link-arg=/STACK:8000000",
]

# Required for `dist` to work with linux arm targets: https://github.com/axodotdev/cargo-dist/issues/74#issuecomment-2053680080
[env]
PIXI_CONFIG_DIR = "pixi"
PIXI_PROJECT_LOCK_FILE = "pixi.lock"
PIXI_DEFAULT_CHANNELS = "conda-forge"
PIXI_DIR = ".pixi"
# Required for `dist` to work with linux arm targets: https://github.com/axodotdev/cargo-dist/issues/74#issuecomment-2053680080
CC_aarch64_unknown_linux_musl = "aarch64-linux-gnu-gcc"

[target.aarch64-unknown-linux-musl]
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ strip = false
async-trait = { workspace = true }
fake = "3.0.1"
http = { workspace = true }
insta = { workspace = true, features = ["yaml", "glob"] }
insta = { workspace = true, features = ["yaml", "glob", "filters"] }
rstest = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true, features = ["rt"] }
Expand Down
15 changes: 10 additions & 5 deletions crates/pixi_config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,7 @@ pub fn get_cache_dir() -> miette::Result<PathBuf> {
.map(PathBuf::from)
.or_else(|| std::env::var("RATTLER_CACHE_DIR").map(PathBuf::from).ok())
.or_else(|| {
let pixi_cache_dir = dirs::cache_dir().map(|d| d.join("pixi"));

let pixi_cache_dir = dirs::cache_dir().map(|d| d.join(consts::PIXI_DIR));
// Only use the xdg cache pixi directory when it exists
pixi_cache_dir.and_then(|d| d.exists().then_some(d))
})
Expand Down Expand Up @@ -964,7 +963,13 @@ impl Config {
if self.default_channels.is_empty() {
consts::DEFAULT_CHANNELS
.iter()
.map(|s| NamedChannelOrUrl::Name(s.to_string()))
.map(|s| {
if s.starts_with("http://") || s.starts_with("https://") {
NamedChannelOrUrl::Url(Url::parse(s).unwrap())
} else {
NamedChannelOrUrl::Name(s.to_string())
}
})
.collect()
} else {
self.default_channels.clone()
Expand Down Expand Up @@ -1256,13 +1261,13 @@ pub fn config_path_system() -> PathBuf {
#[cfg(not(target_os = "windows"))]
let base_path = PathBuf::from("/etc");

base_path.join("pixi").join(consts::CONFIG_FILE)
base_path.join(consts::CONFIG_DIR).join(consts::CONFIG_FILE)
}

/// Returns the path(s) to the global pixi config file.
pub fn config_path_global() -> Vec<PathBuf> {
vec![
dirs::config_dir().map(|d| d.join("pixi").join(consts::CONFIG_FILE)),
dirs::config_dir().map(|d| d.join(consts::CONFIG_DIR).join(consts::CONFIG_FILE)),
pixi_home().map(|d| d.join(consts::CONFIG_FILE)),
]
.into_iter()
Expand Down
40 changes: 34 additions & 6 deletions crates/pixi_consts/src/consts.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use console::Style;
use lazy_static::lazy_static;
use std::fmt::{Display, Formatter};
use std::{
ffi::OsStr,
fmt::{Display, Formatter},
path::Path,
};
use url::Url;

pub const DEFAULT_ENVIRONMENT_NAME: &str = "default";
Expand All @@ -9,9 +13,7 @@ pub const PYPROJECT_PIXI_PREFIX: &str = "tool.pixi";

pub const PROJECT_MANIFEST: &str = "pixi.toml";
pub const PYPROJECT_MANIFEST: &str = "pyproject.toml";
pub const PROJECT_LOCK_FILE: &str = "pixi.lock";
pub const CONFIG_FILE: &str = "config.toml";
pub const PIXI_DIR: &str = ".pixi";
pub const PIXI_VERSION: &str = match option_env!("PIXI_VERSION") {
Some(v) => v,
None => "0.39.4",
Expand All @@ -36,13 +38,39 @@ pub const _CACHED_BUILD_ENVS_DIR: &str = "cached-build-envs-v0";
pub const CACHED_BUILD_TOOL_ENVS_DIR: &str = "cached-build-tool-envs-v0";
pub const CACHED_GIT_DIR: &str = "git-cache-v0";

pub const CONFIG_DIR: &str = match option_env!("PIXI_CONFIG_DIR") {
Some(dir) => dir,
None => "pixi",
};
pub const PROJECT_LOCK_FILE: &str = match option_env!("PIXI_PROJECT_LOCK_FILE") {
Some(file) => file,
None => "pixi.lock",
};
pub const PIXI_DIR: &str = match option_env!("PIXI_DIR") {
Some(dir) => dir,
None => ".pixi",
};

lazy_static! {
/// The default channels to use for a new project.
pub static ref DEFAULT_CHANNELS: Vec<String> = match option_env!("PIXI_DEFAULT_CHANNELS") {
Some(channels) => channels.split(',').map(|s| s.to_string()).collect(),
None => vec!["conda-forge".to_string()],
};

/// The name of the binary.
pub static ref PIXI_BIN_NAME: String = std::env::args().next()
.as_ref()
.map(Path::new)
.and_then(Path::file_name)
.and_then(OsStr::to_str)
.map(String::from).unwrap_or("pixi".to_string());
}

pub const CONDA_INSTALLER: &str = "conda";

pub const ONE_TIME_MESSAGES_DIR: &str = "one-time-messages";

/// The default channels to use for a new project.
pub const DEFAULT_CHANNELS: &[&str] = &["conda-forge"];

pub const ENVIRONMENT_FILE_NAME: &str = "pixi";

lazy_static! {
Expand Down
5 changes: 4 additions & 1 deletion crates/pixi_manifest/src/manifests/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,10 @@ impl Manifest {
) -> miette::Result<bool> {
// Determine the name of the package to add
let (Some(name), spec) = spec.clone().into_nameless() else {
miette::bail!("pixi does not support wildcard dependencies")
miette::bail!(format!(
"{} does not support wildcard dependencies",
consts::PIXI_BIN_NAME.to_string()
));
};
let spec = PixiSpec::from_nameless_matchspec(spec, channel_config);
let mut any_added = false;
Expand Down
65 changes: 44 additions & 21 deletions src/cli/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ impl Generator for Shell {
}

/// Generate completions for the pixi cli, and print those to the stdout
pub(crate) fn execute(args: Args) -> miette::Result<()> {
pub fn execute(args: Args) -> miette::Result<()> {
// Generate the original completion script.
let script = get_completion_script(args.shell);

Expand All @@ -82,39 +82,45 @@ pub(crate) fn execute(args: Args) -> miette::Result<()> {
/// Generate the completion script using clap_complete for a specified shell.
fn get_completion_script(shell: Shell) -> String {
let mut buf = vec![];
clap_complete::generate(shell, &mut CommandArgs::command(), "pixi", &mut buf);
let bin_name: String = pixi_consts::consts::PIXI_BIN_NAME.to_string();
clap_complete::generate(shell, &mut CommandArgs::command(), &bin_name, &mut buf);
String::from_utf8(buf).expect("clap_complete did not generate a valid UTF8 script")
}

/// Replace the parts of the bash completion script that need different functionality.
fn replace_bash_completion(script: &str) -> Cow<str> {
// Adds tab completion to the pixi run command.
// NOTE THIS IS FORMATTED BY HAND
let pattern = r#"(?s)pixi__run\).*?opts="(.*?)".*?(if.*?fi)"#;
let replacement = r#"pixi__run)
// Replace the '-' with '__' since that's what clap's generator does as well for Bash Shell completion.
let bin_name: String = pixi_consts::consts::PIXI_BIN_NAME
.to_string()
.replace('-', "__");
let pattern = format!(r#"(?s){}__run\).*?opts="(.*?)".*?(if.*?fi)"#, &bin_name);
let replacement = r#"BIN_NAME__run)
opts="$1"
if [[ $${cur} == -* ]] ; then
COMPREPLY=( $$(compgen -W "$${opts}" -- "$${cur}") )
return 0
elif [[ $${COMP_CWORD} -eq 2 ]]; then
local tasks=$$(pixi task list --machine-readable 2> /dev/null)
local tasks=$$(BIN_NAME task list --machine-readable 2> /dev/null)
if [[ $$? -eq 0 ]]; then
COMPREPLY=( $$(compgen -W "$${tasks}" -- "$${cur}") )
return 0
fi
fi"#;
let re = Regex::new(pattern).unwrap();
re.replace(script, replacement)
let re = Regex::new(pattern.as_str()).unwrap();
re.replace(script, replacement.replace("BIN_NAME", &bin_name))
}

/// Replace the parts of the zsh completion script that need different functionality.
fn replace_zsh_completion(script: &str) -> Cow<str> {
// Adds tab completion to the pixi run command.
// NOTE THIS IS FORMATTED BY HAND
let pattern = r"(?ms)(\(run\))(?:.*?)(_arguments.*?)(\*::task)";
let bin_name: String = pixi_consts::consts::PIXI_BIN_NAME.to_string();
let replacement = r#"$1
local tasks
tasks=("$${(@s/ /)$$(pixi task list --machine-readable 2> /dev/null)}")
tasks=("$${(@s/ /)$$(BIN_NAME task list --machine-readable 2> /dev/null)}")
if [[ -n "$$tasks" ]]; then
_values 'task' "$${tasks[@]}"
Expand All @@ -124,12 +130,13 @@ fi
$2::task"#;

let re = Regex::new(pattern).unwrap();
re.replace(script, replacement)
re.replace(script, replacement.replace("BIN_NAME", &bin_name))
}

fn replace_fish_completion(script: &str) -> Cow<str> {
// Adds tab completion to the pixi run command.
let addition = "complete -c pixi -n \"__fish_seen_subcommand_from run\" -f -a \"(string split ' ' (pixi task list --machine-readable 2> /dev/null))\"";
let bin_name = pixi_consts::consts::PIXI_BIN_NAME.to_string();
let addition = format!("complete -c {} -n \"__fish_seen_subcommand_from run\" -f -a \"(string split ' ' ({} task list --machine-readable 2> /dev/null))\"", bin_name, bin_name);
let new_script = format!("{}{}\n", script, addition);
let pattern = r#"-n "__fish_seen_subcommand_from run""#;
let replacement = r#"-n "__fish_seen_subcommand_from run; or __fish_seen_subcommand_from r""#;
Expand All @@ -142,20 +149,24 @@ fn replace_fish_completion(script: &str) -> Cow<str> {
fn replace_nushell_completion(script: &str) -> Cow<str> {
// Adds tab completion to the pixi run command.
// NOTE THIS IS FORMATTED BY HAND
let pattern = r#"(#.*\n export extern "pixi run".*\n.*...task: string)([^\]]*--environment\(-e\): string)"#;
let bin_name = pixi_consts::consts::PIXI_BIN_NAME.to_string();
let pattern = format!(
r#"(#.*\n export extern "{} run".*\n.*...task: string)([^\]]*--environment\(-e\): string)"#,
bin_name
);
let replacement = r#"
def "nu-complete pixi run" [] {
^pixi info --json | from json | get environments_info | get tasks | flatten | uniq
def "nu-complete BIN_NAME run" [] {
^BIN_NAME info --json | from json | get environments_info | get tasks | flatten | uniq
}
def "nu-complete pixi run environment" [] {
^pixi info --json | from json | get environments_info | get name
def "nu-complete BIN_NAME run environment" [] {
^BIN_NAME info --json | from json | get environments_info | get name
}
${1}@"nu-complete pixi run"${2}@"nu-complete pixi run environment""#;
${1}@"nu-complete BIN_NAME run"${2}@"nu-complete BIN_NAME run environment""#;

let re = Regex::new(pattern).unwrap();
re.replace(script, replacement)
let re = Regex::new(pattern.as_str()).unwrap();
re.replace(script, replacement.replace("BIN_NAME", &bin_name))
}

#[cfg(test)]
Expand Down Expand Up @@ -201,7 +212,13 @@ _arguments "${_arguments_options[@]}" \
"#;
let result = replace_zsh_completion(script);
insta::assert_snapshot!(result);
let replacement = format!("{} task list", pixi_consts::consts::PIXI_BIN_NAME.as_str());
println!("{}", result);
insta::with_settings!({filters => vec![
(replacement.as_str(), "pixi task list"),
]}, {
insta::assert_snapshot!(result);
});
}

#[test]
Expand All @@ -215,9 +232,15 @@ _arguments "${_arguments_options[@]}" \
;;
pixi__run)
opts="-v -q -h --manifest-path --locked --frozen --verbose --quiet --color --help [TASK]..."
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
if [[ ${cur} == -* ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
elif [[ ${COMP_CWORD} -eq 2 ]]; then
local tasks=$(pixi task list --machine-readable 2> /dev/null)
if [[ $? -eq 0 ]]; then
COMPREPLY=( $(compgen -W "${tasks}" -- "${cur}") )
return 0
fi
fi
case "${prev}" in
--manifest-path)
Expand Down Expand Up @@ -260,7 +283,7 @@ _arguments "${_arguments_options[@]}" \
--manifest-path: string # The path to `pixi.toml` or `pyproject.toml`
--frozen # Install the environment as defined in the lockfile, doesn't update lockfile if it isn't up-to-date with the manifest file
--locked # Check if lockfile is up-to-date before installing the environment, aborts when lockfile isn't up-to-date with the manifest file
--environment(-e): string # The environment to run the task in
--environment(-e): string@"nu-complete pixi run" # The environment to run the task in
--tls-no-verify # Do not verify the TLS certificate of the server
--auth-file: string # Path to the file containing the authentication token
--pypi-keyring-provider: string@"nu-complete pixi run pypi_keyring_provider" # Specifies if we want to use uv keyring provider
Expand Down
2 changes: 1 addition & 1 deletion src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub mod clean;
pub mod cli_config;
pub mod completion;
pub mod config;
mod exec;
pub mod exec;
pub mod global;
pub mod has_specs;
pub mod info;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
source: src/cli/completion.rs
expression: result
---

pixi__project__help__help)
opts=""
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
Expand All @@ -11,8 +10,8 @@ expression: result
pixi__run)
opts="-v -q -h --manifest-path --locked --frozen --verbose --quiet --color --help [TASK]..."
if [[ ${cur} == -* ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
elif [[ ${COMP_CWORD} -eq 2 ]]; then
local tasks=$(pixi task list --machine-readable 2> /dev/null)
if [[ $? -eq 0 ]]; then
Expand Down Expand Up @@ -46,4 +45,3 @@ expression: result
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )

;;

Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,13 @@
source: src/cli/completion.rs
expression: result
---

def "nu-complete pixi run" [] {
^pixi info --json | from json | get environments_info | get tasks | flatten | uniq
}

def "nu-complete pixi run environment" [] {
^pixi info --json | from json | get environments_info | get name
}

# Runs task in project
export extern "pixi run" [
...task: string@"nu-complete pixi run" # The pixi task or a task shell command you want to run in the project's environment, which can be an executable in the environment's PATH
...task: string # The pixi task or a task shell command you want to run in the project's environment, which can be an executable in the environment's PATH
--manifest-path: string # The path to `pixi.toml` or `pyproject.toml`
--frozen # Install the environment as defined in the lockfile, doesn't update lockfile if it isn't up-to-date with the manifest file
--locked # Check if lockfile is up-to-date before installing the environment, aborts when lockfile isn't up-to-date with the manifest file
--environment(-e): string@"nu-complete pixi run environment" # The environment to run the task in
--environment(-e): string@"nu-complete pixi run" # The environment to run the task in
--tls-no-verify # Do not verify the TLS certificate of the server
--auth-file: string # Path to the file containing the authentication token
--pypi-keyring-provider: string@"nu-complete pixi run pypi_keyring_provider" # Specifies if we want to use uv keyring provider
Expand Down
2 changes: 1 addition & 1 deletion src/global/project/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -824,7 +824,7 @@ mod tests {
Some(
DEFAULT_CHANNELS
.iter()
.map(|&name| NamedChannelOrUrl::Name(name.to_string()))
.map(|name| NamedChannelOrUrl::Name(name.to_string()))
.collect(),
),
)
Expand Down
1 change: 1 addition & 0 deletions src/lock_file/records_by_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ impl HasNameVersion for PypiRecord {
&self.0.version
}
}
#[allow(refining_impl_trait)]
impl HasNameVersion for RepoDataRecord {
type N = rattler_conda_types::PackageName;
type V = VersionWithSource;
Expand Down
2 changes: 1 addition & 1 deletion src/project/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ impl<'p> Environment<'p> {
}

/// Returns the name of this environment.
pub(crate) fn name(&self) -> &EnvironmentName {
pub fn name(&self) -> &EnvironmentName {
&self.environment.name
}

Expand Down
7 changes: 4 additions & 3 deletions src/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,10 @@ impl Project {
}

miette::bail!(
"could not find {} or {} which is configured to use pixi",
"could not find {} or {} which is configured to use {}",
consts::PROJECT_MANIFEST,
consts::PYPROJECT_MANIFEST
consts::PYPROJECT_MANIFEST,
consts::PIXI_BIN_NAME.to_string()
);
}

Expand Down Expand Up @@ -423,7 +424,7 @@ impl Project {

/// Returns an environment in this project based on a name or an environment
/// variable.
pub(crate) fn environment_from_name_or_env_var(
pub fn environment_from_name_or_env_var(
&self,
name: Option<String>,
) -> miette::Result<Environment> {
Expand Down

0 comments on commit d708270

Please sign in to comment.