diff --git a/Cargo.lock b/Cargo.lock
index 992488f..1714216 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -109,6 +109,7 @@ dependencies = [
  "env_logger 0.10.2",
  "log",
  "relative-path",
+ "rustc_codegen_spirv-target-specs",
  "semver",
  "serde",
  "serde_json",
@@ -744,10 +745,16 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
 
+[[package]]
+name = "rustc_codegen_spirv-target-specs"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c89eaf493b3dfc730cda42a77014aad65e03213992c7afe0dff60a9f7d3dd94"
+
 [[package]]
 name = "rustc_codegen_spirv-types"
 version = "0.9.0"
-source = "git+https://github.com/Rust-GPU/rust-gpu.git?rev=6d7c1cd6c0920500a3fa8c01c23e7b74302c15c4#6d7c1cd6c0920500a3fa8c01c23e7b74302c15c4"
+source = "git+https://github.com/Rust-GPU/rust-gpu.git?rev=86fc48032c4cd4afb74f1d81ae859711d20386a1#86fc48032c4cd4afb74f1d81ae859711d20386a1"
 dependencies = [
  "rspirv",
  "serde",
@@ -897,7 +904,7 @@ dependencies = [
 [[package]]
 name = "spirv-builder"
 version = "0.9.0"
-source = "git+https://github.com/Rust-GPU/rust-gpu.git?rev=6d7c1cd6c0920500a3fa8c01c23e7b74302c15c4#6d7c1cd6c0920500a3fa8c01c23e7b74302c15c4"
+source = "git+https://github.com/Rust-GPU/rust-gpu.git?rev=86fc48032c4cd4afb74f1d81ae859711d20386a1#86fc48032c4cd4afb74f1d81ae859711d20386a1"
 dependencies = [
  "clap",
  "memchr",
diff --git a/Cargo.toml b/Cargo.toml
index c266e87..036c971 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,7 +13,7 @@ exclude = [
 resolver = "2"
 
 [workspace.dependencies]
-spirv-builder = { git = "https://github.com/Rust-GPU/rust-gpu.git", rev = "6d7c1cd6c0920500a3fa8c01c23e7b74302c15c4", default-features = false }
+spirv-builder = { git = "https://github.com/Rust-GPU/rust-gpu.git", rev = "86fc48032c4cd4afb74f1d81ae859711d20386a1", default-features = false }
 anyhow = "1.0.94"
 clap = { version = "4.5.37", features = ["derive"] }
 crossterm = "0.28.1"
@@ -29,6 +29,9 @@ test-log = "0.2.16"
 cargo_metadata = "0.19.2"
 semver = "1.0.26"
 
+# This crate MUST NEVER be upgraded, we need this particular "first" version to support old rust-gpu builds
+legacy_target_specs = { package = "rustc_codegen_spirv-target-specs", version = "0.9.0", features = ["include_str"] }
+
 [workspace.lints.rust]
 missing_docs = "warn"
 
diff --git a/crates/cargo-gpu/Cargo.toml b/crates/cargo-gpu/Cargo.toml
index 49ea815..9152876 100644
--- a/crates/cargo-gpu/Cargo.toml
+++ b/crates/cargo-gpu/Cargo.toml
@@ -14,6 +14,7 @@ default-run = "cargo-gpu"
 cargo_metadata.workspace = true
 anyhow.workspace = true
 spirv-builder = { workspace = true, features = ["clap", "watch"] }
+legacy_target_specs.workspace = true
 clap.workspace = true
 directories.workspace = true
 env_logger.workspace = true
diff --git a/crates/cargo-gpu/src/args.rs b/crates/cargo-gpu/src/args.rs
deleted file mode 100644
index 346d857..0000000
--- a/crates/cargo-gpu/src/args.rs
+++ /dev/null
@@ -1,106 +0,0 @@
-//! Args for building and installing.
-
-use spirv_builder::SpirvBuilder;
-
-/// All args for a build and install
-#[derive(clap::Parser, Debug, Clone, serde::Deserialize, serde::Serialize)]
-pub struct AllArgs {
-    /// build args
-    #[clap(flatten)]
-    pub build: BuildArgs,
-
-    /// install args
-    #[clap(flatten)]
-    pub install: InstallArgs,
-}
-
-/// Args for just a build
-#[derive(clap::Parser, Debug, Clone, serde::Deserialize, serde::Serialize)]
-pub struct BuildArgs {
-    /// Path to the output directory for the compiled shaders.
-    #[clap(long, short, default_value = "./")]
-    pub output_dir: std::path::PathBuf,
-
-    /// Watch the shader crate directory and automatically recompile on changes.
-    #[clap(long, short, action)]
-    pub watch: bool,
-
-    /// the flattened [`SpirvBuilder`]
-    #[clap(flatten)]
-    #[serde(flatten)]
-    pub spirv_builder: SpirvBuilder,
-
-    ///Renames the manifest.json file to the given name
-    #[clap(long, short, default_value = "manifest.json")]
-    pub manifest_file: String,
-}
-
-/// Args for an install
-#[derive(clap::Parser, Debug, Clone, serde::Deserialize, serde::Serialize)]
-#[expect(
-    clippy::struct_excessive_bools,
-    reason = "cmdline args have many bools"
-)]
-pub struct InstallArgs {
-    /// path to the `rustc_codegen_spirv` dylib
-    #[clap(long, hide(true), default_value = "INTERNALLY_SET")]
-    pub dylib_path: std::path::PathBuf,
-
-    /// Directory containing the shader crate to compile.
-    #[clap(long, default_value = "./")]
-    pub shader_crate: std::path::PathBuf,
-
-    #[expect(
-        clippy::doc_markdown,
-        reason = "The URL should appear literally like this. But Clippy wants a markdown clickable link"
-    )]
-    /// Source of `spirv-builder` dependency
-    /// Eg: "https://github.com/Rust-GPU/rust-gpu"
-    #[clap(long)]
-    pub spirv_builder_source: Option<String>,
-
-    /// Version of `spirv-builder` dependency.
-    /// * If `--spirv-builder-source` is not set, then this is assumed to be a crates.io semantic
-    ///   version such as "0.9.0".
-    /// * If `--spirv-builder-source` is set, then this is assumed to be a Git "commitsh", such
-    ///   as a Git commit hash or a Git tag, therefore anything that `git checkout` can resolve.
-    #[clap(long, verbatim_doc_comment)]
-    pub spirv_builder_version: Option<String>,
-
-    /// Force `rustc_codegen_spirv` to be rebuilt.
-    #[clap(long)]
-    pub rebuild_codegen: bool,
-
-    /// Assume "yes" to "Install Rust toolchain: [y/n]" prompt.
-    #[clap(long, action)]
-    pub auto_install_rust_toolchain: bool,
-
-    /// Clear target dir of `rustc_codegen_spirv` build after a successful build, saves about
-    /// 200MiB of disk space.
-    #[clap(long = "no-clear-target", default_value = "true", action = clap::ArgAction::SetFalse)]
-    pub clear_target: bool,
-
-    /// There is a tricky situation where a shader crate that depends on workspace config can have
-    /// a different `Cargo.lock` lockfile version from the the workspace's `Cargo.lock`. This can
-    /// prevent builds when an old Rust toolchain doesn't recognise the newer lockfile version.
-    ///
-    /// The ideal way to resolve this would be to match the shader crate's toolchain with the
-    /// workspace's toolchain. However, that is not always possible. Another solution is to
-    /// `exclude = [...]` the problematic shader crate from the workspace. This also may not be a
-    /// suitable solution if there are a number of shader crates all sharing similar config and
-    /// you don't want to have to copy/paste and maintain that config across all the shaders.
-    ///
-    /// So a somewhat hacky workaround is to have `cargo gpu` overwrite lockfile versions. Enabling
-    /// this flag will only come into effect if there are a mix of v3/v4 lockfiles. It will also
-    /// only overwrite versions for the duration of a build. It will attempt to return the versions
-    /// to their original values once the build is finished. However, of course, unexpected errors
-    /// can occur and the overwritten values can remain. Hence why this behaviour is not enabled by
-    /// default.
-    ///
-    /// This hack is possible because the change from v3 to v4 only involves a minor change to the
-    /// way source URLs are encoded. See these PRs for more details:
-    ///   * <https://github.com/rust-lang/cargo/pull/12280>
-    ///   * <https://github.com/rust-lang/cargo/pull/14595>
-    #[clap(long, action, verbatim_doc_comment)]
-    pub force_overwrite_lockfiles_v4_to_v3: bool,
-}
diff --git a/crates/cargo-gpu/src/build.rs b/crates/cargo-gpu/src/build.rs
index b77e5a2..1bb5078 100644
--- a/crates/cargo-gpu/src/build.rs
+++ b/crates/cargo-gpu/src/build.rs
@@ -2,13 +2,46 @@
 #![allow(clippy::unwrap_used, reason = "this is basically a test")]
 //! `cargo gpu build`, analogous to `cargo build`
 
-use crate::args::BuildArgs;
+use crate::install::Install;
 use crate::linkage::Linkage;
 use crate::lockfile::LockfileMismatchHandler;
-use crate::{install::Install, target_spec_dir};
 use anyhow::Context as _;
-use spirv_builder::{CompileResult, ModuleResult};
+use spirv_builder::{CompileResult, ModuleResult, SpirvBuilder};
 use std::io::Write as _;
+use std::path::PathBuf;
+
+/// Args for just a build
+#[derive(clap::Parser, Debug, Clone, serde::Deserialize, serde::Serialize)]
+pub struct BuildArgs {
+    /// Path to the output directory for the compiled shaders.
+    #[clap(long, short, default_value = "./")]
+    pub output_dir: PathBuf,
+
+    /// Watch the shader crate directory and automatically recompile on changes.
+    #[clap(long, short, action)]
+    pub watch: bool,
+
+    /// the flattened [`SpirvBuilder`]
+    #[clap(flatten)]
+    #[serde(flatten)]
+    pub spirv_builder: SpirvBuilder,
+
+    ///Renames the manifest.json file to the given name
+    #[clap(long, short, default_value = "manifest.json")]
+    pub manifest_file: String,
+}
+
+impl Default for BuildArgs {
+    #[inline]
+    fn default() -> Self {
+        Self {
+            output_dir: PathBuf::from("./"),
+            watch: false,
+            spirv_builder: SpirvBuilder::default(),
+            manifest_file: String::from("manifest.json"),
+        }
+    }
+}
 
 /// `cargo build` subcommands
 #[derive(Clone, clap::Parser, Debug, serde::Deserialize, serde::Serialize)]
@@ -19,54 +52,46 @@ pub struct Build {
 
     /// CLI args for configuring the build of the shader
     #[clap(flatten)]
-    pub build_args: BuildArgs,
+    pub build: BuildArgs,
 }
 
 impl Build {
     /// Entrypoint
     pub fn run(&mut self) -> anyhow::Result<()> {
-        let (rustc_codegen_spirv_location, toolchain_channel) = self.install.run()?;
+        let installed_backend = self.install.run()?;
 
         let _lockfile_mismatch_handler = LockfileMismatchHandler::new(
-            &self.install.spirv_install.shader_crate,
-            &toolchain_channel,
-            self.install
-                .spirv_install
-                .force_overwrite_lockfiles_v4_to_v3,
+            &self.install.shader_crate,
+            &installed_backend.toolchain_channel,
+            self.install.force_overwrite_lockfiles_v4_to_v3,
         )?;
 
-        let builder = &mut self.build_args.spirv_builder;
-        builder.rustc_codegen_spirv_location = Some(rustc_codegen_spirv_location);
-        builder.toolchain_overwrite = Some(toolchain_channel);
-        builder.path_to_crate = Some(self.install.spirv_install.shader_crate.clone());
-        builder.path_to_target_spec = Some(target_spec_dir()?.join(format!(
-            "{}.json",
-            builder.target.as_ref().context("expect target to be set")?
-        )));
+        let builder = &mut self.build.spirv_builder;
+        builder.path_to_crate = Some(self.install.shader_crate.clone());
+        installed_backend.configure_spirv_builder(builder)?;
 
         // Ensure the shader output dir exists
         log::debug!(
             "ensuring output-dir '{}' exists",
-            self.build_args.output_dir.display()
+            self.build.output_dir.display()
         );
-        std::fs::create_dir_all(&self.build_args.output_dir)?;
-        let canonicalized = self.build_args.output_dir.canonicalize()?;
-        log::debug!("canonicalized output dir: {canonicalized:?}");
-        self.build_args.output_dir = canonicalized;
+        std::fs::create_dir_all(&self.build.output_dir)?;
+        let canonicalized = self.build.output_dir.canonicalize()?;
+        log::debug!("canonicalized output dir: {}", canonicalized.display());
+        self.build.output_dir = canonicalized;
 
         // Ensure the shader crate exists
-        self.install.spirv_install.shader_crate =
-            self.install.spirv_install.shader_crate.canonicalize()?;
+        self.install.shader_crate = self.install.shader_crate.canonicalize()?;
         anyhow::ensure!(
-            self.install.spirv_install.shader_crate.exists(),
+            self.install.shader_crate.exists(),
             "shader crate '{}' does not exist. (Current dir is '{}')",
-            self.install.spirv_install.shader_crate.display(),
+            self.install.shader_crate.display(),
             std::env::current_dir()?.display()
         );
 
-        if self.build_args.watch {
+        if self.build.watch {
             let this = self.clone();
-            self.build_args
+            self.build
                 .spirv_builder
                 .watch(move |result, accept| {
                     let result1 = this.parse_compilation_result(&result);
@@ -79,9 +104,9 @@ impl Build {
         } else {
             crate::user_output!(
                 "Compiling shaders at {}...\n",
-                self.install.spirv_install.shader_crate.display()
+                self.install.shader_crate.display()
             );
-            let result = self.build_args.spirv_builder.build()?;
+            let result = self.build.spirv_builder.build()?;
             self.parse_compilation_result(&result)?;
         }
         Ok(())
@@ -104,7 +129,7 @@ impl Build {
             .into_iter()
             .map(|(entry, filepath)| -> anyhow::Result<Linkage> {
                 use relative_path::PathExt as _;
-                let path = self.build_args.output_dir.join(
+                let path = self.build.output_dir.join(
                     filepath
                         .file_name()
                         .context("Couldn't parse file name from shader module path")?,
@@ -114,10 +139,10 @@ impl Build {
                 log::debug!(
                     "linkage of {} relative to {}",
                     path.display(),
-                    self.install.spirv_install.shader_crate.display()
+                    self.install.shader_crate.display()
                 );
                 let spv_path = path
-                    .relative_to(&self.install.spirv_install.shader_crate)
+                    .relative_to(&self.install.shader_crate)
                     .map_or(path, |path_relative_to_shader_crate| {
                         path_relative_to_shader_crate.to_path("")
                     });
@@ -128,10 +153,7 @@ impl Build {
         linkage.sort();
 
         // Write the shader manifest json file
-        let manifest_path = self
-            .build_args
-            .output_dir
-            .join(&self.build_args.manifest_file);
+        let manifest_path = self.build.output_dir.join(&self.build.manifest_file);
         let json = serde_json::to_string_pretty(&linkage)?;
         let mut file = std::fs::File::create(&manifest_path).with_context(|| {
             format!(
@@ -176,8 +198,8 @@ mod test {
             command: Command::Build(build),
         } = Cli::parse_from(args)
         {
-            assert_eq!(shader_crate_path, build.install.spirv_install.shader_crate);
-            assert_eq!(output_dir, build.build_args.output_dir);
+            assert_eq!(shader_crate_path, build.install.shader_crate);
+            assert_eq!(output_dir, build.build.output_dir);
 
             // TODO:
             // For some reason running a full build (`build.run()`) inside tests fails on Windows.
diff --git a/crates/cargo-gpu/src/config.rs b/crates/cargo-gpu/src/config.rs
index ad86402..f1a67fa 100644
--- a/crates/cargo-gpu/src/config.rs
+++ b/crates/cargo-gpu/src/config.rs
@@ -14,28 +14,9 @@ impl Config {
 
     /// Convert CLI args to their serde JSON representation.
     fn cli_args_to_json(env_args: Vec<String>) -> anyhow::Result<serde_json::Value> {
-        let mut cli_args_json = serde_json::to_value(crate::build::Build::parse_from(env_args))?;
-
-        // Move `/install/spirv_install` to `/install`
-        let spirv_install = cli_args_json
-            .pointer("/install/spirv_install")
-            .context("`/install/spirv_install` not found in config")?
-            .clone();
-        *cli_args_json
-            .get_mut("install")
-            .context("`/install` not found in config")? = spirv_install;
-
-        let build = cli_args_json
-            .pointer("/build_args")
-            .context("`/build_args` not found in config")?
-            .clone();
-
-        // Move `/build_args` to `/build`
-        let object = cli_args_json.as_object_mut().context("!")?;
-        object.remove("build_args");
-        object.insert("build".to_owned(), build);
-
-        Ok(cli_args_json)
+        Ok(serde_json::to_value(crate::build::Build::parse_from(
+            env_args,
+        ))?)
     }
 
     /// Config for the `cargo gpu build` and `cargo gpu install` can be set in the shader crate's
@@ -47,30 +28,11 @@ impl Config {
     ) -> anyhow::Result<crate::build::Build> {
         let mut config = crate::metadata::Metadata::as_json(shader_crate_path)?;
 
-        env_args = env_args
-            .into_iter()
-            .filter(|arg| !(arg == "build" || arg == "install"))
-            .collect::<Vec<_>>();
+        env_args.retain(|arg| !(arg == "build" || arg == "install"));
         let cli_args_json = Self::cli_args_to_json(env_args)?;
-
         Self::json_merge(&mut config, cli_args_json, None)?;
 
-        let build = config
-            .get("build")
-            .context("`build` not found in merged configs")?
-            .clone();
-
-        let install = config
-            .get("install")
-            .context("`install` not found in merged configs")?
-            .clone();
-
-        let args = serde_json::from_value::<crate::build::Build>(serde_json::json!({
-            "build_args": build,
-            "install": {
-                "spirv_install": install
-            }
-        }))?;
+        let args = serde_json::from_value::<crate::build::Build>(config)?;
         Ok(args)
     }
 
@@ -140,8 +102,8 @@ mod test {
             ],
         )
         .unwrap();
-        assert!(!args.build_args.spirv_builder.release);
-        assert!(args.install.spirv_install.auto_install_rust_toolchain);
+        assert!(!args.build.spirv_builder.release);
+        assert!(args.install.auto_install_rust_toolchain);
     }
 
     #[test_log::test]
@@ -161,8 +123,8 @@ mod test {
         .unwrap();
 
         let args = Config::clap_command_with_cargo_config(&shader_crate_path, vec![]).unwrap();
-        assert!(!args.build_args.spirv_builder.release);
-        assert!(args.install.spirv_install.auto_install_rust_toolchain);
+        assert!(!args.build.spirv_builder.release);
+        assert!(args.install.auto_install_rust_toolchain);
     }
 
     fn update_cargo_output_dir() -> std::path::PathBuf {
@@ -186,15 +148,9 @@ mod test {
 
         let args = Config::clap_command_with_cargo_config(&shader_crate_path, vec![]).unwrap();
         if cfg!(target_os = "windows") {
-            assert_eq!(
-                args.build_args.output_dir,
-                std::path::Path::new("C:/the/moon")
-            );
+            assert_eq!(args.build.output_dir, std::path::Path::new("C:/the/moon"));
         } else {
-            assert_eq!(
-                args.build_args.output_dir,
-                std::path::Path::new("/the/moon")
-            );
+            assert_eq!(args.build.output_dir, std::path::Path::new("/the/moon"));
         }
     }
 
@@ -212,10 +168,7 @@ mod test {
             ],
         )
         .unwrap();
-        assert_eq!(
-            args.build_args.output_dir,
-            std::path::Path::new("/the/river")
-        );
+        assert_eq!(args.build.output_dir, std::path::Path::new("/the/river"));
     }
 
     #[test_log::test]
@@ -234,7 +187,7 @@ mod test {
 
         let args = Config::clap_command_with_cargo_config(&shader_crate_path, vec![]).unwrap();
         assert_eq!(
-            args.build_args.spirv_builder.capabilities,
+            args.build.spirv_builder.capabilities,
             vec![
                 spirv_builder::Capability::AtomicStorage,
                 spirv_builder::Capability::Matrix
@@ -256,6 +209,6 @@ mod test {
             ],
         )
         .unwrap();
-        assert_eq!(args.build_args.manifest_file, "mymanifest".to_owned());
+        assert_eq!(args.build.manifest_file, "mymanifest".to_owned());
     }
 }
diff --git a/crates/cargo-gpu/src/install.rs b/crates/cargo-gpu/src/install.rs
index 0b6f5fd..bd08e6d 100644
--- a/crates/cargo-gpu/src/install.rs
+++ b/crates/cargo-gpu/src/install.rs
@@ -1,29 +1,126 @@
 //! Install a dedicated per-shader crate that has the `rust-gpu` compiler in it.
 
-use crate::args::InstallArgs;
+use crate::legacy_target_specs::write_legacy_target_specs;
 use crate::spirv_source::{
-    get_channel_from_rustc_codegen_spirv_build_script, get_package_from_crate,
+    get_channel_from_rustc_codegen_spirv_build_script, query_metadata, FindPackage as _,
 };
-use crate::{cache_dir, spirv_source::SpirvSource, target_spec_dir};
+use crate::{cache_dir, spirv_source::SpirvSource};
 use anyhow::Context as _;
-use log::trace;
-use spirv_builder::TARGET_SPECS;
-use std::io::Write as _;
+use cargo_metadata::Metadata;
+use log::{info, trace};
+use spirv_builder::SpirvBuilder;
 use std::path::{Path, PathBuf};
 
-/// `cargo gpu install`
-#[derive(Clone, clap::Parser, Debug, serde::Deserialize, serde::Serialize)]
+/// Args for an install
+#[expect(
+    clippy::struct_excessive_bools,
+    reason = "cmdline args have many bools"
+)]
+#[derive(clap::Parser, Debug, Clone, serde::Deserialize, serde::Serialize)]
 pub struct Install {
-    /// CLI arguments for installing the Rust toolchain and components
-    #[clap(flatten)]
-    pub spirv_install: InstallArgs,
+    /// Directory containing the shader crate to compile.
+    #[clap(long, default_value = "./")]
+    pub shader_crate: PathBuf,
+
+    #[expect(
+        clippy::doc_markdown,
+        reason = "The URL should appear literally like this. But Clippy wants a markdown clickable link"
+    )]
+    /// Source of `spirv-builder` dependency
+    /// Eg: "https://github.com/Rust-GPU/rust-gpu"
+    #[clap(long)]
+    pub spirv_builder_source: Option<String>,
+
+    /// Version of `spirv-builder` dependency.
+    /// * If `--spirv-builder-source` is not set, then this is assumed to be a crates.io semantic
+    ///   version such as "0.9.0".
+    /// * If `--spirv-builder-source` is set, then this is assumed to be a Git "commitsh", such
+    ///   as a Git commit hash or a Git tag, therefore anything that `git checkout` can resolve.
+    #[clap(long, verbatim_doc_comment)]
+    pub spirv_builder_version: Option<String>,
+
+    /// Force `rustc_codegen_spirv` to be rebuilt.
+    #[clap(long)]
+    pub rebuild_codegen: bool,
+
+    /// Assume "yes" to "Install Rust toolchain: [y/n]" prompt.
+    #[clap(long, action)]
+    pub auto_install_rust_toolchain: bool,
+
+    /// Clear target dir of `rustc_codegen_spirv` build after a successful build, saves about
+    /// 200MiB of disk space.
+    #[clap(long = "no-clear-target", default_value = "true", action = clap::ArgAction::SetFalse)]
+    pub clear_target: bool,
+
+    /// There is a tricky situation where a shader crate that depends on workspace config can have
+    /// a different `Cargo.lock` lockfile version from the the workspace's `Cargo.lock`. This can
+    /// prevent builds when an old Rust toolchain doesn't recognise the newer lockfile version.
+    ///
+    /// The ideal way to resolve this would be to match the shader crate's toolchain with the
+    /// workspace's toolchain. However, that is not always possible. Another solution is to
+    /// `exclude = [...]` the problematic shader crate from the workspace. This also may not be a
+    /// suitable solution if there are a number of shader crates all sharing similar config and
+    /// you don't want to have to copy/paste and maintain that config across all the shaders.
+    ///
+    /// So a somewhat hacky workaround is to have `cargo gpu` overwrite lockfile versions. Enabling
+    /// this flag will only come into effect if there are a mix of v3/v4 lockfiles. It will also
+    /// only overwrite versions for the duration of a build. It will attempt to return the versions
+    /// to their original values once the build is finished. However, of course, unexpected errors
+    /// can occur and the overwritten values can remain. Hence why this behaviour is not enabled by
+    /// default.
+    ///
+    /// This hack is possible because the change from v3 to v4 only involves a minor change to the
+    /// way source URLs are encoded. See these PRs for more details:
+    ///   * <https://github.com/rust-lang/cargo/pull/12280>
+    ///   * <https://github.com/rust-lang/cargo/pull/14595>
+    #[clap(long, action, verbatim_doc_comment)]
+    pub force_overwrite_lockfiles_v4_to_v3: bool,
+}
+
+/// Represents a functional backend installation, whether it was cached or just installed.
+#[derive(Clone, Debug)]
+pub struct InstalledBackend {
+    /// path to the `rustc_codegen_spirv` dylib
+    pub rustc_codegen_spirv_location: PathBuf,
+    /// toolchain channel name
+    pub toolchain_channel: String,
+    /// directory with target-specs json files
+    pub target_spec_dir: PathBuf,
+}
+
+impl InstalledBackend {
+    /// Configures the supplied [`SpirvBuilder`]. `SpirvBuilder.target` must be set and must not change after calling this function.
+    pub fn configure_spirv_builder(&self, builder: &mut SpirvBuilder) -> anyhow::Result<()> {
+        builder.rustc_codegen_spirv_location = Some(self.rustc_codegen_spirv_location.clone());
+        builder.toolchain_overwrite = Some(self.toolchain_channel.clone());
+        builder.path_to_target_spec = Some(self.target_spec_dir.join(format!(
+            "{}.json",
+            builder.target.as_ref().context("expect target to be set")?
+        )));
+        Ok(())
+    }
+}
+
+impl Default for Install {
+    #[inline]
+    fn default() -> Self {
+        Self {
+            shader_crate: PathBuf::from("./"),
+            spirv_builder_source: None,
+            spirv_builder_version: None,
+            rebuild_codegen: false,
+            auto_install_rust_toolchain: false,
+            clear_target: true,
+            force_overwrite_lockfiles_v4_to_v3: false,
+        }
+    }
 }
 
 impl Install {
     /// Create the `rustc_codegen_spirv_dummy` crate that depends on `rustc_codegen_spirv`
     fn write_source_files(source: &SpirvSource, checkout: &Path) -> anyhow::Result<()> {
         // skip writing a dummy project if we use a local rust-gpu checkout
-        if matches!(source, SpirvSource::Path { .. }) {
+        if source.is_path() {
             return Ok(());
         }
         log::debug!(
@@ -47,10 +144,12 @@ impl Install {
                 }
                 SpirvSource::Git { url, rev } => format!("git = \"{url}\"\nrev = \"{rev}\""),
                 SpirvSource::Path {
-                    rust_gpu_repo_root: rust_gpu_path,
+                    rust_gpu_repo_root,
                     version,
                 } => {
-                    let mut new_path = rust_gpu_path.to_owned();
+                    // this branch is currently unreachable, as we just build `rustc_codegen_spirv` directly,
+                    // since we don't need the `dummy` crate to make cargo download it for us
+                    let mut new_path = rust_gpu_repo_root.to_owned();
                     new_path.push("crates/spirv-builder");
                     format!("path = \"{new_path}\"\nversion = \"{version}\"")
                 }
@@ -73,25 +172,75 @@ package = "rustc_codegen_spirv"
         Ok(())
     }
 
-    /// Add the target spec files to the crate.
-    fn write_target_spec_files(&self) -> anyhow::Result<()> {
-        for (filename, contents) in TARGET_SPECS {
-            let path = target_spec_dir()
-                .context("creating target spec dir")?
-                .join(filename);
-            if !path.is_file() || self.spirv_install.rebuild_codegen {
-                let mut file = std::fs::File::create(&path)
-                    .with_context(|| format!("creating file at [{}]", path.display()))?;
-                file.write_all(contents.as_bytes())
-                    .context("writing to file")?;
+    /// Copy spec files from one dir to another, assuming no subdirectories
+    fn copy_spec_files(src: &Path, dst: &Path) -> anyhow::Result<()> {
+        info!(
+            "Copy target specs from {:?} to {:?}",
+            src.display(),
+            dst.display()
+        );
+        std::fs::create_dir_all(dst)?;
+        let dir = std::fs::read_dir(src)?;
+        for dir_entry in dir {
+            let file = dir_entry?;
+            let file_path = file.path();
+            if file_path.is_file() {
+                std::fs::copy(file_path, dst.join(file.file_name()))?;
             }
         }
         Ok(())
     }
 
+    /// Add the target spec files to the crate.
+    fn update_spec_files(
+        &self,
+        source: &SpirvSource,
+        install_dir: &Path,
+        dummy_metadata: &Metadata,
+        skip_rebuild: bool,
+    ) -> anyhow::Result<PathBuf> {
+        let mut target_specs_dst = install_dir.join("target-specs");
+        if !skip_rebuild {
+            if let Ok(target_specs) =
+                dummy_metadata.find_package("rustc_codegen_spirv-target-specs")
+            {
+                let target_specs_src = target_specs
+                    .manifest_path
+                    .as_std_path()
+                    .parent()
+                    .and_then(|root| {
+                        let src = root.join("target-specs");
+                        src.is_dir().then_some(src)
+                    })
+                    .context("Could not find `target-specs` directory within `rustc_codegen_spirv-target-specs` dependency")?;
+                if source.is_path() {
+                    // skip copy
+                    target_specs_dst = target_specs_src;
+                } else {
+                    // copy over the target-specs
+                    Self::copy_spec_files(&target_specs_src, &target_specs_dst)
+                        .context("copying target-specs json files")?;
+                }
+            } else {
+                // use legacy target specs bundled with cargo gpu
+                if source.is_path() {
+                    // This is a stupid situation:
+                    // * We can't be certain that there are `target-specs` in the local checkout (there may be some in `spirv-builder`)
+                    // * We can't dump our legacy ones into the `install_dir`, as that would modify the local rust-gpu checkout
+                    // -> do what the old cargo gpu did, one global dir for all target specs
+                    // and hope parallel runs don't shred each other
+                    target_specs_dst = cache_dir()?.join("legacy-target-specs-for-local-checkout");
+                }
+                write_legacy_target_specs(&target_specs_dst, self.rebuild_codegen)?;
+            }
+        }
+
+        Ok(target_specs_dst)
+    }
+
     /// Install the binary pair and return the `(dylib_path, toolchain_channel)`.
     #[expect(clippy::too_many_lines, reason = "it's fine")]
-    pub fn run(&mut self) -> anyhow::Result<(PathBuf, String)> {
+    pub fn run(&self) -> anyhow::Result<InstalledBackend> {
         // Ensure the cache dir exists
         let cache_dir = cache_dir()?;
         log::info!("cache directory is '{}'", cache_dir.display());
@@ -100,12 +249,11 @@ package = "rustc_codegen_spirv"
         })?;
 
         let source = SpirvSource::new(
-            &self.spirv_install.shader_crate,
-            self.spirv_install.spirv_builder_source.as_deref(),
-            self.spirv_install.spirv_builder_version.as_deref(),
+            &self.shader_crate,
+            self.spirv_builder_source.as_deref(),
+            self.spirv_builder_version.as_deref(),
         )?;
-        let source_is_path = matches!(source, SpirvSource::Path { .. });
-        let checkout = source.install_dir()?;
+        let install_dir = source.install_dir()?;
 
         let dylib_filename = format!(
             "{}rustc_codegen_spirv{}",
@@ -114,60 +262,70 @@ package = "rustc_codegen_spirv"
         );
 
         let dest_dylib_path;
-        if source_is_path {
-            dest_dylib_path = checkout
+        if source.is_path() {
+            dest_dylib_path = install_dir
                 .join("target")
                 .join("release")
                 .join(&dylib_filename);
         } else {
-            dest_dylib_path = checkout.join(&dylib_filename);
+            dest_dylib_path = install_dir.join(&dylib_filename);
             if dest_dylib_path.is_file() {
                 log::info!(
                     "cargo-gpu artifacts are already installed in '{}'",
-                    checkout.display()
+                    install_dir.display()
                 );
             }
         }
 
-        let skip_rebuild =
-            !source_is_path && dest_dylib_path.is_file() && !self.spirv_install.rebuild_codegen;
+        // if `source` is a path, always rebuild
+        let skip_rebuild = !source.is_path() && dest_dylib_path.is_file() && !self.rebuild_codegen;
         if skip_rebuild {
             log::info!("...and so we are aborting the install step.");
         } else {
-            Self::write_source_files(&source, &checkout).context("writing source files")?;
+            Self::write_source_files(&source, &install_dir).context("writing source files")?;
         }
 
         // TODO cache toolchain channel in a file?
         log::debug!("resolving toolchain version to use");
-        let rustc_codegen_spirv = get_package_from_crate(&checkout, "rustc_codegen_spirv")
-            .context("get `rustc_codegen_spirv` metadata")?;
+        let dummy_metadata = query_metadata(&install_dir)
+            .context("resolving toolchain version: get `rustc_codegen_spirv_dummy` metadata")?;
+        let rustc_codegen_spirv = dummy_metadata.find_package("rustc_codegen_spirv").context(
+            "resolving toolchain version: expected a dependency on `rustc_codegen_spirv`",
+        )?;
         let toolchain_channel =
-            get_channel_from_rustc_codegen_spirv_build_script(&rustc_codegen_spirv)
-                .context("read toolchain from `rustc_codegen_spirv`'s build.rs")?;
+            get_channel_from_rustc_codegen_spirv_build_script(rustc_codegen_spirv).context(
+                "resolving toolchain version: read toolchain from `rustc_codegen_spirv`'s build.rs",
+            )?;
         log::info!("selected toolchain channel `{toolchain_channel:?}`");
 
+        log::debug!("update_spec_files");
+        let target_spec_dir = self
+            .update_spec_files(&source, &install_dir, &dummy_metadata, skip_rebuild)
+            .context("writing target spec files")?;
+
         if !skip_rebuild {
             log::debug!("ensure_toolchain_and_components_exist");
             crate::install_toolchain::ensure_toolchain_and_components_exist(
                 &toolchain_channel,
-                self.spirv_install.auto_install_rust_toolchain,
+                self.auto_install_rust_toolchain,
             )
             .context("ensuring toolchain and components exist")?;
 
             // to prevent unsupported version errors when using older toolchains
-            if !source_is_path {
+            if !source.is_path() {
                 log::debug!("remove Cargo.lock");
-                std::fs::remove_file(checkout.join("Cargo.lock")).context("remove Cargo.lock")?;
+                std::fs::remove_file(install_dir.join("Cargo.lock"))
+                    .context("remove Cargo.lock")?;
             }
 
             crate::user_output!("Compiling `rustc_codegen_spirv` from source {}\n", source,);
             let mut build_command = std::process::Command::new("cargo");
             build_command
-                .current_dir(&checkout)
+                .current_dir(&install_dir)
                 .arg(format!("+{toolchain_channel}"))
                 .args(["build", "--release"])
                 .env_remove("RUSTC");
-            if source_is_path {
+            if source.is_path() {
                 build_command.args(["-p", "rustc_codegen_spirv", "--lib"]);
             }
 
@@ -187,15 +345,15 @@ package = "rustc_codegen_spirv"
                 })
                 .context("running build command")?;
 
-            let target = checkout.join("target");
+            let target = install_dir.join("target");
             let dylib_path = target.join("release").join(&dylib_filename);
             if dylib_path.is_file() {
                 log::info!("successfully built {}", dylib_path.display());
-                if !source_is_path {
+                if !source.is_path() {
                     std::fs::rename(&dylib_path, &dest_dylib_path)
                         .context("renaming dylib path")?;
 
-                    if self.spirv_install.clear_target {
+                    if self.clear_target {
                         log::warn!("clearing target dir {}", target.display());
                         std::fs::remove_dir_all(&target).context("clearing target dir")?;
                     }
@@ -204,13 +362,12 @@ package = "rustc_codegen_spirv"
                 log::error!("could not find {}", dylib_path.display());
                 anyhow::bail!("`rustc_codegen_spirv` build failed");
             }
-
-            log::debug!("write_target_spec_files");
-            self.write_target_spec_files()
-                .context("writing target spec files")?;
         }
 
-        self.spirv_install.dylib_path.clone_from(&dest_dylib_path);
-        Ok((dest_dylib_path, toolchain_channel))
+        Ok(InstalledBackend {
+            rustc_codegen_spirv_location: dest_dylib_path,
+            toolchain_channel,
+            target_spec_dir,
+        })
     }
 }
diff --git a/crates/cargo-gpu/src/legacy_target_specs.rs b/crates/cargo-gpu/src/legacy_target_specs.rs
new file mode 100644
index 0000000..cabcdec
--- /dev/null
+++ b/crates/cargo-gpu/src/legacy_target_specs.rs
@@ -0,0 +1,26 @@
+//! Legacy target specs are spec jsons for versions before `rustc_codegen_spirv-target-specs`
+//! came bundled with them. Instead, cargo gpu needs to bundle these legacy spec files. Luckily,
+//! they are the same for all versions, as bundling target specs with the codegen backend was
+//! introduced before the first target spec update.
+
+use anyhow::Context as _;
+use log::info;
+use std::path::Path;
+
+/// Extract legacy target specs from our executable into some directory
+pub fn write_legacy_target_specs(target_spec_dir: &Path, rebuild: bool) -> anyhow::Result<()> {
+    info!(
+        "Writing legacy target specs to {}",
+        target_spec_dir.display()
+    );
+    std::fs::create_dir_all(target_spec_dir)?;
+    for (filename, contents) in legacy_target_specs::TARGET_SPECS {
+        let path = target_spec_dir.join(filename);
+        if !path.is_file() || rebuild {
+            std::fs::write(&path, contents.as_bytes()).with_context(|| {
+                format!("writing legacy target spec file at [{}]", path.display())
+            })?;
+        }
+    }
+    Ok(())
+}
diff --git a/crates/cargo-gpu/src/main.rs b/crates/cargo-gpu/src/main.rs
index 852766f..9aae856 100644
--- a/crates/cargo-gpu/src/main.rs
+++ b/crates/cargo-gpu/src/main.rs
@@ -55,11 +55,11 @@ use clap::Parser as _;
 use install::Install;
 use show::Show;
 
-mod args;
 mod build;
 mod config;
 mod install;
 mod install_toolchain;
+mod legacy_target_specs;
 mod linkage;
 mod lockfile;
 mod metadata;
@@ -128,8 +128,8 @@ fn run() -> anyhow::Result<()> {
 
     match cli.command {
         Command::Install(install) => {
-            let shader_crate_path = install.spirv_install.shader_crate;
-            let mut command =
+            let shader_crate_path = install.shader_crate;
+            let command =
                 config::Config::clap_command_with_cargo_config(&shader_crate_path, env_args)?;
             log::debug!(
                 "installing with final merged arguments: {:#?}",
@@ -138,16 +138,16 @@ fn run() -> anyhow::Result<()> {
             command.install.run()?;
         }
         Command::Build(build) => {
-            let shader_crate_path = build.install.spirv_install.shader_crate;
+            let shader_crate_path = build.install.shader_crate;
             let mut command =
                 config::Config::clap_command_with_cargo_config(&shader_crate_path, env_args)?;
             log::debug!("building with final merged arguments: {command:#?}");
 
-            if command.build_args.watch {
+            if command.build.watch {
                 //  When watching, do one normal run to setup the `manifest.json` file.
-                command.build_args.watch = false;
+                command.build.watch = false;
                 command.run()?;
-                command.build_args.watch = true;
+                command.build.watch = true;
                 command.run()?;
             } else {
                 command.run()?;
@@ -202,13 +202,6 @@ fn cache_dir() -> anyhow::Result<std::path::PathBuf> {
     })
 }
 
-/// Location of the target spec metadata files
-fn target_spec_dir() -> anyhow::Result<std::path::PathBuf> {
-    let dir = cache_dir()?.join("target-specs");
-    std::fs::create_dir_all(&dir)?;
-    Ok(dir)
-}
-
 /// Convenience function for internal use. Dumps all the CLI usage instructions. Useful for
 /// updating the README.
 fn dump_full_usage_for_readme() -> anyhow::Result<()> {
diff --git a/crates/cargo-gpu/src/spirv_source.rs b/crates/cargo-gpu/src/spirv_source.rs
index 32fde8d..28e93bd 100644
--- a/crates/cargo-gpu/src/spirv_source.rs
+++ b/crates/cargo-gpu/src/spirv_source.rs
@@ -7,7 +7,7 @@
 use anyhow::Context as _;
 use cargo_metadata::camino::{Utf8Path, Utf8PathBuf};
 use cargo_metadata::semver::Version;
-use cargo_metadata::{MetadataCommand, Package};
+use cargo_metadata::{Metadata, MetadataCommand, Package};
 use std::fs;
 use std::path::{Path, PathBuf};
 
@@ -96,8 +96,9 @@ impl SpirvSource {
 
     /// Look into the shader crate to get the version of `rust-gpu` it's using.
     pub fn get_rust_gpu_deps_from_shader(shader_crate_path: &Path) -> anyhow::Result<Self> {
-        let spirv_std_package = get_package_from_crate(shader_crate_path, "spirv-std")?;
-        let spirv_source = Self::parse_spirv_std_source_and_version(&spirv_std_package)?;
+        let crate_metadata = query_metadata(shader_crate_path)?;
+        let spirv_std_package = crate_metadata.find_package("spirv-std")?;
+        let spirv_source = Self::parse_spirv_std_source_and_version(spirv_std_package)?;
         log::debug!(
             "Parsed `SpirvSource` from crate `{}`: \
             {spirv_source:?}",
@@ -121,6 +122,11 @@ impl SpirvSource {
         }
     }
 
+    /// Returns true if self is a Path
+    pub const fn is_path(&self) -> bool {
+        matches!(self, Self::Path { .. })
+    }
+
     /// Parse a string like:
     ///   `spirv-std v0.9.0 (https://github.com/Rust-GPU/rust-gpu?rev=54f6978c#54f6978c) (*)`
     /// Which would return:
@@ -175,46 +181,41 @@ impl SpirvSource {
     }
 }
 
-/// Make sure shader crate path is absolute and canonical.
-fn crate_path_canonical(shader_crate_path: &Path) -> anyhow::Result<PathBuf> {
-    let mut canonical_path = shader_crate_path.to_path_buf();
-
-    if !canonical_path.is_absolute() {
-        let cwd = std::env::current_dir().context("no cwd")?;
-        canonical_path = cwd.join(canonical_path);
-    }
-    canonical_path = canonical_path
-        .canonicalize()
-        .context("could not get absolute path to shader crate")?;
-
-    if !canonical_path.is_dir() {
-        log::error!("{shader_crate_path:?} is not a directory, aborting");
-        anyhow::bail!("{shader_crate_path:?} is not a directory");
-    }
-    Ok(canonical_path)
-}
-
 /// get the Package metadata from some crate
-pub fn get_package_from_crate(crate_path: &Path, crate_name: &str) -> anyhow::Result<Package> {
-    let canonical_crate_path = crate_path_canonical(crate_path)?;
-
-    log::debug!(
-        "Running `cargo metadata` on `{}` to query for package `{crate_name}`",
-        canonical_crate_path.display()
-    );
+pub fn query_metadata(crate_path: &Path) -> anyhow::Result<Metadata> {
+    log::debug!("Running `cargo metadata` on `{}`", crate_path.display());
     let metadata = MetadataCommand::new()
-        .current_dir(&canonical_crate_path)
+        .current_dir(
+            &crate_path
+                .canonicalize()
+                .context("could not get absolute path to shader crate")?,
+        )
         .exec()?;
+    Ok(metadata)
+}
+
+/// implements [`Self::find_package`]
+pub trait FindPackage {
+    /// Search for a package or return a nice error
+    fn find_package(&self, crate_name: &str) -> anyhow::Result<&Package>;
+}
 
-    let Some(package) = metadata
-        .packages
-        .into_iter()
-        .find(|package| package.name.eq(crate_name))
-    else {
-        anyhow::bail!("`{crate_name}` not found in `Cargo.toml` at `{canonical_crate_path:?}`");
-    };
-    log::trace!("  found `{}` version `{}`", package.name, package.version);
-    Ok(package)
+impl FindPackage for Metadata {
+    fn find_package(&self, crate_name: &str) -> anyhow::Result<&Package> {
+        if let Some(package) = self
+            .packages
+            .iter()
+            .find(|package| package.name.eq(crate_name))
+        {
+            log::trace!("  found `{}` version `{}`", package.name, package.version);
+            Ok(package)
+        } else {
+            anyhow::bail!(
+                "`{crate_name}` not found in `Cargo.toml` at `{:?}`",
+                self.workspace_root
+            );
+        }
+    }
 }
 
 /// Parse the `rust-toolchain.toml` in the working tree of the checked-out version of the `rust-gpu` repo.
@@ -252,7 +253,7 @@ mod test {
             source,
             SpirvSource::Git {
                 url: "https://github.com/Rust-GPU/rust-gpu".to_owned(),
-                rev: "82a0f69008414f51d59184763146caa6850ac588".to_owned()
+                rev: "86fc48032c4cd4afb74f1d81ae859711d20386a1".to_owned()
             }
         );
     }
@@ -274,6 +275,6 @@ mod test {
             .to_str()
             .map(std::string::ToString::to_string)
             .unwrap();
-        assert_eq!("https___github_com_Rust-GPU_rust-gpu+82a0f690", &name);
+        assert_eq!("https___github_com_Rust-GPU_rust-gpu+86fc4803", &name);
     }
 }
diff --git a/crates/shader-crate-template/Cargo.lock b/crates/shader-crate-template/Cargo.lock
index 8a7a320..89add75 100644
--- a/crates/shader-crate-template/Cargo.lock
+++ b/crates/shader-crate-template/Cargo.lock
@@ -1,6 +1,6 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
-version = 3
+version = 4
 
 [[package]]
 name = "autocfg"
@@ -68,10 +68,11 @@ dependencies = [
 [[package]]
 name = "spirv-std"
 version = "0.9.0"
-source = "git+https://github.com/Rust-GPU/rust-gpu?rev=82a0f69#82a0f69008414f51d59184763146caa6850ac588"
+source = "git+https://github.com/Rust-GPU/rust-gpu?rev=86fc48032c4cd4afb74f1d81ae859711d20386a1#86fc48032c4cd4afb74f1d81ae859711d20386a1"
 dependencies = [
  "bitflags",
  "glam",
+ "libm",
  "num-traits",
  "spirv-std-macros",
  "spirv-std-types",
@@ -80,7 +81,7 @@ dependencies = [
 [[package]]
 name = "spirv-std-macros"
 version = "0.9.0"
-source = "git+https://github.com/Rust-GPU/rust-gpu?rev=82a0f69#82a0f69008414f51d59184763146caa6850ac588"
+source = "git+https://github.com/Rust-GPU/rust-gpu?rev=86fc48032c4cd4afb74f1d81ae859711d20386a1#86fc48032c4cd4afb74f1d81ae859711d20386a1"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -91,7 +92,7 @@ dependencies = [
 [[package]]
 name = "spirv-std-types"
 version = "0.9.0"
-source = "git+https://github.com/Rust-GPU/rust-gpu?rev=82a0f69#82a0f69008414f51d59184763146caa6850ac588"
+source = "git+https://github.com/Rust-GPU/rust-gpu?rev=86fc48032c4cd4afb74f1d81ae859711d20386a1#86fc48032c4cd4afb74f1d81ae859711d20386a1"
 
 [[package]]
 name = "syn"
diff --git a/crates/shader-crate-template/Cargo.toml b/crates/shader-crate-template/Cargo.toml
index faae1f2..9ed5a5a 100644
--- a/crates/shader-crate-template/Cargo.toml
+++ b/crates/shader-crate-template/Cargo.toml
@@ -9,7 +9,7 @@ crate-type = ["rlib", "cdylib"]
 # Dependencies for CPU and GPU code
 [dependencies]
 # TODO: use a simple crate version once v0.10.0 is released
-spirv-std = { git = "https://github.com/Rust-GPU/rust-gpu", rev = "82a0f69" }
+spirv-std = { git = "https://github.com/Rust-GPU/rust-gpu", rev = "86fc48032c4cd4afb74f1d81ae859711d20386a1" }
 
 # Dependencies for GPU code
 [target.'cfg(target_arch = "spirv")'.dependencies]