Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 0 additions & 22 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,28 +124,6 @@ pub enum SortBy {
}
```

## Tests

To run all tests, use:
```bash
pixi run test
```
But if you have modified recipe data under the tests/data/channels directory, you need to update the test channel before running tests:
```bash
pixi run update-test-channel <channel_name>
```
> [!NOTE]
> This task currently only works on unix systems. If you are on Windows, it is recommended to use [WSL](https://learn.microsoft.com/en-us/windows/wsl/install).

For example, if you modified data for dummy_channel_1:
```bash
pixi run update-test-channel dummy_channel_1
```
After updating the test channel, run the tests again:
```
pixi run test
```

## CLI documentation
The CLI reference is automatically generated from the code documentation of CLI commands under `src/cli`.

Expand Down
20 changes: 0 additions & 20 deletions crates/pixi_consts/src/consts.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use console::Style;
use rattler_conda_types::NamedChannelOrUrl;
use std::{
collections::HashSet,
fmt::{Display, Formatter},
str::FromStr,
sync::LazyLock,
Expand Down Expand Up @@ -141,22 +140,3 @@ impl Display for PypiEmoji {
}
}
}

pub const OVERRIDE_EXCLUDED_KEYS: &[&str] = &[
"PIXI_PROJECT_ROOT",
"PIXI_PROJECT_NAME",
"PIXI_PROJECT_VERSION",
"PIXI_PROMPT",
"PIXI_ENVIRONMENT_NAME",
"PIXI_ENVIRONMENT_PLATFORMS",
"CONDA_PREFIX",
"CONDA_DEFAULT_ENV",
"PATH",
"INIT_CWD",
"PWD",
"PROJECT_NAME",
];

pub fn get_override_excluded_keys() -> HashSet<&'static str> {
OVERRIDE_EXCLUDED_KEYS.iter().copied().collect()
}
22 changes: 2 additions & 20 deletions crates/pixi_core/src/activation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ use fs_err::tokio as tokio_fs;
use indexmap::IndexMap;
use itertools::Itertools;
use miette::IntoDiagnostic;
use pixi_consts::consts;
use pixi_manifest::EnvironmentName;
use pixi_manifest::FeaturesExt;
use rattler_conda_types::Platform;
Expand Down Expand Up @@ -287,7 +286,7 @@ pub async fn run_activation(
let current_env = std::env::vars().collect::<HashMap<_, _>>();

// Run and cache the activation script
let new_activator = activator.run_activation(
activator.run_activation(
ActivationVariables {
// Get the current PATH variable
path: Default::default(),
Expand All @@ -302,24 +301,7 @@ pub async fn run_activation(
current_env,
},
None,
);

let override_excluded_keys = consts::get_override_excluded_keys();
// `activator.env_vars` should override `activator_result` for duplicate keys
new_activator.map(|mut map: HashMap<String, String>| {
// First pass: Add all variables from activator.env_vars(as map is unordered, we need to update the referenced value in the second loop)
for (k, v) in &activator.env_vars {
let should_exclude = override_excluded_keys.contains(k.as_str());
if !should_exclude {
map.insert(k.clone(), v.clone());
}
}

// Second pass: Loop through the map and resolve variable references
Environment::resolve_variable_references(&mut map);

map
})
)
})
.await
.into_diagnostic()?
Expand Down
192 changes: 24 additions & 168 deletions crates/pixi_core/src/task/executable_task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use deno_task_shell::{
ShellPipeWriter, ShellState, execute_with_pipes, parser::SequentialList, pipe,
};
use fs_err::tokio as tokio_fs;
use indexmap::IndexMap;
use itertools::Itertools;
use miette::{Context, Diagnostic};
use pixi_consts::consts;
Expand Down Expand Up @@ -130,18 +129,15 @@ impl<'p> ExecutableTask<'p> {
}

/// Returns the task as script
fn as_script(
&self,
command_env: IndexMap<String, String>,
) -> Result<Option<String>, FailedToParseShellScript> {
fn as_script(&self) -> Result<Option<String>, FailedToParseShellScript> {
// Convert the task into an executable string
let task = self
.task
.as_single_command(Some(&self.args))
.map_err(FailedToParseShellScript::ArgumentReplacement)?;
if let Some(task) = task {
// Get the export specific environment variables
let export = get_export_specific_task_env(self.task.as_ref(), command_env);
let export = get_export_specific_task_env(self.task.as_ref());

// Append the command line arguments verbatim
let cli_args = if let ArgValues::FreeFormArgs(additional_args) = &self.args {
Expand Down Expand Up @@ -169,15 +165,8 @@ impl<'p> ExecutableTask<'p> {
/// Returns a [`SequentialList`] which can be executed by deno task shell.
/// Returns `None` if the command is not executable like in the case of
/// an alias.
pub fn as_deno_script(
&self,
command_env: &HashMap<OsString, OsString>,
) -> Result<Option<SequentialList>, FailedToParseShellScript> {
let command_env_converted: IndexMap<String, String> = command_env
.iter()
.filter_map(|(k, v)| Some((k.to_str()?.to_string(), v.to_str()?.to_string())))
.collect();
let full_script = self.as_script(command_env_converted)?;
pub fn as_deno_script(&self) -> Result<Option<SequentialList>, FailedToParseShellScript> {
let full_script = self.as_script()?;

if let Some(full_script) = full_script {
tracing::debug!("Parsing shell script: {}", full_script);
Expand Down Expand Up @@ -248,7 +237,7 @@ impl<'p> ExecutableTask<'p> {
command_env: &HashMap<OsString, OsString>,
input: Option<&[u8]>,
) -> Result<RunOutput, TaskExecutionError> {
let Some(script) = self.as_deno_script(command_env)? else {
let Some(script) = self.as_deno_script()? else {
return Ok(RunOutput {
exit_code: 0,
stdout: String::new(),
Expand Down Expand Up @@ -393,98 +382,24 @@ fn get_output_writer_and_handle() -> (ShellPipeWriter, JoinHandle<String>) {
(writer, handle)
}

struct EnvMap {
command_env: IndexMap<String, String>,
task_specific_envs: IndexMap<String, String>,
}

impl EnvMap {
fn new(
command_env: IndexMap<String, String>,
task_specific_envs: Option<&IndexMap<String, String>>,
) -> Self {
Self {
command_env,
task_specific_envs: task_specific_envs.cloned().unwrap_or_default(),
}
}

// Get environment by key - returns reference to IndexMap directly
fn get(&self, key: &str) -> Option<&IndexMap<String, String>> {
match key {
"command_env" => Some(&self.command_env),
"task_specific_envs" => Some(&self.task_specific_envs),
_ => None,
}
}

// Priority keys sorted from lowest to highest priority
fn priority_keys(&self) -> [&'static str; 2] {
["command_env", "task_specific_envs"]
}

// Check if task_specific_envs contains a key
fn task_specific_contains_key(&self, key: &str) -> bool {
self.task_specific_envs.contains_key(key)
}

// Merge by priority
fn merge_by_priority(&self) -> IndexMap<String, String> {
let mut merged = IndexMap::new();

// Apply priority order: from lowest to highest using priority_keys()
for key in self.priority_keys() {
if let Some(env_map_key) = self.get(key) {
merged.extend(env_map_key.clone());
}
}

merged
}
}

/// Get the environment variables based on their priority
fn get_export_specific_task_env(task: &Task, command_env: IndexMap<String, String>) -> String {
// Early return if task.env() is empty
if task.env().is_none_or(|map| map.is_empty()) {
return String::new();
}

// Define keys that should not be overridden
let override_excluded_keys = consts::get_override_excluded_keys();

// Create environment map struct
let env_map = EnvMap::new(command_env.clone(), task.env());

let task_env = task
.env()
.expect("Task environment should exist at this point");

// Determine export strategy
let mut export_merged = if task_env.keys().all(|k| !command_env.contains_key(k)) {
// If task.env() and command_env don't have duplicated keys, simply export task.env().
task_env.clone()
} else {
// Handle conflicts with priority merging
env_map.merge_by_priority()
};

Environment::resolve_variable_references(&mut export_merged);
// Build export string
// Put all merged environment variables to export.
// Only the keys that are in "task_specific_envs" map would be exported.
/// Task specific environment variables.
fn get_export_specific_task_env(task: &Task) -> String {
// Append the environment variables if they don't exist
let mut export = String::new();
for (key, value) in export_merged {
let should_exclude = override_excluded_keys.contains(key.as_str());

if env_map.task_specific_contains_key(&key) && !should_exclude {
tracing::info!("Setting environment variable: {}=\"{}\"", key, value);
export.push_str(&format!("export \"{}={}\";\n", key, value));
if let Some(env) = task.env() {
for (key, value) in env {
if value.contains(format!("${}", key).as_str()) || std::env::var(key.as_str()).is_err()
{
tracing::info!("Setting environment variable: {}=\"{}\"", key, value);
export.push_str(&format!("export \"{}={}\";\n", key, value));
} else {
tracing::info!("Environment variable {} already set", key);
}
}
}

export
}

/// Determine the environment variables to use when executing a command. The
/// method combines the activation environment with the system environment
/// variables.
Expand Down Expand Up @@ -544,10 +459,10 @@ mod tests {
"#;

#[test]
fn test_export_specific_task_env_merge() {
fn test_export_specific_task_env() {
let file_contents = r#"
[tasks]
test = {cmd = "test", cwd = "tests", env = {FOO = "bar"}}
test = {cmd = "test", cwd = "tests", env = {FOO = "bar", BAR = "$FOO"}}
"#;
let workspace = Workspace::from_str(
Path::new("pixi.toml"),
Expand All @@ -559,53 +474,10 @@ mod tests {
.default_environment()
.task(&TaskName::from("test"), None)
.unwrap();
// Environment Variables
let mut my_map: IndexMap<String, String> = IndexMap::new();

my_map.insert("PATH".to_string(), "myPath".to_string());
my_map.insert("HOME".to_string(), "myHome".to_string());

let result = get_export_specific_task_env(task, my_map);

let expected_prefix = "export \"FOO=bar\"";
let path_prefix = "export \"PATH=myPath\"";
let home_prefix = "export \"HOME=myHome\"";

assert!(result.contains(expected_prefix));
// keys not defined in the task are not exported
assert!(!result.contains(path_prefix));
assert!(!result.contains(home_prefix));
}

#[test]
fn test_export_specific_task_env_priority() {
let file_contents = r#"
[tasks]
test = {cmd = "test", cwd = "tests", env = {FOO = "bar"}}
"#;
let workspace = Workspace::from_str(
Path::new("pixi.toml"),
&format!("{PROJECT_BOILERPLATE}\n{file_contents}"),
)
.unwrap();
let export = get_export_specific_task_env(task);

let task = workspace
.default_environment()
.task(&TaskName::from("test"), None)
.unwrap();
// Environment Variables
let mut my_map: IndexMap<String, String> = IndexMap::new();

my_map.insert("FOO".to_string(), "123".to_string());
my_map.insert("HOME".to_string(), "myHome".to_string());

let result = get_export_specific_task_env(task, my_map);
// task specific env overrides outside environment variables
let expected_prefix = "export \"FOO=bar\"";
let home_prefix = "export \"HOME=myHome\"";
assert!(result.contains(expected_prefix));
// keys not defined in the task are not exported
assert!(!result.contains(home_prefix));
assert_eq!(export, "export \"FOO=bar\";\nexport \"BAR=$FOO\";\n");
}

#[test]
Expand Down Expand Up @@ -634,24 +506,8 @@ mod tests {
args: ArgValues::default(),
};

// Environment Variables
let mut my_map: IndexMap<String, String> = IndexMap::new();

my_map.insert("PATH".to_string(), "myPath".to_string());
my_map.insert("HOME".to_string(), "myHome".to_string());

let result = executable_task.as_script(my_map);

let expected_prefix = "export \"FOO=bar\"";

let script = result.unwrap().expect("Script should not be None");
let path_prefix = "export \"PATH=myPath\"";
let home_prefix = "export \"HOME=myHome\"";

assert!(script.contains(expected_prefix));
// keys not defined in the task are not included
assert!(!script.contains(path_prefix));
assert!(!script.contains(home_prefix));
let script = executable_task.as_script().unwrap().unwrap();
assert_eq!(script, "export \"FOO=bar\";\n\ntest ");
}

#[tokio::test]
Expand Down
Loading