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: run tasks in the env they are defined #731

Merged
merged 5 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
1,100 changes: 38 additions & 1,062 deletions examples/cpp-sdl/pixi.lock

Large diffs are not rendered by default.

22 changes: 14 additions & 8 deletions examples/cpp-sdl/pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ channels = ["conda-forge"]
platforms = ["win-64", "linux-64", "osx-64", "osx-arm64"]

[tasks]
# Start the built executable
start = { cmd = ".build/bin/sdl_example", depends_on = ["build"] }

[dependencies]
sdl2 = "2.26.5.*"

[feature.build.dependencies]
cmake = "3.26.4.*"
cxx-compiler = "1.5.2.*"
ninja = "1.11.1.*"

[feature.build.tasks]
# Configures CMake
configure = { cmd = [
"cmake",
Expand All @@ -24,11 +36,5 @@ configure = { cmd = [
# Build the executable but make sure CMake is configured first.
build = { cmd = ["ninja", "-C", ".build"], depends_on = ["configure"] }

# Start the built executable
start = { cmd = ".build/bin/sdl_example", depends_on = ["build"] }

[dependencies]
cmake = "3.26.4.*"
cxx-compiler = "1.5.2.*"
sdl2 = "2.26.5.*"
ninja = "1.11.1.*"
[environments]
build = ["build"]
4 changes: 3 additions & 1 deletion src/activation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,9 @@ pub fn get_activator<'p>(
}

/// Runs and caches the activation script.
async fn run_activation(environment: &Environment<'_>) -> miette::Result<HashMap<String, String>> {
pub async fn run_activation(
environment: &Environment<'_>,
) -> miette::Result<HashMap<String, String>> {
let activator = get_activator(environment, ShellEnum::default()).map_err(|e| {
miette::miette!(format!(
"failed to create activator for {:?}\n{}",
Expand Down
120 changes: 90 additions & 30 deletions src/cli/run.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
use std::collections::hash_map::Entry;
use std::collections::HashSet;
use std::str::FromStr;
use std::{collections::HashMap, path::PathBuf, string::String};

use clap::Parser;
use itertools::Itertools;
use miette::{miette, Context, Diagnostic};
use rattler_conda_types::Platform;

use crate::activation::get_activation_env;
use crate::activation::get_environment_variables;
use crate::project::errors::UnsupportedPlatformError;
use crate::task::{ExecutableTask, FailedToParseShellScript, InvalidWorkingDirectory, TaskGraph};
use crate::Project;
use crate::{Project, UpdateLockFileOptions};

use crate::environment::LockFileUsage;
use crate::environment::LockFileDerivedData;
use crate::progress::await_in_progress;
use crate::project::manifest::EnvironmentName;
use crate::project::Environment;
use thiserror::Error;
Expand All @@ -38,13 +41,28 @@ pub struct Args {
/// CLI entry point for `pixi run`
/// When running the sigints are ignored and child can react to them. As it pleases.
pub async fn execute(args: Args) -> miette::Result<()> {
// Load the project
let project = Project::load_or_else_discover(args.manifest_path.as_deref())?;
let environment_name = args

// Extract the passed in environment name.
let explicit_environment = args
.environment
.map_or_else(|| EnvironmentName::Default, EnvironmentName::Named);
let environment = project
.environment(&environment_name)
.ok_or_else(|| miette::miette!("unknown environment '{environment_name}'"))?;
.map(|n| EnvironmentName::from_str(n.as_str()))
.transpose()?
.map(|n| {
project
.environment(&n)
.ok_or_else(|| miette::miette!("unknown environment '{n}'"))
})
.transpose()?;

// Ensure that the lock-file is up-to-date.
let mut lock_file = project
.up_to_date_lock_file(UpdateLockFileOptions {
lock_file_usage: args.lock_file_usage.into(),
..UpdateLockFileOptions::default()
})
.await?;

// Split 'task' into arguments if it's a single string, supporting commands like:
// `"test 1 == 0 || echo failed"` or `"echo foo && echo bar"` or `"echo 'Hello World'"`
Expand All @@ -59,8 +77,13 @@ pub async fn execute(args: Args) -> miette::Result<()> {
tracing::debug!("Task parsed from run command: {:?}", task_args);

// Construct a task graph from the input arguments
let task_graph = TaskGraph::from_cmd_args(&project, task_args, Some(Platform::current()))
.context("failed to construct task graph from command line arguments")?;
let task_graph = TaskGraph::from_cmd_args(
&project,
task_args,
Some(Platform::current()),
explicit_environment.clone(),
)
.context("failed to construct task graph from command line arguments")?;

// Traverse the task graph in topological order and execute each individual task.
let mut task_idx = 0;
Expand All @@ -81,18 +104,24 @@ pub async fn execute(args: Args) -> miette::Result<()> {
eprintln!();
}
eprintln!(
"{}{}",
console::style("✨ Pixi task: ").bold(),
"{}{}{}{}{}",
console::Emoji("✨ ", ""),
console::style("Pixi task (").bold(),
console::style(executable_task.run_environment.name())
ruben-arts marked this conversation as resolved.
Show resolved Hide resolved
.bold()
.cyan(),
console::style("): ").bold(),
executable_task.display_command(),
);
}

// If we don't have a command environment yet, we need to compute it. We lazily compute the
// task environment because we only need the environment if a task is actually executed.
let task_env: &_ = match task_envs.entry(environment.clone()) {
let task_env: &_ = match task_envs.entry(executable_task.run_environment.clone()) {
Entry::Occupied(env) => env.into_mut(),
Entry::Vacant(entry) => {
let command_env = get_task_env(&environment, args.lock_file_usage.into()).await?;
let command_env =
get_task_env(&mut lock_file, &executable_task.run_environment).await?;
entry.insert(command_env)
}
};
Expand All @@ -105,7 +134,7 @@ pub async fn execute(args: Args) -> miette::Result<()> {
}
Err(TaskExecutionError::NonZeroExitCode(code)) => {
if code == 127 {
command_not_found(&project);
command_not_found(&project, explicit_environment);
}
std::process::exit(code);
}
Expand All @@ -117,34 +146,65 @@ pub async fn execute(args: Args) -> miette::Result<()> {
}

/// Called when a command was not found.
fn command_not_found(project: &Project) {
let available_tasks = project
.tasks(Some(Platform::current()))
.into_keys()
.sorted()
.collect_vec();
fn command_not_found<'p>(project: &'p Project, explicit_environment: Option<Environment<'p>>) {
let available_tasks: HashSet<String> = if let Some(explicit_environment) = explicit_environment
{
explicit_environment
.tasks(Some(Platform::current()))
.into_iter()
.flat_map(|tasks| tasks.into_keys())
.map(ToOwned::to_owned)
.collect()
} else {
project
.environments()
.into_iter()
.flat_map(|env| {
env.tasks(Some(Platform::current()))
.into_iter()
.flat_map(|tasks| tasks.into_keys())
.map(ToOwned::to_owned)
})
.collect()
};

if !available_tasks.is_empty() {
eprintln!(
"\nAvailable tasks:\n{}",
available_tasks.into_iter().format_with("\n", |name, f| {
f(&format_args!("\t{}", console::style(name).bold()))
})
available_tasks
.into_iter()
.sorted()
.format_with("\n", |name, f| {
f(&format_args!("\t{}", console::style(name).bold()))
})
);
}
}

/// Determine the environment variables to use when executing a command. The method combines the
/// activation environment with the system environment variables.
pub async fn get_task_env(
environment: &Environment<'_>,
lock_file_usage: LockFileUsage,
pub async fn get_task_env<'p>(
lock_file_derived_data: &mut LockFileDerivedData<'p>,
environment: &Environment<'p>,
) -> miette::Result<HashMap<String, String>> {
// Activate the environment.
let activation_env = get_activation_env(environment, lock_file_usage).await?;
// Ensure there is a valid prefix
lock_file_derived_data.prefix(environment).await?;

// Get environment variables from the activation
let activation_env = await_in_progress("activating environment", |_| {
crate::activation::run_activation(environment)
})
.await
.wrap_err("failed to activate environment")?;

// Get environments from pixi
let environment_variables = get_environment_variables(environment);

// Concatenate with the system environment variables
Ok(std::env::vars().chain(activation_env).collect())
Ok(std::env::vars()
.chain(activation_env)
.chain(environment_variables)
.collect())
}

#[derive(Debug, Error, Diagnostic)]
Expand Down
44 changes: 40 additions & 4 deletions src/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,13 @@ pub async fn get_up_to_date_prefix(
sanity_check_project(project)?;

// Ensure that the lock-file is up-to-date
let mut lock_file =
ensure_up_to_date_lock_file(project, existing_repo_data, lock_file_usage, no_install)
.await?;
let mut lock_file = project
.up_to_date_lock_file(UpdateLockFileOptions {
existing_repo_data,
lock_file_usage,
no_install,
})
.await?;

// Get the locked environment from the lock-file.
if no_install {
Expand All @@ -183,6 +187,38 @@ pub async fn get_up_to_date_prefix(
}
}

/// Options to pass to [`Project::up_to_date_lock_file`].
#[derive(Default)]
pub struct UpdateLockFileOptions {
/// Defines what to do if the lock-file is out of date
pub lock_file_usage: LockFileUsage,

/// Don't install anything to disk.
pub no_install: bool,

/// Existing repodata that can be used to avoid downloading it again.
pub existing_repo_data: IndexMap<(Channel, Platform), SparseRepoData>,
}

impl Project {
/// Ensures that the lock-file is up-to-date with the project information.
///
/// Returns the lock-file and any potential derived data that was computed as part of this
/// operation.
pub async fn up_to_date_lock_file(
&self,
options: UpdateLockFileOptions,
) -> miette::Result<LockFileDerivedData<'_>> {
ensure_up_to_date_lock_file(
self,
options.existing_repo_data,
options.lock_file_usage,
options.no_install,
)
.await
}
}

#[allow(clippy::too_many_arguments)]
// TODO: refactor args into struct
pub async fn update_prefix_pypi(
Expand Down Expand Up @@ -689,7 +725,7 @@ async fn ensure_up_to_date_lock_file(
existing_repo_data: IndexMap<(Channel, Platform), SparseRepoData>,
lock_file_usage: LockFileUsage,
no_install: bool,
) -> miette::Result<LockFileDerivedData> {
) -> miette::Result<LockFileDerivedData<'_>> {
let lock_file = load_lock_file(project).await?;
let current_platform = Platform::current();
let package_cache = Arc::new(PackageCache::new(config::get_cache_dir()?.join("pkgs")));
Expand Down
7 changes: 5 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ mod pypi_marker_env;
mod pypi_tags;

pub use activation::get_activation_env;
pub use environment::UpdateLockFileOptions;
pub use lock_file::load_lock_file;
pub use project::manifest::FeatureName;
pub use project::{DependencyType, Project, SpecType};
pub use project::{
manifest::{EnvironmentName, FeatureName},
DependencyType, Project, SpecType,
};
pub use task::{
CmdArgs, ExecutableTask, RunOutput, Task, TaskExecutionError, TaskGraph, TaskGraphError,
};
Expand Down
5 changes: 5 additions & 0 deletions src/project/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ impl Debug for Environment<'_> {
}

impl<'p> Environment<'p> {
/// Returns true if this environment is the default environment.
pub fn is_default(&self) -> bool {
self.environment.name == EnvironmentName::Default
}

/// Returns the project this environment belongs to.
pub fn project(&self) -> &'p Project {
self.project
Expand Down
5 changes: 5 additions & 0 deletions src/project/manifest/feature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ pub struct Feature {
}

impl Feature {
/// Returns true if this feature is the default feature.
pub fn is_default(&self) -> bool {
self.name == FeatureName::Default
}

/// Returns the dependencies of the feature for a given `spec_type` and `platform`.
///
/// This function returns a [`Cow`]. If the dependencies are not combined or overwritten by
Expand Down
9 changes: 9 additions & 0 deletions src/task/error.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::project::manifest::EnvironmentName;
use miette::Diagnostic;
use thiserror::Error;

Expand All @@ -6,3 +7,11 @@ use thiserror::Error;
pub struct MissingTaskError {
pub task_name: String,
}

// TODO: We should make this error much better
#[derive(Debug, Error, Diagnostic)]
#[error("'{task_name}' is ambiguous")]
ruben-arts marked this conversation as resolved.
Show resolved Hide resolved
pub struct AmbiguousTaskError {
pub task_name: String,
pub environments: Vec<EnvironmentName>,
}
3 changes: 3 additions & 0 deletions src/task/executable_task.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::project::Environment;
use crate::{
task::task_graph::{TaskGraph, TaskId},
task::{quote_arguments, Task},
Expand Down Expand Up @@ -52,6 +53,7 @@ pub struct ExecutableTask<'p> {
pub project: &'p Project,
pub name: Option<String>,
pub task: Cow<'p, Task>,
pub run_environment: Environment<'p>,
pub additional_args: Vec<String>,
}

Expand All @@ -63,6 +65,7 @@ impl<'p> ExecutableTask<'p> {
project: task_graph.project(),
name: node.name.clone(),
task: node.task.clone(),
run_environment: node.run_environment.clone(),
additional_args: node.additional_args.clone(),
}
}
Expand Down
Loading
Loading