fix(run): keep toolchain-derived values out of saved run configurations (#1023)

The run list applies Languages & Frameworks to every configuration it returns (interpreter or toolchain command, PATH, VIRTUAL_ENV, JAVA_HOME, GOROOT, GOPATH, Go module variables, RUSTUP_TOOLCHAIN), and every save sends the user configurations back from that list, so the derived values were written to run.toml: reopening a configuration showed the IDE's whole PATH in its environment, and since applied values never replace existing ones, a saved configuration kept the old toolchain after the interpreter or SDK changed.

Saving now drops what settings derive: an environment entry or an absolute command counts as derived when applying settings to the configuration without it produces it again. The user's own values, including ones for the same variables, are kept. Covers python, pytest, django-test, npm, cargo, go, maven, tomcat, ruby, rake, rspec and godot; Gradle stays as saved, as the run list already leaves it unapplied.
This commit is contained in:
l0ng-ai
2026-11-03 18:17:00 +08:00
committed by GitHub
parent cc4a29fa5d
commit bbc6a2bc4c
3 changed files with 135 additions and 2 deletions
+12 -1
View File
@@ -62,13 +62,24 @@ fn detected_and_saved(
all
}
/// Saves the user configurations. They come from [`configs`], toolchains applied, so what
/// settings derived is dropped again: only the user's own choices reach `run.toml`.
pub fn save_configs(
host: &Host,
window: &str,
configs: &[RunConfig],
removed_detected_ids: &[String],
) -> IdeResult<()> {
ide_run::save_user_configs_with_removals(&host.project(window)?.root, configs, removed_detected_ids)
let project = host.project(window)?;
let settings = project.settings.effective();
let configs: Vec<RunConfig> = configs
.iter()
.map(|c| match c.kind.as_str() {
"gradle" => c.clone(),
_ => ide_sdk::apply::without_derived(c, &project.root, &settings),
})
.collect();
ide_run::save_user_configs_with_removals(&project.root, &configs, removed_detected_ids)
}
pub async fn start(host: &Host, window: &str, config: RunConfig, on_output: Emit<OutputChunk>) -> IdeResult<RunInfo> {
+41
View File
@@ -769,6 +769,47 @@ pub fn apply_to_run_configs(configs: &mut [RunConfig], root: &Path, s: &Settings
}
}
/// `config` as it should be saved: without what [`apply_to_run_configs`] derives from
/// settings, so a configuration read from the run list, edited and saved keeps following
/// Languages & Frameworks instead of freezing today's toolchain (and the IDE's whole
/// `PATH`) into `run.toml`. A value counts as derived when applying settings to the
/// configuration without it produces it again: an environment entry (`PATH`,
/// `VIRTUAL_ENV`, `JAVA_HOME`, `GOROOT`, …) or an absolute command its bare name
/// resolves to (the project interpreter, a toolchain's `cargo` / `go`, Maven's `mvn`).
pub fn without_derived(config: &RunConfig, root: &Path, s: &Settings) -> RunConfig {
let applied = |c: &RunConfig| {
let mut c = c.clone();
apply_to_run_configs(std::slice::from_mut(&mut c), root, s);
c
};
let mut out = config.clone();
// Until nothing changes: some entries are derived only once others are gone
// (the virtualenv's `PATH` is added only without `VIRTUAL_ENV`).
while let Some(key) = out
.env
.iter()
.find(|(key, value)| {
let mut probe = out.clone();
probe.env.remove(*key);
applied(&probe).env.get(*key) == Some(*value)
})
.map(|(key, _)| key.clone())
{
out.env.remove(&key);
}
let path = Path::new(&config.command);
if path.is_absolute()
&& let Some(name) = path.file_name().map(|n| n.to_string_lossy().into_owned())
{
let mut probe = out.clone();
probe.command = name.clone();
if applied(&probe).command == config.command {
out.command = name;
}
}
out
}
fn sh_quote(s: &str) -> String {
format!("'{}'", s.replace('\'', r"'\''"))
}
+82 -1
View File
@@ -1,6 +1,6 @@
use crate::apply::{
apply_to_run_configs, configure_server, jdtls_jdk, maven_command, maven_options, project_jdk, python_interpreter,
terminal_activation, tsdk,
terminal_activation, tsdk, without_derived,
};
use crate::python::parse_pip_list;
use crate::{bin_dir, exe, venv_of};
@@ -1108,3 +1108,84 @@ fn go_modules_configure_gopls_in_place_and_commands_with_explicit_run_overrides(
configure_server(&mut cleared, tmp.path(), tmp.path(), &Settings::default());
assert_eq!(cleared.settings.unwrap()["env"], json!({}));
}
#[test]
fn saving_a_loaded_run_configuration_drops_what_settings_derived() {
if cfg!(windows) {
return; // Unix paths
}
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let touch = |rel: &str| {
let p = root.join(rel);
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
std::fs::write(&p, "").unwrap();
p.to_string_lossy().into_owned()
};
let mut s = Settings::default();
s.languages.python.interpreter = fake_venv(root, ".venv").to_string_lossy().into_owned();
s.languages.node.interpreter = touch("node/bin/node");
touch("rustup/toolchains/stable/bin/cargo");
s.languages.rust.toolchain = root.join("rustup/toolchains/stable").to_string_lossy().into_owned();
touch("go/bin/go");
s.languages.go.goroot = root.join("go").to_string_lossy().into_owned();
s.languages.go.gopath = root.join("gopath").to_string_lossy().into_owned();
s.languages.go.modules.env = BTreeMap::from([("GOPROXY".into(), "https://proxy.example".into())]);
fake_jdk(&root.join("jdk17"));
s.languages.java.jdk = root.join("jdk17").to_string_lossy().into_owned();
touch("maven/bin/mvn");
s.languages.java.maven.home = root.join("maven").to_string_lossy().into_owned();
s.languages.ruby.interpreter = touch("ruby/bin/ruby");
touch("ruby/bin/bundle");
s.tools.insert("godot".into(), ide_core::settings::ToolSettings { path: touch("Godot/godot") });
let saved = |kind: &str, command: &str, args: &[&str], env: &[(&str, &str)]| {
let mut c = run(kind, command, args);
c.detected = false;
c.env = env.iter().map(|(k, v)| ((*k).to_owned(), (*v).to_owned())).collect();
c
};
// As the user saved them: bare tools, only their own environment.
let configs = [
saved("python", "python", &["app.py"], &[]),
saved("pytest", ".venv/bin/pytest", &["-x"], &[("FOO", "1")]),
saved("npm", "npm", &["run", "dev"], &[]),
saved("cargo", "cargo", &["run"], &[]),
saved("go", "go", &["run", "."], &[]),
saved("go", "go", &["mod", "tidy"], &[("GOPROXY", "off")]),
saved("maven", "mvn", &["package"], &[]),
saved("tomcat", "catalina", &[], &[]),
saved("ruby", "ruby", &["app.rb"], &[]),
saved("rake", "bundle", &["exec", "rake"], &[("RAILS_ENV", "test")]),
saved("godot", "godot", &["--path", "."], &[]),
// The user's own values that merely look like derived ones stay.
saved("cargo", "/opt/rust/bin/cargo", &["build"], &[("PATH", "/custom/bin")]),
saved("maven", "mvn", &["verify"], &[("JAVA_HOME", "/other/jdk")]),
saved("python", "python", &["x.py"], &[("VIRTUAL_ENV", "/elsewhere/venv")]),
];
for user in configs {
let mut loaded = user.clone();
apply_to_run_configs(std::slice::from_mut(&mut loaded), root, &s);
assert_ne!(loaded, user, "{} {}: settings apply to it", user.kind, user.command);
let stored = without_derived(&loaded, root, &s);
assert_eq!(stored, user, "{} {}: saved as the user left it", user.kind, user.command);
let mut reloaded = stored;
apply_to_run_configs(std::slice::from_mut(&mut reloaded), root, &s);
assert_eq!(reloaded, loaded, "{} {}: runs the same after a save", user.kind, user.command);
}
// Another bare name for the interpreter comes back as the interpreter's own.
let mut loaded = saved("python", "python3", &["app.py"], &[]);
apply_to_run_configs(std::slice::from_mut(&mut loaded), root, &s);
assert_eq!(without_derived(&loaded, root, &s).command, exe("python"));
// A saved configuration follows a later interpreter change instead of a frozen PATH.
let mut loaded = saved("ruby", "ruby", &["app.rb"], &[]);
apply_to_run_configs(std::slice::from_mut(&mut loaded), root, &s);
let mut next = without_derived(&loaded, root, &s);
s.languages.ruby.interpreter = touch("ruby2/bin/ruby");
apply_to_run_configs(std::slice::from_mut(&mut next), root, &s);
let ruby2 = root.join("ruby2/bin");
assert_eq!(next.command, ruby2.join("ruby").to_string_lossy());
assert!(next.env["PATH"].starts_with(&*ruby2.to_string_lossy()), "{}", next.env["PATH"]);
}