chore: Cleanup cargo config queries
diff --git a/crates/project-model/src/cargo_config_file.rs b/crates/project-model/src/cargo_config_file.rs
new file mode 100644
index 0000000..7966f74
--- /dev/null
+++ b/crates/project-model/src/cargo_config_file.rs
@@ -0,0 +1,34 @@
+//! Read `.cargo/config.toml` as a JSON object
+use rustc_hash::FxHashMap;
+use toolchain::Tool;
+
+use crate::{ManifestPath, Sysroot, utf8_stdout};
+
+pub(crate) type CargoConfigFile = serde_json::Map<String, serde_json::Value>;
+
+pub(crate) fn read(
+    manifest: &ManifestPath,
+    extra_env: &FxHashMap<String, Option<String>>,
+    sysroot: &Sysroot,
+) -> Option<CargoConfigFile> {
+    let mut cargo_config = sysroot.tool(Tool::Cargo, manifest.parent(), extra_env);
+    cargo_config
+        .args(["-Z", "unstable-options", "config", "get", "--format", "json"])
+        .env("RUSTC_BOOTSTRAP", "1");
+    if manifest.is_rust_manifest() {
+        cargo_config.arg("-Zscript");
+    }
+
+    tracing::debug!("Discovering cargo config by {:?}", cargo_config);
+    let json: serde_json::Map<String, serde_json::Value> = utf8_stdout(&mut cargo_config)
+        .inspect(|json| {
+            tracing::debug!("Discovered cargo config: {:?}", json);
+        })
+        .inspect_err(|err| {
+            tracing::debug!("Failed to discover cargo config: {:?}", err);
+        })
+        .ok()
+        .and_then(|stdout| serde_json::from_str(&stdout).ok())?;
+
+    Some(json)
+}
diff --git a/crates/project-model/src/env.rs b/crates/project-model/src/env.rs
index 9e0415c..d281492 100644
--- a/crates/project-model/src/env.rs
+++ b/crates/project-model/src/env.rs
@@ -1,10 +1,9 @@
 //! Cargo-like environment variables injection.
 use base_db::Env;
-use paths::{Utf8Path, Utf8PathBuf};
-use rustc_hash::FxHashMap;
+use paths::Utf8Path;
 use toolchain::Tool;
 
-use crate::{ManifestPath, PackageData, Sysroot, TargetKind, utf8_stdout};
+use crate::{ManifestPath, PackageData, TargetKind, cargo_config_file::CargoConfigFile};
 
 /// Recreates the compile-time environment variables that Cargo sets.
 ///
@@ -61,104 +60,68 @@
     env.set("CARGO_CRATE_NAME", cargo_name.replace('-', "_"));
 }
 
-pub(crate) fn cargo_config_env(
-    manifest: &ManifestPath,
-    extra_env: &FxHashMap<String, Option<String>>,
-    sysroot: &Sysroot,
-) -> Env {
-    let mut cargo_config = sysroot.tool(Tool::Cargo, manifest.parent(), extra_env);
-    cargo_config
-        .args(["-Z", "unstable-options", "config", "get", "env"])
-        .env("RUSTC_BOOTSTRAP", "1");
-    if manifest.is_rust_manifest() {
-        cargo_config.arg("-Zscript");
-    }
-    // if successful we receive `env.key.value = "value" per entry
-    tracing::debug!("Discovering cargo config env by {:?}", cargo_config);
-    utf8_stdout(&mut cargo_config)
-        .map(|stdout| parse_output_cargo_config_env(manifest, &stdout))
-        .inspect(|env| {
-            tracing::debug!("Discovered cargo config env: {:?}", env);
-        })
-        .inspect_err(|err| {
-            tracing::debug!("Failed to discover cargo config env: {:?}", err);
-        })
-        .unwrap_or_default()
-}
-
-fn parse_output_cargo_config_env(manifest: &ManifestPath, stdout: &str) -> Env {
+pub(crate) fn cargo_config_env(manifest: &ManifestPath, config: &Option<CargoConfigFile>) -> Env {
     let mut env = Env::default();
-    let mut relatives = vec![];
-    for (key, val) in
-        stdout.lines().filter_map(|l| l.strip_prefix("env.")).filter_map(|l| l.split_once(" = "))
-    {
-        let val = val.trim_matches('"').to_owned();
-        if let Some((key, modifier)) = key.split_once('.') {
-            match modifier {
-                "relative" => relatives.push((key, val)),
-                "value" => _ = env.insert(key, val),
-                _ => {
-                    tracing::warn!(
-                        "Unknown modifier in cargo config env: {}, expected `relative` or `value`",
-                        modifier
-                    );
-                    continue;
-                }
-            }
-        } else {
-            env.insert(key, val);
-        }
-    }
+    let Some(serde_json::Value::Object(env_json)) = config.as_ref().and_then(|c| c.get("env"))
+    else {
+        return env;
+    };
+
     // FIXME: The base here should be the parent of the `.cargo/config` file, not the manifest.
     // But cargo does not provide this information.
     let base = <_ as AsRef<Utf8Path>>::as_ref(manifest.parent());
-    for (key, relative) in relatives {
-        if relative != "true" {
-            continue;
-        }
-        if let Some(suffix) = env.get(key) {
-            env.insert(key, base.join(suffix).to_string());
-        }
-    }
-    env
-}
 
-pub(crate) fn cargo_config_build_target_dir(
-    manifest: &ManifestPath,
-    extra_env: &FxHashMap<String, Option<String>>,
-    sysroot: &Sysroot,
-) -> Option<Utf8PathBuf> {
-    let mut cargo_config = sysroot.tool(Tool::Cargo, manifest.parent(), extra_env);
-    cargo_config
-        .args(["-Z", "unstable-options", "config", "get", "build.target-dir"])
-        .env("RUSTC_BOOTSTRAP", "1");
-    if manifest.is_rust_manifest() {
-        cargo_config.arg("-Zscript");
+    for (key, entry) in env_json {
+        let serde_json::Value::Object(entry) = entry else {
+            continue;
+        };
+        let Some(value) = entry.get("value").and_then(|v| v.as_str()) else {
+            continue;
+        };
+
+        let value = if entry
+            .get("relative")
+            .and_then(|v| v.as_bool())
+            .is_some_and(std::convert::identity)
+        {
+            base.join(value).to_string()
+        } else {
+            value.to_owned()
+        };
+        env.insert(key, value);
     }
-    utf8_stdout(&mut cargo_config)
-        .map(|stdout| {
-            Utf8Path::new(stdout.trim_start_matches("build.target-dir = ").trim_matches('"'))
-                .to_owned()
-        })
-        .ok()
+
+    env
 }
 
 #[test]
 fn parse_output_cargo_config_env_works() {
-    let stdout = r#"
-env.CARGO_WORKSPACE_DIR.relative = true
-env.CARGO_WORKSPACE_DIR.value = ""
-env.RELATIVE.relative = true
-env.RELATIVE.value = "../relative"
-env.INVALID.relative = invalidbool
-env.INVALID.value = "../relative"
-env.TEST.value = "test"
-"#
-    .trim();
+    let raw = r#"
+{
+  "env": {
+    "CARGO_WORKSPACE_DIR": {
+      "relative": true,
+      "value": ""
+    },
+    "INVALID": {
+      "relative": "invalidbool",
+      "value": "../relative"
+    },
+    "RELATIVE": {
+      "relative": true,
+      "value": "../relative"
+    },
+    "TEST": {
+      "value": "test"
+    }
+  }
+}
+"#;
+    let config: CargoConfigFile = serde_json::from_str(raw).unwrap();
     let cwd = paths::Utf8PathBuf::try_from(std::env::current_dir().unwrap()).unwrap();
     let manifest = paths::AbsPathBuf::assert(cwd.join("Cargo.toml"));
     let manifest = ManifestPath::try_from(manifest).unwrap();
-    let env = parse_output_cargo_config_env(&manifest, stdout);
+    let env = cargo_config_env(&manifest, &Some(config));
     assert_eq!(env.get("CARGO_WORKSPACE_DIR").as_deref(), Some(cwd.join("").as_str()));
     assert_eq!(env.get("RELATIVE").as_deref(), Some(cwd.join("../relative").as_str()));
     assert_eq!(env.get("INVALID").as_deref(), Some("../relative"));
diff --git a/crates/project-model/src/lib.rs b/crates/project-model/src/lib.rs
index 436af64..3bf3d06 100644
--- a/crates/project-model/src/lib.rs
+++ b/crates/project-model/src/lib.rs
@@ -24,7 +24,7 @@
 
     use std::path::Path;
 
-    use crate::{ManifestPath, Sysroot};
+    use crate::{ManifestPath, Sysroot, cargo_config_file::CargoConfigFile};
 
     #[derive(Copy, Clone)]
     pub enum QueryConfig<'a> {
@@ -32,11 +32,12 @@
         Rustc(&'a Sysroot, &'a Path),
         /// Attempt to use cargo to query the desired information, honoring cargo configurations.
         /// If this fails, falls back to invoking `rustc` directly.
-        Cargo(&'a Sysroot, &'a ManifestPath),
+        Cargo(&'a Sysroot, &'a ManifestPath, &'a Option<CargoConfigFile>),
     }
 }
 
 mod build_dependencies;
+mod cargo_config_file;
 mod cargo_workspace;
 mod env;
 mod manifest_path;
diff --git a/crates/project-model/src/toolchain_info/rustc_cfg.rs b/crates/project-model/src/toolchain_info/rustc_cfg.rs
index a77f767..6e06e88 100644
--- a/crates/project-model/src/toolchain_info/rustc_cfg.rs
+++ b/crates/project-model/src/toolchain_info/rustc_cfg.rs
@@ -63,7 +63,7 @@
 ) -> anyhow::Result<String> {
     const RUSTC_ARGS: [&str; 2] = ["--print", "cfg"];
     let (sysroot, current_dir) = match config {
-        QueryConfig::Cargo(sysroot, cargo_toml) => {
+        QueryConfig::Cargo(sysroot, cargo_toml, _) => {
             let mut cmd = sysroot.tool(Tool::Cargo, cargo_toml.parent(), extra_env);
             cmd.args(["rustc", "-Z", "unstable-options"]).args(RUSTC_ARGS);
             if let Some(target) = target {
@@ -109,7 +109,7 @@
         let sysroot = Sysroot::empty();
         let manifest_path =
             ManifestPath::try_from(AbsPathBuf::assert(Utf8PathBuf::from(manifest_path))).unwrap();
-        let cfg = QueryConfig::Cargo(&sysroot, &manifest_path);
+        let cfg = QueryConfig::Cargo(&sysroot, &manifest_path, &None);
         assert_ne!(get(cfg, None, &FxHashMap::default()), vec![]);
     }
 
diff --git a/crates/project-model/src/toolchain_info/target_data_layout.rs b/crates/project-model/src/toolchain_info/target_data_layout.rs
index a4d0ec6..a28f468 100644
--- a/crates/project-model/src/toolchain_info/target_data_layout.rs
+++ b/crates/project-model/src/toolchain_info/target_data_layout.rs
@@ -20,7 +20,7 @@
             })
     };
     let (sysroot, current_dir) = match config {
-        QueryConfig::Cargo(sysroot, cargo_toml) => {
+        QueryConfig::Cargo(sysroot, cargo_toml, _) => {
             let mut cmd = sysroot.tool(Tool::Cargo, cargo_toml.parent(), extra_env);
             cmd.env("RUSTC_BOOTSTRAP", "1");
             cmd.args(["rustc", "-Z", "unstable-options"]).args(RUSTC_ARGS).args([
@@ -66,7 +66,7 @@
         let sysroot = Sysroot::empty();
         let manifest_path =
             ManifestPath::try_from(AbsPathBuf::assert(Utf8PathBuf::from(manifest_path))).unwrap();
-        let cfg = QueryConfig::Cargo(&sysroot, &manifest_path);
+        let cfg = QueryConfig::Cargo(&sysroot, &manifest_path, &None);
         assert!(get(cfg, None, &FxHashMap::default()).is_ok());
     }
 
diff --git a/crates/project-model/src/toolchain_info/target_tuple.rs b/crates/project-model/src/toolchain_info/target_tuple.rs
index f6ab853..9f12ede 100644
--- a/crates/project-model/src/toolchain_info/target_tuple.rs
+++ b/crates/project-model/src/toolchain_info/target_tuple.rs
@@ -5,7 +5,9 @@
 use rustc_hash::FxHashMap;
 use toolchain::Tool;
 
-use crate::{ManifestPath, Sysroot, toolchain_info::QueryConfig, utf8_stdout};
+use crate::{
+    Sysroot, cargo_config_file::CargoConfigFile, toolchain_info::QueryConfig, utf8_stdout,
+};
 
 /// For cargo, runs `cargo -Zunstable-options config get build.target` to get the configured project target(s).
 /// For rustc, runs `rustc --print -vV` to get the host target.
@@ -20,8 +22,8 @@
     }
 
     let (sysroot, current_dir) = match config {
-        QueryConfig::Cargo(sysroot, cargo_toml) => {
-            match cargo_config_build_target(cargo_toml, extra_env, sysroot) {
+        QueryConfig::Cargo(sysroot, cargo_toml, config_file) => {
+            match config_file.as_ref().and_then(cargo_config_build_target) {
                 Some(it) => return Ok(it),
                 None => (sysroot, cargo_toml.parent().as_ref()),
             }
@@ -50,30 +52,30 @@
     }
 }
 
-fn cargo_config_build_target(
-    cargo_toml: &ManifestPath,
-    extra_env: &FxHashMap<String, Option<String>>,
-    sysroot: &Sysroot,
-) -> Option<Vec<String>> {
-    let mut cmd = sysroot.tool(Tool::Cargo, cargo_toml.parent(), extra_env);
-    cmd.current_dir(cargo_toml.parent()).env("RUSTC_BOOTSTRAP", "1");
-    cmd.args(["-Z", "unstable-options", "config", "get", "build.target"]);
-    // if successful we receive `build.target = "target-tuple"`
-    // or `build.target = ["<target 1>", ..]`
-    // this might be `error: config value `build.target` is not set` in which case we
-    // don't wanna log the error
-    utf8_stdout(&mut cmd).and_then(parse_output_cargo_config_build_target).ok()
+fn cargo_config_build_target(config: &CargoConfigFile) -> Option<Vec<String>> {
+    match parse_json_cargo_config_build_target(config) {
+        Ok(v) => v,
+        Err(e) => {
+            tracing::debug!("Failed to discover cargo config build target {e:?}");
+            None
+        }
+    }
 }
 
 // Parses `"build.target = [target-tuple, target-tuple, ...]"` or `"build.target = "target-tuple"`
-fn parse_output_cargo_config_build_target(stdout: String) -> anyhow::Result<Vec<String>> {
-    let trimmed = stdout.trim_start_matches("build.target = ").trim_matches('"');
-
-    if !trimmed.starts_with('[') {
-        return Ok([trimmed.to_owned()].to_vec());
+fn parse_json_cargo_config_build_target(
+    config: &CargoConfigFile,
+) -> anyhow::Result<Option<Vec<String>>> {
+    let target = config.get("build").and_then(|v| v.as_object()).and_then(|m| m.get("target"));
+    match target {
+        Some(serde_json::Value::String(s)) => Ok(Some(vec![s.to_owned()])),
+        Some(v) => serde_json::from_value(v.clone())
+            .map(Option::Some)
+            .context("Failed to parse `build.target` as an array of target"),
+        // t`error: config value `build.target` is not set`, in which case we
+        // don't wanna log the error
+        None => Ok(None),
     }
-
-    serde_json::from_str(trimmed).context("Failed to parse `build.target` as an array of target")
 }
 
 #[cfg(test)]
@@ -90,7 +92,7 @@
         let sysroot = Sysroot::empty();
         let manifest_path =
             ManifestPath::try_from(AbsPathBuf::assert(Utf8PathBuf::from(manifest_path))).unwrap();
-        let cfg = QueryConfig::Cargo(&sysroot, &manifest_path);
+        let cfg = QueryConfig::Cargo(&sysroot, &manifest_path, &None);
         assert!(get(cfg, None, &FxHashMap::default()).is_ok());
     }
 
diff --git a/crates/project-model/src/toolchain_info/version.rs b/crates/project-model/src/toolchain_info/version.rs
index 91ba859..357053d 100644
--- a/crates/project-model/src/toolchain_info/version.rs
+++ b/crates/project-model/src/toolchain_info/version.rs
@@ -12,7 +12,7 @@
     extra_env: &FxHashMap<String, Option<String>>,
 ) -> Result<Option<Version>, anyhow::Error> {
     let (mut cmd, prefix) = match config {
-        QueryConfig::Cargo(sysroot, cargo_toml) => {
+        QueryConfig::Cargo(sysroot, cargo_toml, _) => {
             (sysroot.tool(Tool::Cargo, cargo_toml.parent(), extra_env), "cargo ")
         }
         QueryConfig::Rustc(sysroot, current_dir) => {
@@ -44,7 +44,7 @@
         let sysroot = Sysroot::empty();
         let manifest_path =
             ManifestPath::try_from(AbsPathBuf::assert(Utf8PathBuf::from(manifest_path))).unwrap();
-        let cfg = QueryConfig::Cargo(&sysroot, &manifest_path);
+        let cfg = QueryConfig::Cargo(&sysroot, &manifest_path, &None);
         assert!(get(cfg, &FxHashMap::default()).is_ok());
     }
 
diff --git a/crates/project-model/src/workspace.rs b/crates/project-model/src/workspace.rs
index 43db84b..801c2ab 100644
--- a/crates/project-model/src/workspace.rs
+++ b/crates/project-model/src/workspace.rs
@@ -25,11 +25,9 @@
     ProjectJson, ProjectManifest, RustSourceWorkspaceConfig, Sysroot, TargetData, TargetKind,
     WorkspaceBuildScripts,
     build_dependencies::BuildScriptOutput,
+    cargo_config_file,
     cargo_workspace::{CargoMetadataConfig, DepKind, PackageData, RustLibSource},
-    env::{
-        cargo_config_build_target_dir, cargo_config_env, inject_cargo_env,
-        inject_cargo_package_env, inject_rustc_tool_env,
-    },
+    env::{cargo_config_env, inject_cargo_env, inject_cargo_package_env, inject_rustc_tool_env},
     project_json::{Crate, CrateArrayIdx},
     sysroot::RustLibSrcWorkspace,
     toolchain_info::{QueryConfig, rustc_cfg, target_data_layout, target_tuple, version},
@@ -270,7 +268,9 @@
 
         tracing::info!(workspace = %cargo_toml, src_root = ?sysroot.rust_lib_src_root(), root = ?sysroot.root(), "Using sysroot");
         progress("querying project metadata".to_owned());
-        let toolchain_config = QueryConfig::Cargo(&sysroot, cargo_toml);
+        let config_file = cargo_config_file::read(cargo_toml, extra_env, &sysroot);
+        let config_file_ = config_file.clone();
+        let toolchain_config = QueryConfig::Cargo(&sysroot, cargo_toml, &config_file_);
         let targets =
             target_tuple::get(toolchain_config, target.as_deref(), extra_env).unwrap_or_default();
         let toolchain = version::get(toolchain_config, extra_env)
@@ -285,7 +285,7 @@
         let target_dir = config
             .target_dir
             .clone()
-            .or_else(|| cargo_config_build_target_dir(cargo_toml, extra_env, &sysroot))
+            .or_else(|| cargo_target_dir(cargo_toml, extra_env, &sysroot))
             .unwrap_or_else(|| workspace_dir.join("target").into());
 
         // We spawn a bunch of processes to query various information about the workspace's
@@ -397,7 +397,7 @@
                 )
             });
             let cargo_config_extra_env =
-                s.spawn(|| cargo_config_env(cargo_toml, extra_env, &sysroot));
+                s.spawn(move || cargo_config_env(cargo_toml, &config_file));
             thread::Result::Ok((
                 rustc_cfg.join()?,
                 data_layout.join()?,
@@ -476,9 +476,7 @@
         let target_dir = config
             .target_dir
             .clone()
-            .or_else(|| {
-                cargo_config_build_target_dir(project_json.manifest()?, &config.extra_env, &sysroot)
-            })
+            .or_else(|| cargo_target_dir(project_json.manifest()?, &config.extra_env, &sysroot))
             .unwrap_or_else(|| project_root.join("target").into());
 
         // We spawn a bunch of processes to query various information about the workspace's
@@ -554,7 +552,8 @@
             None => Sysroot::empty(),
         };
 
-        let query_config = QueryConfig::Cargo(&sysroot, detached_file);
+        let config_file = cargo_config_file::read(detached_file, &config.extra_env, &sysroot);
+        let query_config = QueryConfig::Cargo(&sysroot, detached_file, &config_file);
         let toolchain = version::get(query_config, &config.extra_env).ok().flatten();
         let targets = target_tuple::get(query_config, config.target.as_deref(), &config.extra_env)
             .unwrap_or_default();
@@ -563,7 +562,7 @@
         let target_dir = config
             .target_dir
             .clone()
-            .or_else(|| cargo_config_build_target_dir(detached_file, &config.extra_env, &sysroot))
+            .or_else(|| cargo_target_dir(detached_file, &config.extra_env, &sysroot))
             .unwrap_or_else(|| dir.join("target").into());
 
         let loaded_sysroot = sysroot.load_workspace(
@@ -600,8 +599,7 @@
         )
         .ok()
         .map(|(ws, error)| {
-            let cargo_config_extra_env =
-                cargo_config_env(detached_file, &config.extra_env, &sysroot);
+            let cargo_config_extra_env = cargo_config_env(detached_file, &config_file);
             (
                 CargoWorkspace::new(ws, detached_file.clone(), cargo_config_extra_env, false),
                 WorkspaceBuildScripts::default(),
@@ -1901,3 +1899,23 @@
         kind: "sysroot",
     }
 }
+
+fn cargo_target_dir(
+    manifest: &ManifestPath,
+    extra_env: &FxHashMap<String, Option<String>>,
+    sysroot: &Sysroot,
+) -> Option<Utf8PathBuf> {
+    let cargo = sysroot.tool(Tool::Cargo, manifest.parent(), extra_env);
+    let mut meta = cargo_metadata::MetadataCommand::new();
+    meta.cargo_path(cargo.get_program());
+    meta.manifest_path(manifest);
+    // `--no-deps` doesn't (over)write lockfiles as it doesn't do any package resolve.
+    // So we can use it to get `target_directory` before copying lockfiles
+    let mut other_options = vec!["--no-deps".to_owned()];
+    if manifest.is_rust_manifest() {
+        meta.env("RUSTC_BOOTSTRAP", "1");
+        other_options.push("-Zscript".to_owned());
+    }
+    meta.other_options(other_options);
+    meta.exec().map(|m| m.target_directory).ok()
+}