tasks: Use environment variables from project (#15266)
This fixes #12125 and addresses what's described in here: - https://github.com/zed-industries/zed/issues/4977#issuecomment-2162094388 Before the changes in this PR, when running tasks, they inherited the Zed process environment, but that might not be the process environment that you'd get if you `cd` into a project directory. We already ran into that problem with language servers and we fixed it by loading the shell environment in the context of a projects root directory and then passing that to the language servers when starting them (or when looking for their binaries). What the change here does is to add the behavior for tasks too: we use the project-environment as the base environment with which to spawn tasks. Everything else still works the same, except that the base env is different. Release Notes: - Improved the environment-variable detection when running tasks so that tasks can now access environment variables as if the task had been spawned in a terminal that `cd`ed into a project directory. That means environment variables set by `direnv`/`asdf`/`mise` and other tools are now picked up. ([#12125](https://github.com/zed-industries/zed/issues/12125)). Demo: https://github.com/user-attachments/assets/8bfcc98f-0f9b-4439-b0d9-298aef1a3efe
This commit is contained in:
parent
5e04753d1c
commit
0360cda543
5 changed files with 205 additions and 34 deletions
|
@ -271,6 +271,10 @@ pub struct TaskContext {
|
|||
pub cwd: Option<PathBuf>,
|
||||
/// Additional environment variables associated with a given task.
|
||||
pub task_variables: TaskVariables,
|
||||
/// Environment variables obtained when loading the project into Zed.
|
||||
/// This is the environment one would get when `cd`ing in a terminal
|
||||
/// into the project's root directory.
|
||||
pub project_env: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// This is a new type representing a 'tag' on a 'runnable symbol', typically a test of main() function, found via treesitter.
|
||||
|
|
|
@ -184,13 +184,27 @@ impl TaskTemplate {
|
|||
.context("hashing task variables")
|
||||
.log_err()?;
|
||||
let id = TaskId(format!("{id_base}_{task_hash}_{variables_hash}"));
|
||||
let mut env = substitute_all_template_variables_in_map(
|
||||
&self.env,
|
||||
&task_variables,
|
||||
&variable_names,
|
||||
&mut substituted_variables,
|
||||
)?;
|
||||
env.extend(task_variables.into_iter().map(|(k, v)| (k, v.to_owned())));
|
||||
|
||||
let env = {
|
||||
// Start with the project environment as the base.
|
||||
let mut env = cx.project_env.clone();
|
||||
|
||||
// Extend that environment with what's defined in the TaskTemplate
|
||||
env.extend(self.env.clone());
|
||||
|
||||
// Then we replace all task variables that could be set in environment variables
|
||||
let mut env = substitute_all_template_variables_in_map(
|
||||
&env,
|
||||
&task_variables,
|
||||
&variable_names,
|
||||
&mut substituted_variables,
|
||||
)?;
|
||||
|
||||
// Last step: set the task variables as environment variables too
|
||||
env.extend(task_variables.into_iter().map(|(k, v)| (k, v.to_owned())));
|
||||
env
|
||||
};
|
||||
|
||||
Some(ResolvedTask {
|
||||
id: id.clone(),
|
||||
substituted_variables,
|
||||
|
@ -392,6 +406,7 @@ mod tests {
|
|||
let cx = TaskContext {
|
||||
cwd: None,
|
||||
task_variables: TaskVariables::default(),
|
||||
project_env: HashMap::default(),
|
||||
};
|
||||
assert_eq!(
|
||||
resolved_task(&task_without_cwd, &cx).cwd,
|
||||
|
@ -403,6 +418,7 @@ mod tests {
|
|||
let cx = TaskContext {
|
||||
cwd: Some(context_cwd.clone()),
|
||||
task_variables: TaskVariables::default(),
|
||||
project_env: HashMap::default(),
|
||||
};
|
||||
assert_eq!(
|
||||
resolved_task(&task_without_cwd, &cx)
|
||||
|
@ -421,6 +437,7 @@ mod tests {
|
|||
let cx = TaskContext {
|
||||
cwd: None,
|
||||
task_variables: TaskVariables::default(),
|
||||
project_env: HashMap::default(),
|
||||
};
|
||||
assert_eq!(
|
||||
resolved_task(&task_with_cwd, &cx)
|
||||
|
@ -434,6 +451,7 @@ mod tests {
|
|||
let cx = TaskContext {
|
||||
cwd: Some(context_cwd.clone()),
|
||||
task_variables: TaskVariables::default(),
|
||||
project_env: HashMap::default(),
|
||||
};
|
||||
assert_eq!(
|
||||
resolved_task(&task_with_cwd, &cx)
|
||||
|
@ -512,6 +530,7 @@ mod tests {
|
|||
&TaskContext {
|
||||
cwd: None,
|
||||
task_variables: TaskVariables::from_iter(all_variables.clone()),
|
||||
project_env: HashMap::default(),
|
||||
},
|
||||
).unwrap_or_else(|| panic!("Should successfully resolve task {task_with_all_variables:?} with variables {all_variables:?}"));
|
||||
|
||||
|
@ -599,6 +618,7 @@ mod tests {
|
|||
&TaskContext {
|
||||
cwd: None,
|
||||
task_variables: TaskVariables::from_iter(not_all_variables),
|
||||
project_env: HashMap::default(),
|
||||
},
|
||||
);
|
||||
assert_eq!(resolved_task_attempt, None, "If any of the Zed task variables is not substituted, the task should not be resolved, but got some resolution without the variable {removed_variable:?} (index {i})");
|
||||
|
@ -651,6 +671,7 @@ mod tests {
|
|||
VariableName::Symbol,
|
||||
"test_symbol".to_string(),
|
||||
))),
|
||||
project_env: HashMap::default(),
|
||||
};
|
||||
|
||||
for (i, symbol_dependent_task) in [
|
||||
|
@ -725,4 +746,74 @@ mod tests {
|
|||
.insert(VariableName::Symbol, "my-symbol".to_string());
|
||||
assert!(faulty_go_test.resolve_task("base", &context).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_project_env() {
|
||||
let all_variables = [
|
||||
(VariableName::Row, "1234".to_string()),
|
||||
(VariableName::Column, "5678".to_string()),
|
||||
(VariableName::File, "test_file".to_string()),
|
||||
(VariableName::Symbol, "my symbol".to_string()),
|
||||
];
|
||||
|
||||
let template = TaskTemplate {
|
||||
label: "my task".to_string(),
|
||||
command: format!(
|
||||
"echo {} {}",
|
||||
VariableName::File.template_value(),
|
||||
VariableName::Symbol.template_value(),
|
||||
),
|
||||
args: vec![],
|
||||
env: HashMap::from_iter([
|
||||
(
|
||||
"TASK_ENV_VAR1".to_string(),
|
||||
"TASK_ENV_VAR1_VALUE".to_string(),
|
||||
),
|
||||
(
|
||||
"TASK_ENV_VAR2".to_string(),
|
||||
format!(
|
||||
"env_var_2 {} {}",
|
||||
VariableName::Row.template_value(),
|
||||
VariableName::Column.template_value()
|
||||
),
|
||||
),
|
||||
(
|
||||
"PROJECT_ENV_WILL_BE_OVERWRITTEN".to_string(),
|
||||
"overwritten".to_string(),
|
||||
),
|
||||
]),
|
||||
..TaskTemplate::default()
|
||||
};
|
||||
|
||||
let project_env = HashMap::from_iter([
|
||||
(
|
||||
"PROJECT_ENV_VAR1".to_string(),
|
||||
"PROJECT_ENV_VAR1_VALUE".to_string(),
|
||||
),
|
||||
(
|
||||
"PROJECT_ENV_WILL_BE_OVERWRITTEN".to_string(),
|
||||
"PROJECT_ENV_WILL_BE_OVERWRITTEN_VALUE".to_string(),
|
||||
),
|
||||
]);
|
||||
|
||||
let context = TaskContext {
|
||||
cwd: None,
|
||||
task_variables: TaskVariables::from_iter(all_variables.clone()),
|
||||
project_env,
|
||||
};
|
||||
|
||||
let resolved = template
|
||||
.resolve_task(TEST_ID_BASE, &context)
|
||||
.unwrap()
|
||||
.resolved
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resolved.env["TASK_ENV_VAR1"], "TASK_ENV_VAR1_VALUE");
|
||||
assert_eq!(resolved.env["TASK_ENV_VAR2"], "env_var_2 1234 5678");
|
||||
assert_eq!(resolved.env["PROJECT_ENV_VAR1"], "PROJECT_ENV_VAR1_VALUE");
|
||||
assert_eq!(
|
||||
resolved.env["PROJECT_ENV_WILL_BE_OVERWRITTEN"],
|
||||
"overwritten"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue