about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2024-11-29 01:35:20 +0000
committerbors <bors@rust-lang.org>2024-11-29 01:35:20 +0000
commitd6f88291f3ce96375683acc62d54710add042f98 (patch)
tree7506a74d7ef58cf8fb59e78f576b1f6fee3c950f
parentd53f0b1d8e261f2f3535f1cd165c714fc0b0b298 (diff)
parent77080d8eb359be41c696033d45e52f544ab9bd57 (diff)
downloadrust-d6f88291f3ce96375683acc62d54710add042f98.tar.gz
rust-d6f88291f3ce96375683acc62d54710add042f98.zip
Auto merge of #133409 - RalfJung:target-consistency, r=lcnr
ensure JSON-defined targets are consistent

We have a `check_consistency` check that ensures some invariants which (presumably) the rest of the compiler relies on. However, JSON targets can easily be written in a way that violates those invariants. So this PR applies the same consistency check to JSON targets that we already enforce for built-in targets.

I have converted many of the assertions in that function to new macros that show a nice error instead of a panic; if people are okay with the general approach here, I can do that for the rest of the checks as well.
-rw-r--r--compiler/rustc_target/src/spec/json.rs798
-rw-r--r--compiler/rustc_target/src/spec/mod.rs1104
-rw-r--r--compiler/rustc_target/src/spec/tests/tests_impl.rs208
-rw-r--r--tests/ui/codegen/mismatched-data-layout.json2
-rw-r--r--tests/ui/codegen/mismatched-data-layouts.stderr2
5 files changed, 1139 insertions, 975 deletions
diff --git a/compiler/rustc_target/src/spec/json.rs b/compiler/rustc_target/src/spec/json.rs
new file mode 100644
index 00000000000..206766325aa
--- /dev/null
+++ b/compiler/rustc_target/src/spec/json.rs
@@ -0,0 +1,798 @@
+use std::borrow::Cow;
+use std::collections::BTreeMap;
+use std::str::FromStr;
+
+use serde_json::Value;
+
+use super::{Target, TargetKind, TargetOptions, TargetWarnings};
+use crate::json::{Json, ToJson};
+
+impl Target {
+    /// Loads a target descriptor from a JSON object.
+    pub fn from_json(obj: Json) -> Result<(Target, TargetWarnings), String> {
+        // While ugly, this code must remain this way to retain
+        // compatibility with existing JSON fields and the internal
+        // expected naming of the Target and TargetOptions structs.
+        // To ensure compatibility is retained, the built-in targets
+        // are round-tripped through this code to catch cases where
+        // the JSON parser is not updated to match the structs.
+
+        let mut obj = match obj {
+            Value::Object(obj) => obj,
+            _ => return Err("Expected JSON object for target")?,
+        };
+
+        let mut get_req_field = |name: &str| {
+            obj.remove(name)
+                .and_then(|j| j.as_str().map(str::to_string))
+                .ok_or_else(|| format!("Field {name} in target specification is required"))
+        };
+
+        let mut base = Target {
+            llvm_target: get_req_field("llvm-target")?.into(),
+            metadata: Default::default(),
+            pointer_width: get_req_field("target-pointer-width")?
+                .parse::<u32>()
+                .map_err(|_| "target-pointer-width must be an integer".to_string())?,
+            data_layout: get_req_field("data-layout")?.into(),
+            arch: get_req_field("arch")?.into(),
+            options: Default::default(),
+        };
+
+        // FIXME: This doesn't properly validate anything and just ignores the data if it's invalid.
+        // That's okay for now, the only use of this is when generating docs, which we don't do for
+        // custom targets.
+        if let Some(Json::Object(mut metadata)) = obj.remove("metadata") {
+            base.metadata.description = metadata
+                .remove("description")
+                .and_then(|desc| desc.as_str().map(|desc| desc.to_owned().into()));
+            base.metadata.tier = metadata
+                .remove("tier")
+                .and_then(|tier| tier.as_u64())
+                .filter(|tier| (1..=3).contains(tier));
+            base.metadata.host_tools =
+                metadata.remove("host_tools").and_then(|host| host.as_bool());
+            base.metadata.std = metadata.remove("std").and_then(|host| host.as_bool());
+        }
+
+        let mut incorrect_type = vec![];
+
+        macro_rules! key {
+            ($key_name:ident) => ( {
+                let name = (stringify!($key_name)).replace("_", "-");
+                if let Some(s) = obj.remove(&name).and_then(|s| s.as_str().map(str::to_string).map(Cow::from)) {
+                    base.$key_name = s;
+                }
+            } );
+            ($key_name:ident = $json_name:expr) => ( {
+                let name = $json_name;
+                if let Some(s) = obj.remove(name).and_then(|s| s.as_str().map(str::to_string).map(Cow::from)) {
+                    base.$key_name = s;
+                }
+            } );
+            ($key_name:ident, bool) => ( {
+                let name = (stringify!($key_name)).replace("_", "-");
+                if let Some(s) = obj.remove(&name).and_then(|b| b.as_bool()) {
+                    base.$key_name = s;
+                }
+            } );
+            ($key_name:ident = $json_name:expr, bool) => ( {
+                let name = $json_name;
+                if let Some(s) = obj.remove(name).and_then(|b| b.as_bool()) {
+                    base.$key_name = s;
+                }
+            } );
+            ($key_name:ident, u32) => ( {
+                let name = (stringify!($key_name)).replace("_", "-");
+                if let Some(s) = obj.remove(&name).and_then(|b| b.as_u64()) {
+                    if s < 1 || s > 5 {
+                        return Err("Not a valid DWARF version number".into());
+                    }
+                    base.$key_name = s as u32;
+                }
+            } );
+            ($key_name:ident, Option<bool>) => ( {
+                let name = (stringify!($key_name)).replace("_", "-");
+                if let Some(s) = obj.remove(&name).and_then(|b| b.as_bool()) {
+                    base.$key_name = Some(s);
+                }
+            } );
+            ($key_name:ident, Option<u64>) => ( {
+                let name = (stringify!($key_name)).replace("_", "-");
+                if let Some(s) = obj.remove(&name).and_then(|b| b.as_u64()) {
+                    base.$key_name = Some(s);
+                }
+            } );
+            ($key_name:ident, MergeFunctions) => ( {
+                let name = (stringify!($key_name)).replace("_", "-");
+                obj.remove(&name).and_then(|o| o.as_str().and_then(|s| {
+                    match s.parse::<super::MergeFunctions>() {
+                        Ok(mergefunc) => base.$key_name = mergefunc,
+                        _ => return Some(Err(format!("'{}' is not a valid value for \
+                                                      merge-functions. Use 'disabled', \
+                                                      'trampolines', or 'aliases'.",
+                                                      s))),
+                    }
+                    Some(Ok(()))
+                })).unwrap_or(Ok(()))
+            } );
+            ($key_name:ident, RelocModel) => ( {
+                let name = (stringify!($key_name)).replace("_", "-");
+                obj.remove(&name).and_then(|o| o.as_str().and_then(|s| {
+                    match s.parse::<super::RelocModel>() {
+                        Ok(relocation_model) => base.$key_name = relocation_model,
+                        _ => return Some(Err(format!("'{}' is not a valid relocation model. \
+                                                      Run `rustc --print relocation-models` to \
+                                                      see the list of supported values.", s))),
+                    }
+                    Some(Ok(()))
+                })).unwrap_or(Ok(()))
+            } );
+            ($key_name:ident, CodeModel) => ( {
+                let name = (stringify!($key_name)).replace("_", "-");
+                obj.remove(&name).and_then(|o| o.as_str().and_then(|s| {
+                    match s.parse::<super::CodeModel>() {
+                        Ok(code_model) => base.$key_name = Some(code_model),
+                        _ => return Some(Err(format!("'{}' is not a valid code model. \
+                                                      Run `rustc --print code-models` to \
+                                                      see the list of supported values.", s))),
+                    }
+                    Some(Ok(()))
+                })).unwrap_or(Ok(()))
+            } );
+            ($key_name:ident, TlsModel) => ( {
+                let name = (stringify!($key_name)).replace("_", "-");
+                obj.remove(&name).and_then(|o| o.as_str().and_then(|s| {
+                    match s.parse::<super::TlsModel>() {
+                        Ok(tls_model) => base.$key_name = tls_model,
+                        _ => return Some(Err(format!("'{}' is not a valid TLS model. \
+                                                      Run `rustc --print tls-models` to \
+                                                      see the list of supported values.", s))),
+                    }
+                    Some(Ok(()))
+                })).unwrap_or(Ok(()))
+            } );
+            ($key_name:ident, SmallDataThresholdSupport) => ( {
+                obj.remove("small-data-threshold-support").and_then(|o| o.as_str().and_then(|s| {
+                    match s.parse::<super::SmallDataThresholdSupport>() {
+                        Ok(support) => base.small_data_threshold_support = support,
+                        _ => return Some(Err(format!("'{s}' is not a valid value for small-data-threshold-support."))),
+                    }
+                    Some(Ok(()))
+                })).unwrap_or(Ok(()))
+            } );
+            ($key_name:ident, PanicStrategy) => ( {
+                let name = (stringify!($key_name)).replace("_", "-");
+                obj.remove(&name).and_then(|o| o.as_str().and_then(|s| {
+                    match s {
+                        "unwind" => base.$key_name = super::PanicStrategy::Unwind,
+                        "abort" => base.$key_name = super::PanicStrategy::Abort,
+                        _ => return Some(Err(format!("'{}' is not a valid value for \
+                                                      panic-strategy. Use 'unwind' or 'abort'.",
+                                                     s))),
+                }
+                Some(Ok(()))
+            })).unwrap_or(Ok(()))
+            } );
+            ($key_name:ident, RelroLevel) => ( {
+                let name = (stringify!($key_name)).replace("_", "-");
+                obj.remove(&name).and_then(|o| o.as_str().and_then(|s| {
+                    match s.parse::<super::RelroLevel>() {
+                        Ok(level) => base.$key_name = level,
+                        _ => return Some(Err(format!("'{}' is not a valid value for \
+                                                      relro-level. Use 'full', 'partial, or 'off'.",
+                                                      s))),
+                    }
+                    Some(Ok(()))
+                })).unwrap_or(Ok(()))
+            } );
+            ($key_name:ident, Option<SymbolVisibility>) => ( {
+                let name = (stringify!($key_name)).replace("_", "-");
+                obj.remove(&name).and_then(|o| o.as_str().and_then(|s| {
+                    match s.parse::<super::SymbolVisibility>() {
+                        Ok(level) => base.$key_name = Some(level),
+                        _ => return Some(Err(format!("'{}' is not a valid value for \
+                                                      symbol-visibility. Use 'hidden', 'protected, or 'interposable'.",
+                                                      s))),
+                    }
+                    Some(Ok(()))
+                })).unwrap_or(Ok(()))
+            } );
+            ($key_name:ident, DebuginfoKind) => ( {
+                let name = (stringify!($key_name)).replace("_", "-");
+                obj.remove(&name).and_then(|o| o.as_str().and_then(|s| {
+                    match s.parse::<super::DebuginfoKind>() {
+                        Ok(level) => base.$key_name = level,
+                        _ => return Some(Err(
+                            format!("'{s}' is not a valid value for debuginfo-kind. Use 'dwarf', \
+                                  'dwarf-dsym' or 'pdb'.")
+                        )),
+                    }
+                    Some(Ok(()))
+                })).unwrap_or(Ok(()))
+            } );
+            ($key_name:ident, SplitDebuginfo) => ( {
+                let name = (stringify!($key_name)).replace("_", "-");
+                obj.remove(&name).and_then(|o| o.as_str().and_then(|s| {
+                    match s.parse::<super::SplitDebuginfo>() {
+                        Ok(level) => base.$key_name = level,
+                        _ => return Some(Err(format!("'{}' is not a valid value for \
+                                                      split-debuginfo. Use 'off' or 'dsymutil'.",
+                                                      s))),
+                    }
+                    Some(Ok(()))
+                })).unwrap_or(Ok(()))
+            } );
+            ($key_name:ident, list) => ( {
+                let name = (stringify!($key_name)).replace("_", "-");
+                if let Some(j) = obj.remove(&name) {
+                    if let Some(v) = j.as_array() {
+                        base.$key_name = v.iter()
+                            .map(|a| a.as_str().unwrap().to_string().into())
+                            .collect();
+                    } else {
+                        incorrect_type.push(name)
+                    }
+                }
+            } );
+            ($key_name:ident, opt_list) => ( {
+                let name = (stringify!($key_name)).replace("_", "-");
+                if let Some(j) = obj.remove(&name) {
+                    if let Some(v) = j.as_array() {
+                        base.$key_name = Some(v.iter()
+                            .map(|a| a.as_str().unwrap().to_string().into())
+                            .collect());
+                    } else {
+                        incorrect_type.push(name)
+                    }
+                }
+            } );
+            ($key_name:ident, fallible_list) => ( {
+                let name = (stringify!($key_name)).replace("_", "-");
+                obj.remove(&name).and_then(|j| {
+                    if let Some(v) = j.as_array() {
+                        match v.iter().map(|a| FromStr::from_str(a.as_str().unwrap())).collect() {
+                            Ok(l) => { base.$key_name = l },
+                            // FIXME: `fallible_list` can't re-use the `key!` macro for list
+                            // elements and the error messages from that macro, so it has a bad
+                            // generic message instead
+                            Err(_) => return Some(Err(
+                                format!("`{:?}` is not a valid value for `{}`", j, name)
+                            )),
+                        }
+                    } else {
+                        incorrect_type.push(name)
+                    }
+                    Some(Ok(()))
+                }).unwrap_or(Ok(()))
+            } );
+            ($key_name:ident, optional) => ( {
+                let name = (stringify!($key_name)).replace("_", "-");
+                if let Some(o) = obj.remove(&name) {
+                    base.$key_name = o
+                        .as_str()
+                        .map(|s| s.to_string().into());
+                }
+            } );
+            ($key_name:ident = $json_name:expr, LldFlavor) => ( {
+                let name = $json_name;
+                obj.remove(name).and_then(|o| o.as_str().and_then(|s| {
+                    if let Some(flavor) = super::LldFlavor::from_str(&s) {
+                        base.$key_name = flavor;
+                    } else {
+                        return Some(Err(format!(
+                            "'{}' is not a valid value for lld-flavor. \
+                             Use 'darwin', 'gnu', 'link' or 'wasm'.",
+                            s)))
+                    }
+                    Some(Ok(()))
+                })).unwrap_or(Ok(()))
+            } );
+            ($key_name:ident = $json_name:expr, LinkerFlavorCli) => ( {
+                let name = $json_name;
+                obj.remove(name).and_then(|o| o.as_str().and_then(|s| {
+                    match super::LinkerFlavorCli::from_str(s) {
+                        Some(linker_flavor) => base.$key_name = linker_flavor,
+                        _ => return Some(Err(format!("'{}' is not a valid value for linker-flavor. \
+                                                      Use {}", s, super::LinkerFlavorCli::one_of()))),
+                    }
+                    Some(Ok(()))
+                })).unwrap_or(Ok(()))
+            } );
+            ($key_name:ident, StackProbeType) => ( {
+                let name = (stringify!($key_name)).replace("_", "-");
+                obj.remove(&name).and_then(|o| match super::StackProbeType::from_json(&o) {
+                    Ok(v) => {
+                        base.$key_name = v;
+                        Some(Ok(()))
+                    },
+                    Err(s) => Some(Err(
+                        format!("`{:?}` is not a valid value for `{}`: {}", o, name, s)
+                    )),
+                }).unwrap_or(Ok(()))
+            } );
+            ($key_name:ident, SanitizerSet) => ( {
+                let name = (stringify!($key_name)).replace("_", "-");
+                if let Some(o) = obj.remove(&name) {
+                    if let Some(a) = o.as_array() {
+                        for s in a {
+                            use super::SanitizerSet;
+                            base.$key_name |= match s.as_str() {
+                                Some("address") => SanitizerSet::ADDRESS,
+                                Some("cfi") => SanitizerSet::CFI,
+                                Some("dataflow") => SanitizerSet::DATAFLOW,
+                                Some("kcfi") => SanitizerSet::KCFI,
+                                Some("kernel-address") => SanitizerSet::KERNELADDRESS,
+                                Some("leak") => SanitizerSet::LEAK,
+                                Some("memory") => SanitizerSet::MEMORY,
+                                Some("memtag") => SanitizerSet::MEMTAG,
+                                Some("safestack") => SanitizerSet::SAFESTACK,
+                                Some("shadow-call-stack") => SanitizerSet::SHADOWCALLSTACK,
+                                Some("thread") => SanitizerSet::THREAD,
+                                Some("hwaddress") => SanitizerSet::HWADDRESS,
+                                Some(s) => return Err(format!("unknown sanitizer {}", s)),
+                                _ => return Err(format!("not a string: {:?}", s)),
+                            };
+                        }
+                    } else {
+                        incorrect_type.push(name)
+                    }
+                }
+                Ok::<(), String>(())
+            } );
+            ($key_name:ident, link_self_contained_components) => ( {
+                // Skeleton of what needs to be parsed:
+                //
+                // ```
+                // $name: {
+                //     "components": [
+                //         <array of strings>
+                //     ]
+                // }
+                // ```
+                let name = (stringify!($key_name)).replace("_", "-");
+                if let Some(o) = obj.remove(&name) {
+                    if let Some(o) = o.as_object() {
+                        let component_array = o.get("components")
+                            .ok_or_else(|| format!("{name}: expected a \
+                                JSON object with a `components` field."))?;
+                        let component_array = component_array.as_array()
+                            .ok_or_else(|| format!("{name}.components: expected a JSON array"))?;
+                        let mut components = super::LinkSelfContainedComponents::empty();
+                        for s in component_array {
+                            components |= match s.as_str() {
+                                Some(s) => {
+                                    super::LinkSelfContainedComponents::from_str(s)
+                                        .ok_or_else(|| format!("unknown \
+                                        `-Clink-self-contained` component: {s}"))?
+                                },
+                                _ => return Err(format!("not a string: {:?}", s)),
+                            };
+                        }
+                        base.$key_name = super::LinkSelfContainedDefault::WithComponents(components);
+                    } else {
+                        incorrect_type.push(name)
+                    }
+                }
+                Ok::<(), String>(())
+            } );
+            ($key_name:ident = $json_name:expr, link_self_contained_backwards_compatible) => ( {
+                let name = $json_name;
+                obj.remove(name).and_then(|o| o.as_str().and_then(|s| {
+                    match s.parse::<super::LinkSelfContainedDefault>() {
+                        Ok(lsc_default) => base.$key_name = lsc_default,
+                        _ => return Some(Err(format!("'{}' is not a valid `-Clink-self-contained` default. \
+                                                      Use 'false', 'true', 'musl' or 'mingw'", s))),
+                    }
+                    Some(Ok(()))
+                })).unwrap_or(Ok(()))
+            } );
+            ($key_name:ident = $json_name:expr, link_objects) => ( {
+                let name = $json_name;
+                if let Some(val) = obj.remove(name) {
+                    let obj = val.as_object().ok_or_else(|| format!("{}: expected a \
+                        JSON object with fields per CRT object kind.", name))?;
+                    let mut args = super::CrtObjects::new();
+                    for (k, v) in obj {
+                        let kind = super::LinkOutputKind::from_str(&k).ok_or_else(|| {
+                            format!("{}: '{}' is not a valid value for CRT object kind. \
+                                     Use '(dynamic,static)-(nopic,pic)-exe' or \
+                                     '(dynamic,static)-dylib' or 'wasi-reactor-exe'", name, k)
+                        })?;
+
+                        let v = v.as_array().ok_or_else(||
+                            format!("{}.{}: expected a JSON array", name, k)
+                        )?.iter().enumerate()
+                            .map(|(i,s)| {
+                                let s = s.as_str().ok_or_else(||
+                                    format!("{}.{}[{}]: expected a JSON string", name, k, i))?;
+                                Ok(s.to_string().into())
+                            })
+                            .collect::<Result<Vec<_>, String>>()?;
+
+                        args.insert(kind, v);
+                    }
+                    base.$key_name = args;
+                }
+            } );
+            ($key_name:ident = $json_name:expr, link_args) => ( {
+                let name = $json_name;
+                if let Some(val) = obj.remove(name) {
+                    let obj = val.as_object().ok_or_else(|| format!("{}: expected a \
+                        JSON object with fields per linker-flavor.", name))?;
+                    let mut args = super::LinkArgsCli::new();
+                    for (k, v) in obj {
+                        let flavor = super::LinkerFlavorCli::from_str(&k).ok_or_else(|| {
+                            format!("{}: '{}' is not a valid value for linker-flavor. \
+                                     Use 'em', 'gcc', 'ld' or 'msvc'", name, k)
+                        })?;
+
+                        let v = v.as_array().ok_or_else(||
+                            format!("{}.{}: expected a JSON array", name, k)
+                        )?.iter().enumerate()
+                            .map(|(i,s)| {
+                                let s = s.as_str().ok_or_else(||
+                                    format!("{}.{}[{}]: expected a JSON string", name, k, i))?;
+                                Ok(s.to_string().into())
+                            })
+                            .collect::<Result<Vec<_>, String>>()?;
+
+                        args.insert(flavor, v);
+                    }
+                    base.$key_name = args;
+                }
+            } );
+            ($key_name:ident, env) => ( {
+                let name = (stringify!($key_name)).replace("_", "-");
+                if let Some(o) = obj.remove(&name) {
+                    if let Some(a) = o.as_array() {
+                        for o in a {
+                            if let Some(s) = o.as_str() {
+                                if let [k, v] = *s.split('=').collect::<Vec<_>>() {
+                                    base.$key_name
+                                        .to_mut()
+                                        .push((k.to_string().into(), v.to_string().into()))
+                                }
+                            }
+                        }
+                    } else {
+                        incorrect_type.push(name)
+                    }
+                }
+            } );
+            ($key_name:ident, target_families) => ( {
+                if let Some(value) = obj.remove("target-family") {
+                    if let Some(v) = value.as_array() {
+                        base.$key_name = v.iter()
+                            .map(|a| a.as_str().unwrap().to_string().into())
+                            .collect();
+                    } else if let Some(v) = value.as_str() {
+                        base.$key_name = vec![v.to_string().into()].into();
+                    }
+                }
+            } );
+            ($key_name:ident, Conv) => ( {
+                let name = (stringify!($key_name)).replace("_", "-");
+                obj.remove(&name).and_then(|o| o.as_str().and_then(|s| {
+                    match super::Conv::from_str(s) {
+                        Ok(c) => {
+                            base.$key_name = c;
+                            Some(Ok(()))
+                        }
+                        Err(e) => Some(Err(e))
+                    }
+                })).unwrap_or(Ok(()))
+            } );
+        }
+
+        if let Some(j) = obj.remove("target-endian") {
+            if let Some(s) = j.as_str() {
+                base.endian = s.parse()?;
+            } else {
+                incorrect_type.push("target-endian".into())
+            }
+        }
+
+        if let Some(fp) = obj.remove("frame-pointer") {
+            if let Some(s) = fp.as_str() {
+                base.frame_pointer = s
+                    .parse()
+                    .map_err(|()| format!("'{s}' is not a valid value for frame-pointer"))?;
+            } else {
+                incorrect_type.push("frame-pointer".into())
+            }
+        }
+
+        key!(c_int_width = "target-c-int-width");
+        key!(c_enum_min_bits, Option<u64>); // if None, matches c_int_width
+        key!(os);
+        key!(env);
+        key!(abi);
+        key!(vendor);
+        key!(linker, optional);
+        key!(linker_flavor_json = "linker-flavor", LinkerFlavorCli)?;
+        key!(lld_flavor_json = "lld-flavor", LldFlavor)?;
+        key!(linker_is_gnu_json = "linker-is-gnu", bool);
+        key!(pre_link_objects = "pre-link-objects", link_objects);
+        key!(post_link_objects = "post-link-objects", link_objects);
+        key!(pre_link_objects_self_contained = "pre-link-objects-fallback", link_objects);
+        key!(post_link_objects_self_contained = "post-link-objects-fallback", link_objects);
+        // Deserializes the backwards-compatible variants of `-Clink-self-contained`
+        key!(
+            link_self_contained = "crt-objects-fallback",
+            link_self_contained_backwards_compatible
+        )?;
+        // Deserializes the components variant of `-Clink-self-contained`
+        key!(link_self_contained, link_self_contained_components)?;
+        key!(pre_link_args_json = "pre-link-args", link_args);
+        key!(late_link_args_json = "late-link-args", link_args);
+        key!(late_link_args_dynamic_json = "late-link-args-dynamic", link_args);
+        key!(late_link_args_static_json = "late-link-args-static", link_args);
+        key!(post_link_args_json = "post-link-args", link_args);
+        key!(link_script, optional);
+        key!(link_env, env);
+        key!(link_env_remove, list);
+        key!(asm_args, list);
+        key!(cpu);
+        key!(features);
+        key!(dynamic_linking, bool);
+        key!(direct_access_external_data, Option<bool>);
+        key!(dll_tls_export, bool);
+        key!(only_cdylib, bool);
+        key!(executables, bool);
+        key!(relocation_model, RelocModel)?;
+        key!(code_model, CodeModel)?;
+        key!(tls_model, TlsModel)?;
+        key!(disable_redzone, bool);
+        key!(function_sections, bool);
+        key!(dll_prefix);
+        key!(dll_suffix);
+        key!(exe_suffix);
+        key!(staticlib_prefix);
+        key!(staticlib_suffix);
+        key!(families, target_families);
+        key!(abi_return_struct_as_int, bool);
+        key!(is_like_aix, bool);
+        key!(is_like_osx, bool);
+        key!(is_like_solaris, bool);
+        key!(is_like_windows, bool);
+        key!(is_like_msvc, bool);
+        key!(is_like_wasm, bool);
+        key!(is_like_android, bool);
+        key!(default_dwarf_version, u32);
+        key!(allows_weak_linkage, bool);
+        key!(has_rpath, bool);
+        key!(no_default_libraries, bool);
+        key!(position_independent_executables, bool);
+        key!(static_position_independent_executables, bool);
+        key!(plt_by_default, bool);
+        key!(relro_level, RelroLevel)?;
+        key!(archive_format);
+        key!(allow_asm, bool);
+        key!(main_needs_argc_argv, bool);
+        key!(has_thread_local, bool);
+        key!(obj_is_bitcode, bool);
+        key!(bitcode_llvm_cmdline);
+        key!(max_atomic_width, Option<u64>);
+        key!(min_atomic_width, Option<u64>);
+        key!(atomic_cas, bool);
+        key!(panic_strategy, PanicStrategy)?;
+        key!(crt_static_allows_dylibs, bool);
+        key!(crt_static_default, bool);
+        key!(crt_static_respected, bool);
+        key!(stack_probes, StackProbeType)?;
+        key!(min_global_align, Option<u64>);
+        key!(default_codegen_units, Option<u64>);
+        key!(trap_unreachable, bool);
+        key!(requires_lto, bool);
+        key!(singlethread, bool);
+        key!(no_builtins, bool);
+        key!(default_visibility, Option<SymbolVisibility>)?;
+        key!(emit_debug_gdb_scripts, bool);
+        key!(requires_uwtable, bool);
+        key!(default_uwtable, bool);
+        key!(simd_types_indirect, bool);
+        key!(limit_rdylib_exports, bool);
+        key!(override_export_symbols, opt_list);
+        key!(merge_functions, MergeFunctions)?;
+        key!(mcount = "target-mcount");
+        key!(llvm_mcount_intrinsic, optional);
+        key!(llvm_abiname);
+        key!(relax_elf_relocations, bool);
+        key!(llvm_args, list);
+        key!(use_ctors_section, bool);
+        key!(eh_frame_header, bool);
+        key!(has_thumb_interworking, bool);
+        key!(debuginfo_kind, DebuginfoKind)?;
+        key!(split_debuginfo, SplitDebuginfo)?;
+        key!(supported_split_debuginfo, fallible_list)?;
+        key!(supported_sanitizers, SanitizerSet)?;
+        key!(generate_arange_section, bool);
+        key!(supports_stack_protector, bool);
+        key!(small_data_threshold_support, SmallDataThresholdSupport)?;
+        key!(entry_name);
+        key!(entry_abi, Conv)?;
+        key!(supports_xray, bool);
+
+        base.update_from_cli();
+        base.check_consistency(TargetKind::Json)?;
+
+        // Each field should have been read using `Json::remove` so any keys remaining are unused.
+        let remaining_keys = obj.keys();
+        Ok((base, TargetWarnings {
+            unused_fields: remaining_keys.cloned().collect(),
+            incorrect_type,
+        }))
+    }
+}
+
+impl ToJson for Target {
+    fn to_json(&self) -> Json {
+        let mut d = serde_json::Map::new();
+        let default: TargetOptions = Default::default();
+        let mut target = self.clone();
+        target.update_to_cli();
+
+        macro_rules! target_val {
+            ($attr:ident) => {{
+                let name = (stringify!($attr)).replace("_", "-");
+                d.insert(name, target.$attr.to_json());
+            }};
+        }
+
+        macro_rules! target_option_val {
+            ($attr:ident) => {{
+                let name = (stringify!($attr)).replace("_", "-");
+                if default.$attr != target.$attr {
+                    d.insert(name, target.$attr.to_json());
+                }
+            }};
+            ($attr:ident, $json_name:expr) => {{
+                let name = $json_name;
+                if default.$attr != target.$attr {
+                    d.insert(name.into(), target.$attr.to_json());
+                }
+            }};
+            (link_args - $attr:ident, $json_name:expr) => {{
+                let name = $json_name;
+                if default.$attr != target.$attr {
+                    let obj = target
+                        .$attr
+                        .iter()
+                        .map(|(k, v)| (k.desc().to_string(), v.clone()))
+                        .collect::<BTreeMap<_, _>>();
+                    d.insert(name.to_string(), obj.to_json());
+                }
+            }};
+            (env - $attr:ident) => {{
+                let name = (stringify!($attr)).replace("_", "-");
+                if default.$attr != target.$attr {
+                    let obj = target
+                        .$attr
+                        .iter()
+                        .map(|&(ref k, ref v)| format!("{k}={v}"))
+                        .collect::<Vec<_>>();
+                    d.insert(name, obj.to_json());
+                }
+            }};
+        }
+
+        target_val!(llvm_target);
+        target_val!(metadata);
+        d.insert("target-pointer-width".to_string(), self.pointer_width.to_string().to_json());
+        target_val!(arch);
+        target_val!(data_layout);
+
+        target_option_val!(endian, "target-endian");
+        target_option_val!(c_int_width, "target-c-int-width");
+        target_option_val!(os);
+        target_option_val!(env);
+        target_option_val!(abi);
+        target_option_val!(vendor);
+        target_option_val!(linker);
+        target_option_val!(linker_flavor_json, "linker-flavor");
+        target_option_val!(lld_flavor_json, "lld-flavor");
+        target_option_val!(linker_is_gnu_json, "linker-is-gnu");
+        target_option_val!(pre_link_objects);
+        target_option_val!(post_link_objects);
+        target_option_val!(pre_link_objects_self_contained, "pre-link-objects-fallback");
+        target_option_val!(post_link_objects_self_contained, "post-link-objects-fallback");
+        target_option_val!(link_args - pre_link_args_json, "pre-link-args");
+        target_option_val!(link_args - late_link_args_json, "late-link-args");
+        target_option_val!(link_args - late_link_args_dynamic_json, "late-link-args-dynamic");
+        target_option_val!(link_args - late_link_args_static_json, "late-link-args-static");
+        target_option_val!(link_args - post_link_args_json, "post-link-args");
+        target_option_val!(link_script);
+        target_option_val!(env - link_env);
+        target_option_val!(link_env_remove);
+        target_option_val!(asm_args);
+        target_option_val!(cpu);
+        target_option_val!(features);
+        target_option_val!(dynamic_linking);
+        target_option_val!(direct_access_external_data);
+        target_option_val!(dll_tls_export);
+        target_option_val!(only_cdylib);
+        target_option_val!(executables);
+        target_option_val!(relocation_model);
+        target_option_val!(code_model);
+        target_option_val!(tls_model);
+        target_option_val!(disable_redzone);
+        target_option_val!(frame_pointer);
+        target_option_val!(function_sections);
+        target_option_val!(dll_prefix);
+        target_option_val!(dll_suffix);
+        target_option_val!(exe_suffix);
+        target_option_val!(staticlib_prefix);
+        target_option_val!(staticlib_suffix);
+        target_option_val!(families, "target-family");
+        target_option_val!(abi_return_struct_as_int);
+        target_option_val!(is_like_aix);
+        target_option_val!(is_like_osx);
+        target_option_val!(is_like_solaris);
+        target_option_val!(is_like_windows);
+        target_option_val!(is_like_msvc);
+        target_option_val!(is_like_wasm);
+        target_option_val!(is_like_android);
+        target_option_val!(default_dwarf_version);
+        target_option_val!(allows_weak_linkage);
+        target_option_val!(has_rpath);
+        target_option_val!(no_default_libraries);
+        target_option_val!(position_independent_executables);
+        target_option_val!(static_position_independent_executables);
+        target_option_val!(plt_by_default);
+        target_option_val!(relro_level);
+        target_option_val!(archive_format);
+        target_option_val!(allow_asm);
+        target_option_val!(main_needs_argc_argv);
+        target_option_val!(has_thread_local);
+        target_option_val!(obj_is_bitcode);
+        target_option_val!(bitcode_llvm_cmdline);
+        target_option_val!(min_atomic_width);
+        target_option_val!(max_atomic_width);
+        target_option_val!(atomic_cas);
+        target_option_val!(panic_strategy);
+        target_option_val!(crt_static_allows_dylibs);
+        target_option_val!(crt_static_default);
+        target_option_val!(crt_static_respected);
+        target_option_val!(stack_probes);
+        target_option_val!(min_global_align);
+        target_option_val!(default_codegen_units);
+        target_option_val!(trap_unreachable);
+        target_option_val!(requires_lto);
+        target_option_val!(singlethread);
+        target_option_val!(no_builtins);
+        target_option_val!(default_visibility);
+        target_option_val!(emit_debug_gdb_scripts);
+        target_option_val!(requires_uwtable);
+        target_option_val!(default_uwtable);
+        target_option_val!(simd_types_indirect);
+        target_option_val!(limit_rdylib_exports);
+        target_option_val!(override_export_symbols);
+        target_option_val!(merge_functions);
+        target_option_val!(mcount, "target-mcount");
+        target_option_val!(llvm_mcount_intrinsic);
+        target_option_val!(llvm_abiname);
+        target_option_val!(relax_elf_relocations);
+        target_option_val!(llvm_args);
+        target_option_val!(use_ctors_section);
+        target_option_val!(eh_frame_header);
+        target_option_val!(has_thumb_interworking);
+        target_option_val!(debuginfo_kind);
+        target_option_val!(split_debuginfo);
+        target_option_val!(supported_split_debuginfo);
+        target_option_val!(supported_sanitizers);
+        target_option_val!(c_enum_min_bits);
+        target_option_val!(generate_arange_section);
+        target_option_val!(supports_stack_protector);
+        target_option_val!(small_data_threshold_support);
+        target_option_val!(entry_name);
+        target_option_val!(entry_abi);
+        target_option_val!(supports_xray);
+
+        // Serializing `-Clink-self-contained` needs a dynamic key to support the
+        // backwards-compatible variants.
+        d.insert(self.link_self_contained.json_key().into(), self.link_self_contained.to_json());
+
+        Json::Object(d)
+    }
+}
diff --git a/compiler/rustc_target/src/spec/mod.rs b/compiler/rustc_target/src/spec/mod.rs
index fead20ec7d1..a2e9430830a 100644
--- a/compiler/rustc_target/src/spec/mod.rs
+++ b/compiler/rustc_target/src/spec/mod.rs
@@ -42,6 +42,7 @@ use std::path::{Path, PathBuf};
 use std::str::FromStr;
 use std::{fmt, io};
 
+use rustc_data_structures::fx::FxHashSet;
 use rustc_fs_util::try_canonicalize;
 use rustc_macros::{Decodable, Encodable, HashStable_Generic};
 use rustc_serialize::{Decodable, Decoder, Encodable, Encoder};
@@ -65,6 +66,8 @@ pub mod abi {
 }
 
 mod base;
+mod json;
+
 pub use base::avr_gnu::ef_avr_arch;
 
 /// Linker is called through a C/C++ compiler.
@@ -1605,13 +1608,11 @@ macro_rules! supported_targets {
 
         #[cfg(test)]
         mod tests {
-            mod tests_impl;
-
             // Cannot put this into a separate file without duplication, make an exception.
             $(
                 #[test] // `#[test]`
                 fn $module() {
-                    tests_impl::test_target(crate::spec::targets::$module::target());
+                    crate::spec::targets::$module::target().test_target()
                 }
             )+
         }
@@ -1998,6 +1999,14 @@ impl TargetWarnings {
     }
 }
 
+/// For the [`Target::check_consistency`] function, determines whether the given target is a builtin or a JSON
+/// target.
+#[derive(Copy, Clone, Debug, PartialEq)]
+enum TargetKind {
+    Json,
+    Builtin,
+}
+
 /// Everything `rustc` knows about how to compile for a specific target.
 ///
 /// Every field here must be specified, and has no default value.
@@ -2846,619 +2855,355 @@ impl Target {
         self.max_atomic_width.unwrap_or_else(|| self.pointer_width.into())
     }
 
-    /// Loads a target descriptor from a JSON object.
-    pub fn from_json(obj: Json) -> Result<(Target, TargetWarnings), String> {
-        // While ugly, this code must remain this way to retain
-        // compatibility with existing JSON fields and the internal
-        // expected naming of the Target and TargetOptions structs.
-        // To ensure compatibility is retained, the built-in targets
-        // are round-tripped through this code to catch cases where
-        // the JSON parser is not updated to match the structs.
-
-        let mut obj = match obj {
-            Value::Object(obj) => obj,
-            _ => return Err("Expected JSON object for target")?,
-        };
-
-        let mut get_req_field = |name: &str| {
-            obj.remove(name)
-                .and_then(|j| j.as_str().map(str::to_string))
-                .ok_or_else(|| format!("Field {name} in target specification is required"))
-        };
-
-        let mut base = Target {
-            llvm_target: get_req_field("llvm-target")?.into(),
-            metadata: Default::default(),
-            pointer_width: get_req_field("target-pointer-width")?
-                .parse::<u32>()
-                .map_err(|_| "target-pointer-width must be an integer".to_string())?,
-            data_layout: get_req_field("data-layout")?.into(),
-            arch: get_req_field("arch")?.into(),
-            options: Default::default(),
-        };
-
-        // FIXME: This doesn't properly validate anything and just ignores the data if it's invalid.
-        // That's okay for now, the only use of this is when generating docs, which we don't do for
-        // custom targets.
-        if let Some(Json::Object(mut metadata)) = obj.remove("metadata") {
-            base.metadata.description = metadata
-                .remove("description")
-                .and_then(|desc| desc.as_str().map(|desc| desc.to_owned().into()));
-            base.metadata.tier = metadata
-                .remove("tier")
-                .and_then(|tier| tier.as_u64())
-                .filter(|tier| (1..=3).contains(tier));
-            base.metadata.host_tools =
-                metadata.remove("host_tools").and_then(|host| host.as_bool());
-            base.metadata.std = metadata.remove("std").and_then(|host| host.as_bool());
-        }
-
-        let mut incorrect_type = vec![];
-
-        macro_rules! key {
-            ($key_name:ident) => ( {
-                let name = (stringify!($key_name)).replace("_", "-");
-                if let Some(s) = obj.remove(&name).and_then(|s| s.as_str().map(str::to_string).map(Cow::from)) {
-                    base.$key_name = s;
-                }
-            } );
-            ($key_name:ident = $json_name:expr) => ( {
-                let name = $json_name;
-                if let Some(s) = obj.remove(name).and_then(|s| s.as_str().map(str::to_string).map(Cow::from)) {
-                    base.$key_name = s;
-                }
-            } );
-            ($key_name:ident, bool) => ( {
-                let name = (stringify!($key_name)).replace("_", "-");
-                if let Some(s) = obj.remove(&name).and_then(|b| b.as_bool()) {
-                    base.$key_name = s;
-                }
-            } );
-            ($key_name:ident = $json_name:expr, bool) => ( {
-                let name = $json_name;
-                if let Some(s) = obj.remove(name).and_then(|b| b.as_bool()) {
-                    base.$key_name = s;
+    /// Check some basic consistency of the current target. For JSON targets we are less strict;
+    /// some of these checks are more guidelines than strict rules.
+    fn check_consistency(&self, kind: TargetKind) -> Result<(), String> {
+        macro_rules! check {
+            ($b:expr, $($msg:tt)*) => {
+                if !$b {
+                    return Err(format!($($msg)*));
                 }
-            } );
-            ($key_name:ident, u32) => ( {
-                let name = (stringify!($key_name)).replace("_", "-");
-                if let Some(s) = obj.remove(&name).and_then(|b| b.as_u64()) {
-                    if s < 1 || s > 5 {
-                        return Err("Not a valid DWARF version number".into());
-                    }
-                    base.$key_name = s as u32;
-                }
-            } );
-            ($key_name:ident, Option<bool>) => ( {
-                let name = (stringify!($key_name)).replace("_", "-");
-                if let Some(s) = obj.remove(&name).and_then(|b| b.as_bool()) {
-                    base.$key_name = Some(s);
-                }
-            } );
-            ($key_name:ident, Option<u64>) => ( {
-                let name = (stringify!($key_name)).replace("_", "-");
-                if let Some(s) = obj.remove(&name).and_then(|b| b.as_u64()) {
-                    base.$key_name = Some(s);
-                }
-            } );
-            ($key_name:ident, MergeFunctions) => ( {
-                let name = (stringify!($key_name)).replace("_", "-");
-                obj.remove(&name).and_then(|o| o.as_str().and_then(|s| {
-                    match s.parse::<MergeFunctions>() {
-                        Ok(mergefunc) => base.$key_name = mergefunc,
-                        _ => return Some(Err(format!("'{}' is not a valid value for \
-                                                      merge-functions. Use 'disabled', \
-                                                      'trampolines', or 'aliases'.",
-                                                      s))),
-                    }
-                    Some(Ok(()))
-                })).unwrap_or(Ok(()))
-            } );
-            ($key_name:ident, RelocModel) => ( {
-                let name = (stringify!($key_name)).replace("_", "-");
-                obj.remove(&name).and_then(|o| o.as_str().and_then(|s| {
-                    match s.parse::<RelocModel>() {
-                        Ok(relocation_model) => base.$key_name = relocation_model,
-                        _ => return Some(Err(format!("'{}' is not a valid relocation model. \
-                                                      Run `rustc --print relocation-models` to \
-                                                      see the list of supported values.", s))),
-                    }
-                    Some(Ok(()))
-                })).unwrap_or(Ok(()))
-            } );
-            ($key_name:ident, CodeModel) => ( {
-                let name = (stringify!($key_name)).replace("_", "-");
-                obj.remove(&name).and_then(|o| o.as_str().and_then(|s| {
-                    match s.parse::<CodeModel>() {
-                        Ok(code_model) => base.$key_name = Some(code_model),
-                        _ => return Some(Err(format!("'{}' is not a valid code model. \
-                                                      Run `rustc --print code-models` to \
-                                                      see the list of supported values.", s))),
-                    }
-                    Some(Ok(()))
-                })).unwrap_or(Ok(()))
-            } );
-            ($key_name:ident, TlsModel) => ( {
-                let name = (stringify!($key_name)).replace("_", "-");
-                obj.remove(&name).and_then(|o| o.as_str().and_then(|s| {
-                    match s.parse::<TlsModel>() {
-                        Ok(tls_model) => base.$key_name = tls_model,
-                        _ => return Some(Err(format!("'{}' is not a valid TLS model. \
-                                                      Run `rustc --print tls-models` to \
-                                                      see the list of supported values.", s))),
-                    }
-                    Some(Ok(()))
-                })).unwrap_or(Ok(()))
-            } );
-            ($key_name:ident, SmallDataThresholdSupport) => ( {
-                obj.remove("small-data-threshold-support").and_then(|o| o.as_str().and_then(|s| {
-                    match s.parse::<SmallDataThresholdSupport>() {
-                        Ok(support) => base.small_data_threshold_support = support,
-                        _ => return Some(Err(format!("'{s}' is not a valid value for small-data-threshold-support."))),
-                    }
-                    Some(Ok(()))
-                })).unwrap_or(Ok(()))
-            } );
-            ($key_name:ident, PanicStrategy) => ( {
-                let name = (stringify!($key_name)).replace("_", "-");
-                obj.remove(&name).and_then(|o| o.as_str().and_then(|s| {
-                    match s {
-                        "unwind" => base.$key_name = PanicStrategy::Unwind,
-                        "abort" => base.$key_name = PanicStrategy::Abort,
-                        _ => return Some(Err(format!("'{}' is not a valid value for \
-                                                      panic-strategy. Use 'unwind' or 'abort'.",
-                                                     s))),
-                }
-                Some(Ok(()))
-            })).unwrap_or(Ok(()))
-            } );
-            ($key_name:ident, RelroLevel) => ( {
-                let name = (stringify!($key_name)).replace("_", "-");
-                obj.remove(&name).and_then(|o| o.as_str().and_then(|s| {
-                    match s.parse::<RelroLevel>() {
-                        Ok(level) => base.$key_name = level,
-                        _ => return Some(Err(format!("'{}' is not a valid value for \
-                                                      relro-level. Use 'full', 'partial, or 'off'.",
-                                                      s))),
-                    }
-                    Some(Ok(()))
-                })).unwrap_or(Ok(()))
-            } );
-            ($key_name:ident, Option<SymbolVisibility>) => ( {
-                let name = (stringify!($key_name)).replace("_", "-");
-                obj.remove(&name).and_then(|o| o.as_str().and_then(|s| {
-                    match s.parse::<SymbolVisibility>() {
-                        Ok(level) => base.$key_name = Some(level),
-                        _ => return Some(Err(format!("'{}' is not a valid value for \
-                                                      symbol-visibility. Use 'hidden', 'protected, or 'interposable'.",
-                                                      s))),
-                    }
-                    Some(Ok(()))
-                })).unwrap_or(Ok(()))
-            } );
-            ($key_name:ident, DebuginfoKind) => ( {
-                let name = (stringify!($key_name)).replace("_", "-");
-                obj.remove(&name).and_then(|o| o.as_str().and_then(|s| {
-                    match s.parse::<DebuginfoKind>() {
-                        Ok(level) => base.$key_name = level,
-                        _ => return Some(Err(
-                            format!("'{s}' is not a valid value for debuginfo-kind. Use 'dwarf', \
-                                  'dwarf-dsym' or 'pdb'.")
-                        )),
-                    }
-                    Some(Ok(()))
-                })).unwrap_or(Ok(()))
-            } );
-            ($key_name:ident, SplitDebuginfo) => ( {
-                let name = (stringify!($key_name)).replace("_", "-");
-                obj.remove(&name).and_then(|o| o.as_str().and_then(|s| {
-                    match s.parse::<SplitDebuginfo>() {
-                        Ok(level) => base.$key_name = level,
-                        _ => return Some(Err(format!("'{}' is not a valid value for \
-                                                      split-debuginfo. Use 'off' or 'dsymutil'.",
-                                                      s))),
-                    }
-                    Some(Ok(()))
-                })).unwrap_or(Ok(()))
-            } );
-            ($key_name:ident, list) => ( {
-                let name = (stringify!($key_name)).replace("_", "-");
-                if let Some(j) = obj.remove(&name) {
-                    if let Some(v) = j.as_array() {
-                        base.$key_name = v.iter()
-                            .map(|a| a.as_str().unwrap().to_string().into())
-                            .collect();
-                    } else {
-                        incorrect_type.push(name)
-                    }
+            }
+        }
+        macro_rules! check_eq {
+            ($left:expr, $right:expr, $($msg:tt)*) => {
+                if ($left) != ($right) {
+                    return Err(format!($($msg)*));
                 }
-            } );
-            ($key_name:ident, opt_list) => ( {
-                let name = (stringify!($key_name)).replace("_", "-");
-                if let Some(j) = obj.remove(&name) {
-                    if let Some(v) = j.as_array() {
-                        base.$key_name = Some(v.iter()
-                            .map(|a| a.as_str().unwrap().to_string().into())
-                            .collect());
-                    } else {
-                        incorrect_type.push(name)
-                    }
+            }
+        }
+        macro_rules! check_ne {
+            ($left:expr, $right:expr, $($msg:tt)*) => {
+                if ($left) == ($right) {
+                    return Err(format!($($msg)*));
                 }
-            } );
-            ($key_name:ident, fallible_list) => ( {
-                let name = (stringify!($key_name)).replace("_", "-");
-                obj.remove(&name).and_then(|j| {
-                    if let Some(v) = j.as_array() {
-                        match v.iter().map(|a| FromStr::from_str(a.as_str().unwrap())).collect() {
-                            Ok(l) => { base.$key_name = l },
-                            // FIXME: `fallible_list` can't re-use the `key!` macro for list
-                            // elements and the error messages from that macro, so it has a bad
-                            // generic message instead
-                            Err(_) => return Some(Err(
-                                format!("`{:?}` is not a valid value for `{}`", j, name)
-                            )),
-                        }
-                    } else {
-                        incorrect_type.push(name)
-                    }
-                    Some(Ok(()))
-                }).unwrap_or(Ok(()))
-            } );
-            ($key_name:ident, optional) => ( {
-                let name = (stringify!($key_name)).replace("_", "-");
-                if let Some(o) = obj.remove(&name) {
-                    base.$key_name = o
-                        .as_str()
-                        .map(|s| s.to_string().into());
+            }
+        }
+        macro_rules! check_matches {
+            ($left:expr, $right:pat, $($msg:tt)*) => {
+                if !matches!($left, $right) {
+                    return Err(format!($($msg)*));
                 }
-            } );
-            ($key_name:ident = $json_name:expr, LldFlavor) => ( {
-                let name = $json_name;
-                obj.remove(name).and_then(|o| o.as_str().and_then(|s| {
-                    if let Some(flavor) = LldFlavor::from_str(&s) {
-                        base.$key_name = flavor;
-                    } else {
-                        return Some(Err(format!(
-                            "'{}' is not a valid value for lld-flavor. \
-                             Use 'darwin', 'gnu', 'link' or 'wasm'.",
-                            s)))
-                    }
-                    Some(Ok(()))
-                })).unwrap_or(Ok(()))
-            } );
-            ($key_name:ident = $json_name:expr, LinkerFlavor) => ( {
-                let name = $json_name;
-                obj.remove(name).and_then(|o| o.as_str().and_then(|s| {
-                    match LinkerFlavorCli::from_str(s) {
-                        Some(linker_flavor) => base.$key_name = linker_flavor,
-                        _ => return Some(Err(format!("'{}' is not a valid value for linker-flavor. \
-                                                      Use {}", s, LinkerFlavorCli::one_of()))),
+            }
+        }
+
+        check_eq!(
+            self.is_like_osx,
+            self.vendor == "apple",
+            "`is_like_osx` must be set if and only if `vendor` is `apple`"
+        );
+        check_eq!(
+            self.is_like_solaris,
+            self.os == "solaris" || self.os == "illumos",
+            "`is_like_solaris` must be set if and only if `os` is `solaris` or `illumos`"
+        );
+        check_eq!(
+            self.is_like_windows,
+            self.os == "windows" || self.os == "uefi",
+            "`is_like_windows` must be set if and only if `os` is `windows` or `uefi`"
+        );
+        check_eq!(
+            self.is_like_wasm,
+            self.arch == "wasm32" || self.arch == "wasm64",
+            "`is_like_wasm` must be set if and only if `arch` is `wasm32` or `wasm64`"
+        );
+        if self.is_like_msvc {
+            check!(self.is_like_windows, "if `is_like_msvc` is set, `is_like_windows` must be set");
+        }
+        if self.os == "emscripten" {
+            check!(self.is_like_wasm, "the `emcscripten` os only makes sense on wasm-like targets");
+        }
+
+        // Check that default linker flavor is compatible with some other key properties.
+        check_eq!(
+            self.is_like_osx,
+            matches!(self.linker_flavor, LinkerFlavor::Darwin(..)),
+            "`linker_flavor` must be `darwin` if and only if `is_like_osx` is set"
+        );
+        check_eq!(
+            self.is_like_msvc,
+            matches!(self.linker_flavor, LinkerFlavor::Msvc(..)),
+            "`linker_flavor` must be `msvc` if and only if `is_like_msvc` is set"
+        );
+        check_eq!(
+            self.is_like_wasm && self.os != "emscripten",
+            matches!(self.linker_flavor, LinkerFlavor::WasmLld(..)),
+            "`linker_flavor` must be `wasm-lld` if and only if `is_like_wasm` is set and the `os` is not `emscripten`",
+        );
+        check_eq!(
+            self.os == "emscripten",
+            matches!(self.linker_flavor, LinkerFlavor::EmCc),
+            "`linker_flavor` must be `em-cc` if and only if `os` is `emscripten`"
+        );
+        check_eq!(
+            self.arch == "bpf",
+            matches!(self.linker_flavor, LinkerFlavor::Bpf),
+            "`linker_flavor` must be `bpf` if and only if `arch` is `bpf`"
+        );
+        check_eq!(
+            self.arch == "nvptx64",
+            matches!(self.linker_flavor, LinkerFlavor::Ptx),
+            "`linker_flavor` must be `ptc` if and only if `arch` is `nvptx64`"
+        );
+
+        for args in [
+            &self.pre_link_args,
+            &self.late_link_args,
+            &self.late_link_args_dynamic,
+            &self.late_link_args_static,
+            &self.post_link_args,
+        ] {
+            for (&flavor, flavor_args) in args {
+                check!(!flavor_args.is_empty(), "linker flavor args must not be empty");
+                // Check that flavors mentioned in link args are compatible with the default flavor.
+                match self.linker_flavor {
+                    LinkerFlavor::Gnu(..) => {
+                        check_matches!(
+                            flavor,
+                            LinkerFlavor::Gnu(..),
+                            "mixing GNU and non-GNU linker flavors"
+                        );
                     }
-                    Some(Ok(()))
-                })).unwrap_or(Ok(()))
-            } );
-            ($key_name:ident, StackProbeType) => ( {
-                let name = (stringify!($key_name)).replace("_", "-");
-                obj.remove(&name).and_then(|o| match StackProbeType::from_json(&o) {
-                    Ok(v) => {
-                        base.$key_name = v;
-                        Some(Ok(()))
-                    },
-                    Err(s) => Some(Err(
-                        format!("`{:?}` is not a valid value for `{}`: {}", o, name, s)
-                    )),
-                }).unwrap_or(Ok(()))
-            } );
-            ($key_name:ident, SanitizerSet) => ( {
-                let name = (stringify!($key_name)).replace("_", "-");
-                if let Some(o) = obj.remove(&name) {
-                    if let Some(a) = o.as_array() {
-                        for s in a {
-                            base.$key_name |= match s.as_str() {
-                                Some("address") => SanitizerSet::ADDRESS,
-                                Some("cfi") => SanitizerSet::CFI,
-                                Some("dataflow") => SanitizerSet::DATAFLOW,
-                                Some("kcfi") => SanitizerSet::KCFI,
-                                Some("kernel-address") => SanitizerSet::KERNELADDRESS,
-                                Some("leak") => SanitizerSet::LEAK,
-                                Some("memory") => SanitizerSet::MEMORY,
-                                Some("memtag") => SanitizerSet::MEMTAG,
-                                Some("safestack") => SanitizerSet::SAFESTACK,
-                                Some("shadow-call-stack") => SanitizerSet::SHADOWCALLSTACK,
-                                Some("thread") => SanitizerSet::THREAD,
-                                Some("hwaddress") => SanitizerSet::HWADDRESS,
-                                Some(s) => return Err(format!("unknown sanitizer {}", s)),
-                                _ => return Err(format!("not a string: {:?}", s)),
-                            };
-                        }
-                    } else {
-                        incorrect_type.push(name)
+                    LinkerFlavor::Darwin(..) => {
+                        check_matches!(
+                            flavor,
+                            LinkerFlavor::Darwin(..),
+                            "mixing Darwin and non-Darwin linker flavors"
+                        )
                     }
-                }
-                Ok::<(), String>(())
-            } );
-            ($key_name:ident, link_self_contained_components) => ( {
-                // Skeleton of what needs to be parsed:
-                //
-                // ```
-                // $name: {
-                //     "components": [
-                //         <array of strings>
-                //     ]
-                // }
-                // ```
-                let name = (stringify!($key_name)).replace("_", "-");
-                if let Some(o) = obj.remove(&name) {
-                    if let Some(o) = o.as_object() {
-                        let component_array = o.get("components")
-                            .ok_or_else(|| format!("{name}: expected a \
-                                JSON object with a `components` field."))?;
-                        let component_array = component_array.as_array()
-                            .ok_or_else(|| format!("{name}.components: expected a JSON array"))?;
-                        let mut components = LinkSelfContainedComponents::empty();
-                        for s in component_array {
-                            components |= match s.as_str() {
-                                Some(s) => {
-                                    LinkSelfContainedComponents::from_str(s)
-                                        .ok_or_else(|| format!("unknown \
-                                        `-Clink-self-contained` component: {s}"))?
-                                },
-                                _ => return Err(format!("not a string: {:?}", s)),
-                            };
-                        }
-                        base.$key_name = LinkSelfContainedDefault::WithComponents(components);
-                    } else {
-                        incorrect_type.push(name)
+                    LinkerFlavor::WasmLld(..) => {
+                        check_matches!(
+                            flavor,
+                            LinkerFlavor::WasmLld(..),
+                            "mixing wasm and non-wasm linker flavors"
+                        )
                     }
-                }
-                Ok::<(), String>(())
-            } );
-            ($key_name:ident = $json_name:expr, link_self_contained_backwards_compatible) => ( {
-                let name = $json_name;
-                obj.remove(name).and_then(|o| o.as_str().and_then(|s| {
-                    match s.parse::<LinkSelfContainedDefault>() {
-                        Ok(lsc_default) => base.$key_name = lsc_default,
-                        _ => return Some(Err(format!("'{}' is not a valid `-Clink-self-contained` default. \
-                                                      Use 'false', 'true', 'musl' or 'mingw'", s))),
+                    LinkerFlavor::Unix(..) => {
+                        check_matches!(
+                            flavor,
+                            LinkerFlavor::Unix(..),
+                            "mixing unix and non-unix linker flavors"
+                        );
                     }
-                    Some(Ok(()))
-                })).unwrap_or(Ok(()))
-            } );
-            ($key_name:ident = $json_name:expr, link_objects) => ( {
-                let name = $json_name;
-                if let Some(val) = obj.remove(name) {
-                    let obj = val.as_object().ok_or_else(|| format!("{}: expected a \
-                        JSON object with fields per CRT object kind.", name))?;
-                    let mut args = CrtObjects::new();
-                    for (k, v) in obj {
-                        let kind = LinkOutputKind::from_str(&k).ok_or_else(|| {
-                            format!("{}: '{}' is not a valid value for CRT object kind. \
-                                     Use '(dynamic,static)-(nopic,pic)-exe' or \
-                                     '(dynamic,static)-dylib' or 'wasi-reactor-exe'", name, k)
-                        })?;
-
-                        let v = v.as_array().ok_or_else(||
-                            format!("{}.{}: expected a JSON array", name, k)
-                        )?.iter().enumerate()
-                            .map(|(i,s)| {
-                                let s = s.as_str().ok_or_else(||
-                                    format!("{}.{}[{}]: expected a JSON string", name, k, i))?;
-                                Ok(s.to_string().into())
-                            })
-                            .collect::<Result<Vec<_>, String>>()?;
-
-                        args.insert(kind, v);
+                    LinkerFlavor::Msvc(..) => {
+                        check_matches!(
+                            flavor,
+                            LinkerFlavor::Msvc(..),
+                            "mixing MSVC and non-MSVC linker flavors"
+                        );
                     }
-                    base.$key_name = args;
-                }
-            } );
-            ($key_name:ident = $json_name:expr, link_args) => ( {
-                let name = $json_name;
-                if let Some(val) = obj.remove(name) {
-                    let obj = val.as_object().ok_or_else(|| format!("{}: expected a \
-                        JSON object with fields per linker-flavor.", name))?;
-                    let mut args = LinkArgsCli::new();
-                    for (k, v) in obj {
-                        let flavor = LinkerFlavorCli::from_str(&k).ok_or_else(|| {
-                            format!("{}: '{}' is not a valid value for linker-flavor. \
-                                     Use 'em', 'gcc', 'ld' or 'msvc'", name, k)
-                        })?;
-
-                        let v = v.as_array().ok_or_else(||
-                            format!("{}.{}: expected a JSON array", name, k)
-                        )?.iter().enumerate()
-                            .map(|(i,s)| {
-                                let s = s.as_str().ok_or_else(||
-                                    format!("{}.{}[{}]: expected a JSON string", name, k, i))?;
-                                Ok(s.to_string().into())
-                            })
-                            .collect::<Result<Vec<_>, String>>()?;
-
-                        args.insert(flavor, v);
+                    LinkerFlavor::EmCc
+                    | LinkerFlavor::Bpf
+                    | LinkerFlavor::Ptx
+                    | LinkerFlavor::Llbc => {
+                        check_eq!(flavor, self.linker_flavor, "mixing different linker flavors")
                     }
-                    base.$key_name = args;
                 }
-            } );
-            ($key_name:ident, env) => ( {
-                let name = (stringify!($key_name)).replace("_", "-");
-                if let Some(o) = obj.remove(&name) {
-                    if let Some(a) = o.as_array() {
-                        for o in a {
-                            if let Some(s) = o.as_str() {
-                                if let [k, v] = *s.split('=').collect::<Vec<_>>() {
-                                    base.$key_name
-                                        .to_mut()
-                                        .push((k.to_string().into(), v.to_string().into()))
-                                }
+
+                // Check that link args for cc and non-cc versions of flavors are consistent.
+                let check_noncc = |noncc_flavor| -> Result<(), String> {
+                    if let Some(noncc_args) = args.get(&noncc_flavor) {
+                        for arg in flavor_args {
+                            if let Some(suffix) = arg.strip_prefix("-Wl,") {
+                                check!(
+                                    noncc_args.iter().any(|a| a == suffix),
+                                    " link args for cc and non-cc versions of flavors are not consistent"
+                                );
                             }
                         }
-                    } else {
-                        incorrect_type.push(name)
-                    }
-                }
-            } );
-            ($key_name:ident, TargetFamilies) => ( {
-                if let Some(value) = obj.remove("target-family") {
-                    if let Some(v) = value.as_array() {
-                        base.$key_name = v.iter()
-                            .map(|a| a.as_str().unwrap().to_string().into())
-                            .collect();
-                    } else if let Some(v) = value.as_str() {
-                        base.$key_name = vec![v.to_string().into()].into();
                     }
+                    Ok(())
+                };
+
+                match self.linker_flavor {
+                    LinkerFlavor::Gnu(Cc::Yes, lld) => check_noncc(LinkerFlavor::Gnu(Cc::No, lld))?,
+                    LinkerFlavor::WasmLld(Cc::Yes) => check_noncc(LinkerFlavor::WasmLld(Cc::No))?,
+                    LinkerFlavor::Unix(Cc::Yes) => check_noncc(LinkerFlavor::Unix(Cc::No))?,
+                    _ => {}
                 }
-            } );
-            ($key_name:ident, Conv) => ( {
-                let name = (stringify!($key_name)).replace("_", "-");
-                obj.remove(&name).and_then(|o| o.as_str().and_then(|s| {
-                    match Conv::from_str(s) {
-                        Ok(c) => {
-                            base.$key_name = c;
-                            Some(Ok(()))
-                        }
-                        Err(e) => Some(Err(e))
-                    }
-                })).unwrap_or(Ok(()))
-            } );
-        }
+            }
 
-        if let Some(j) = obj.remove("target-endian") {
-            if let Some(s) = j.as_str() {
-                base.endian = s.parse()?;
-            } else {
-                incorrect_type.push("target-endian".into())
+            // Check that link args for lld and non-lld versions of flavors are consistent.
+            for cc in [Cc::No, Cc::Yes] {
+                check_eq!(
+                    args.get(&LinkerFlavor::Gnu(cc, Lld::No)),
+                    args.get(&LinkerFlavor::Gnu(cc, Lld::Yes)),
+                    "link args for lld and non-lld versions of flavors are not consistent",
+                );
+                check_eq!(
+                    args.get(&LinkerFlavor::Darwin(cc, Lld::No)),
+                    args.get(&LinkerFlavor::Darwin(cc, Lld::Yes)),
+                    "link args for lld and non-lld versions of flavors are not consistent",
+                );
+            }
+            check_eq!(
+                args.get(&LinkerFlavor::Msvc(Lld::No)),
+                args.get(&LinkerFlavor::Msvc(Lld::Yes)),
+                "link args for lld and non-lld versions of flavors are not consistent",
+            );
+        }
+
+        if self.link_self_contained.is_disabled() {
+            check!(
+                self.pre_link_objects_self_contained.is_empty()
+                    && self.post_link_objects_self_contained.is_empty(),
+                "if `link_self_contained` is disabled, then `pre_link_objects_self_contained` and `post_link_objects_self_contained` must be empty",
+            );
+        }
+
+        // If your target really needs to deviate from the rules below,
+        // except it and document the reasons.
+        // Keep the default "unknown" vendor instead.
+        check_ne!(self.vendor, "", "`vendor` cannot be empty");
+        check_ne!(self.os, "", "`os` cannot be empty");
+        if !self.can_use_os_unknown() {
+            // Keep the default "none" for bare metal targets instead.
+            check_ne!(
+                self.os,
+                "unknown",
+                "`unknown` os can only be used on particular targets; use `none` for bare-metal targets"
+            );
+        }
+
+        // Check dynamic linking stuff.
+        // We skip this for JSON targets since otherwise, our default values would fail this test.
+        // These checks are not critical for correctness, but more like default guidelines.
+        // FIXME (https://github.com/rust-lang/rust/issues/133459): do we want to change the JSON
+        // target defaults so that they pass these checks?
+        if kind == TargetKind::Builtin {
+            // BPF: when targeting user space vms (like rbpf), those can load dynamic libraries.
+            // hexagon: when targeting QuRT, that OS can load dynamic libraries.
+            // wasm{32,64}: dynamic linking is inherent in the definition of the VM.
+            if self.os == "none"
+                && (self.arch != "bpf"
+                    && self.arch != "hexagon"
+                    && self.arch != "wasm32"
+                    && self.arch != "wasm64")
+            {
+                check!(
+                    !self.dynamic_linking,
+                    "dynamic linking is not supported on this OS/architecture"
+                );
+            }
+            if self.only_cdylib
+                || self.crt_static_allows_dylibs
+                || !self.late_link_args_dynamic.is_empty()
+            {
+                check!(
+                    self.dynamic_linking,
+                    "dynamic linking must be allowed when `only_cdylib` or `crt_static_allows_dylibs` or `late_link_args_dynamic` are set"
+                );
+            }
+            // Apparently PIC was slow on wasm at some point, see comments in wasm_base.rs
+            if self.dynamic_linking && !self.is_like_wasm {
+                check_eq!(
+                    self.relocation_model,
+                    RelocModel::Pic,
+                    "targets that support dynamic linking must use the `pic` relocation model"
+                );
+            }
+            if self.position_independent_executables {
+                check_eq!(
+                    self.relocation_model,
+                    RelocModel::Pic,
+                    "targets that support position-independent executables must use the `pic` relocation model"
+                );
+            }
+            // The UEFI targets do not support dynamic linking but still require PIC (#101377).
+            if self.relocation_model == RelocModel::Pic && (self.os != "uefi") {
+                check!(
+                    self.dynamic_linking || self.position_independent_executables,
+                    "when the relocation model is `pic`, the target must support dynamic linking or use position-independent executables. \
+                Set the relocation model to `static` to avoid this requirement"
+                );
+            }
+            if self.static_position_independent_executables {
+                check!(
+                    self.position_independent_executables,
+                    "if `static_position_independent_executables` is set, then `position_independent_executables` must be set"
+                );
             }
+            if self.position_independent_executables {
+                check!(
+                    self.executables,
+                    "if `position_independent_executables` is set then `executables` must be set"
+                );
+            }
+        }
+
+        // Check crt static stuff
+        if self.crt_static_default || self.crt_static_allows_dylibs {
+            check!(
+                self.crt_static_respected,
+                "static CRT can be enabled but `crt_static_respected` is not set"
+            );
         }
 
-        if let Some(fp) = obj.remove("frame-pointer") {
-            if let Some(s) = fp.as_str() {
-                base.frame_pointer = s
-                    .parse()
-                    .map_err(|()| format!("'{s}' is not a valid value for frame-pointer"))?;
-            } else {
-                incorrect_type.push("frame-pointer".into())
+        // Check that RISC-V targets always specify which ABI they use.
+        match &*self.arch {
+            "riscv32" => {
+                check_matches!(
+                    &*self.llvm_abiname,
+                    "ilp32" | "ilp32f" | "ilp32d" | "ilp32e",
+                    "invalid RISC-V ABI name"
+                );
+            }
+            "riscv64" => {
+                // Note that the `lp64e` is still unstable as it's not (yet) part of the ELF psABI.
+                check_matches!(
+                    &*self.llvm_abiname,
+                    "lp64" | "lp64f" | "lp64d" | "lp64q" | "lp64e",
+                    "invalid RISC-V ABI name"
+                );
+            }
+            _ => {}
+        }
+
+        // Check that the given target-features string makes some basic sense.
+        if !self.features.is_empty() {
+            let mut features_enabled = FxHashSet::default();
+            let mut features_disabled = FxHashSet::default();
+            for feat in self.features.split(',') {
+                if let Some(feat) = feat.strip_prefix("+") {
+                    features_enabled.insert(feat);
+                    if features_disabled.contains(feat) {
+                        return Err(format!(
+                            "target feature `{feat}` is both enabled and disabled"
+                        ));
+                    }
+                } else if let Some(feat) = feat.strip_prefix("-") {
+                    features_disabled.insert(feat);
+                    if features_enabled.contains(feat) {
+                        return Err(format!(
+                            "target feature `{feat}` is both enabled and disabled"
+                        ));
+                    }
+                } else {
+                    return Err(format!(
+                        "target feature `{feat}` is invalid, must start with `+` or `-`"
+                    ));
+                }
             }
         }
 
-        key!(c_int_width = "target-c-int-width");
-        key!(c_enum_min_bits, Option<u64>); // if None, matches c_int_width
-        key!(os);
-        key!(env);
-        key!(abi);
-        key!(vendor);
-        key!(linker, optional);
-        key!(linker_flavor_json = "linker-flavor", LinkerFlavor)?;
-        key!(lld_flavor_json = "lld-flavor", LldFlavor)?;
-        key!(linker_is_gnu_json = "linker-is-gnu", bool);
-        key!(pre_link_objects = "pre-link-objects", link_objects);
-        key!(post_link_objects = "post-link-objects", link_objects);
-        key!(pre_link_objects_self_contained = "pre-link-objects-fallback", link_objects);
-        key!(post_link_objects_self_contained = "post-link-objects-fallback", link_objects);
-        // Deserializes the backwards-compatible variants of `-Clink-self-contained`
-        key!(
-            link_self_contained = "crt-objects-fallback",
-            link_self_contained_backwards_compatible
-        )?;
-        // Deserializes the components variant of `-Clink-self-contained`
-        key!(link_self_contained, link_self_contained_components)?;
-        key!(pre_link_args_json = "pre-link-args", link_args);
-        key!(late_link_args_json = "late-link-args", link_args);
-        key!(late_link_args_dynamic_json = "late-link-args-dynamic", link_args);
-        key!(late_link_args_static_json = "late-link-args-static", link_args);
-        key!(post_link_args_json = "post-link-args", link_args);
-        key!(link_script, optional);
-        key!(link_env, env);
-        key!(link_env_remove, list);
-        key!(asm_args, list);
-        key!(cpu);
-        key!(features);
-        key!(dynamic_linking, bool);
-        key!(direct_access_external_data, Option<bool>);
-        key!(dll_tls_export, bool);
-        key!(only_cdylib, bool);
-        key!(executables, bool);
-        key!(relocation_model, RelocModel)?;
-        key!(code_model, CodeModel)?;
-        key!(tls_model, TlsModel)?;
-        key!(disable_redzone, bool);
-        key!(function_sections, bool);
-        key!(dll_prefix);
-        key!(dll_suffix);
-        key!(exe_suffix);
-        key!(staticlib_prefix);
-        key!(staticlib_suffix);
-        key!(families, TargetFamilies);
-        key!(abi_return_struct_as_int, bool);
-        key!(is_like_aix, bool);
-        key!(is_like_osx, bool);
-        key!(is_like_solaris, bool);
-        key!(is_like_windows, bool);
-        key!(is_like_msvc, bool);
-        key!(is_like_wasm, bool);
-        key!(is_like_android, bool);
-        key!(default_dwarf_version, u32);
-        key!(allows_weak_linkage, bool);
-        key!(has_rpath, bool);
-        key!(no_default_libraries, bool);
-        key!(position_independent_executables, bool);
-        key!(static_position_independent_executables, bool);
-        key!(plt_by_default, bool);
-        key!(relro_level, RelroLevel)?;
-        key!(archive_format);
-        key!(allow_asm, bool);
-        key!(main_needs_argc_argv, bool);
-        key!(has_thread_local, bool);
-        key!(obj_is_bitcode, bool);
-        key!(bitcode_llvm_cmdline);
-        key!(max_atomic_width, Option<u64>);
-        key!(min_atomic_width, Option<u64>);
-        key!(atomic_cas, bool);
-        key!(panic_strategy, PanicStrategy)?;
-        key!(crt_static_allows_dylibs, bool);
-        key!(crt_static_default, bool);
-        key!(crt_static_respected, bool);
-        key!(stack_probes, StackProbeType)?;
-        key!(min_global_align, Option<u64>);
-        key!(default_codegen_units, Option<u64>);
-        key!(trap_unreachable, bool);
-        key!(requires_lto, bool);
-        key!(singlethread, bool);
-        key!(no_builtins, bool);
-        key!(default_visibility, Option<SymbolVisibility>)?;
-        key!(emit_debug_gdb_scripts, bool);
-        key!(requires_uwtable, bool);
-        key!(default_uwtable, bool);
-        key!(simd_types_indirect, bool);
-        key!(limit_rdylib_exports, bool);
-        key!(override_export_symbols, opt_list);
-        key!(merge_functions, MergeFunctions)?;
-        key!(mcount = "target-mcount");
-        key!(llvm_mcount_intrinsic, optional);
-        key!(llvm_abiname);
-        key!(relax_elf_relocations, bool);
-        key!(llvm_args, list);
-        key!(use_ctors_section, bool);
-        key!(eh_frame_header, bool);
-        key!(has_thumb_interworking, bool);
-        key!(debuginfo_kind, DebuginfoKind)?;
-        key!(split_debuginfo, SplitDebuginfo)?;
-        key!(supported_split_debuginfo, fallible_list)?;
-        key!(supported_sanitizers, SanitizerSet)?;
-        key!(generate_arange_section, bool);
-        key!(supports_stack_protector, bool);
-        key!(small_data_threshold_support, SmallDataThresholdSupport)?;
-        key!(entry_name);
-        key!(entry_abi, Conv)?;
-        key!(supports_xray, bool);
-
-        base.update_from_cli();
-
-        // Each field should have been read using `Json::remove` so any keys remaining are unused.
-        let remaining_keys = obj.keys();
-        Ok((base, TargetWarnings {
-            unused_fields: remaining_keys.cloned().collect(),
-            incorrect_type,
-        }))
+        Ok(())
+    }
+
+    /// Test target self-consistency and JSON encoding/decoding roundtrip.
+    #[cfg(test)]
+    fn test_target(mut self) {
+        let recycled_target = Target::from_json(self.to_json()).map(|(j, _)| j);
+        self.update_to_cli();
+        self.check_consistency(TargetKind::Builtin).unwrap();
+        assert_eq!(recycled_target, Ok(self));
+    }
+
+    // Add your target to the whitelist if it has `std` library
+    // and you certainly want "unknown" for the OS name.
+    fn can_use_os_unknown(&self) -> bool {
+        self.llvm_target == "wasm32-unknown-unknown"
+            || self.llvm_target == "wasm64-unknown-unknown"
+            || (self.env == "sgx" && self.vendor == "fortanix")
     }
 
     /// Load a built-in target
@@ -3563,177 +3308,6 @@ impl Target {
     }
 }
 
-impl ToJson for Target {
-    fn to_json(&self) -> Json {
-        let mut d = serde_json::Map::new();
-        let default: TargetOptions = Default::default();
-        let mut target = self.clone();
-        target.update_to_cli();
-
-        macro_rules! target_val {
-            ($attr:ident) => {{
-                let name = (stringify!($attr)).replace("_", "-");
-                d.insert(name, target.$attr.to_json());
-            }};
-        }
-
-        macro_rules! target_option_val {
-            ($attr:ident) => {{
-                let name = (stringify!($attr)).replace("_", "-");
-                if default.$attr != target.$attr {
-                    d.insert(name, target.$attr.to_json());
-                }
-            }};
-            ($attr:ident, $json_name:expr) => {{
-                let name = $json_name;
-                if default.$attr != target.$attr {
-                    d.insert(name.into(), target.$attr.to_json());
-                }
-            }};
-            (link_args - $attr:ident, $json_name:expr) => {{
-                let name = $json_name;
-                if default.$attr != target.$attr {
-                    let obj = target
-                        .$attr
-                        .iter()
-                        .map(|(k, v)| (k.desc().to_string(), v.clone()))
-                        .collect::<BTreeMap<_, _>>();
-                    d.insert(name.to_string(), obj.to_json());
-                }
-            }};
-            (env - $attr:ident) => {{
-                let name = (stringify!($attr)).replace("_", "-");
-                if default.$attr != target.$attr {
-                    let obj = target
-                        .$attr
-                        .iter()
-                        .map(|&(ref k, ref v)| format!("{k}={v}"))
-                        .collect::<Vec<_>>();
-                    d.insert(name, obj.to_json());
-                }
-            }};
-        }
-
-        target_val!(llvm_target);
-        target_val!(metadata);
-        d.insert("target-pointer-width".to_string(), self.pointer_width.to_string().to_json());
-        target_val!(arch);
-        target_val!(data_layout);
-
-        target_option_val!(endian, "target-endian");
-        target_option_val!(c_int_width, "target-c-int-width");
-        target_option_val!(os);
-        target_option_val!(env);
-        target_option_val!(abi);
-        target_option_val!(vendor);
-        target_option_val!(linker);
-        target_option_val!(linker_flavor_json, "linker-flavor");
-        target_option_val!(lld_flavor_json, "lld-flavor");
-        target_option_val!(linker_is_gnu_json, "linker-is-gnu");
-        target_option_val!(pre_link_objects);
-        target_option_val!(post_link_objects);
-        target_option_val!(pre_link_objects_self_contained, "pre-link-objects-fallback");
-        target_option_val!(post_link_objects_self_contained, "post-link-objects-fallback");
-        target_option_val!(link_args - pre_link_args_json, "pre-link-args");
-        target_option_val!(link_args - late_link_args_json, "late-link-args");
-        target_option_val!(link_args - late_link_args_dynamic_json, "late-link-args-dynamic");
-        target_option_val!(link_args - late_link_args_static_json, "late-link-args-static");
-        target_option_val!(link_args - post_link_args_json, "post-link-args");
-        target_option_val!(link_script);
-        target_option_val!(env - link_env);
-        target_option_val!(link_env_remove);
-        target_option_val!(asm_args);
-        target_option_val!(cpu);
-        target_option_val!(features);
-        target_option_val!(dynamic_linking);
-        target_option_val!(direct_access_external_data);
-        target_option_val!(dll_tls_export);
-        target_option_val!(only_cdylib);
-        target_option_val!(executables);
-        target_option_val!(relocation_model);
-        target_option_val!(code_model);
-        target_option_val!(tls_model);
-        target_option_val!(disable_redzone);
-        target_option_val!(frame_pointer);
-        target_option_val!(function_sections);
-        target_option_val!(dll_prefix);
-        target_option_val!(dll_suffix);
-        target_option_val!(exe_suffix);
-        target_option_val!(staticlib_prefix);
-        target_option_val!(staticlib_suffix);
-        target_option_val!(families, "target-family");
-        target_option_val!(abi_return_struct_as_int);
-        target_option_val!(is_like_aix);
-        target_option_val!(is_like_osx);
-        target_option_val!(is_like_solaris);
-        target_option_val!(is_like_windows);
-        target_option_val!(is_like_msvc);
-        target_option_val!(is_like_wasm);
-        target_option_val!(is_like_android);
-        target_option_val!(default_dwarf_version);
-        target_option_val!(allows_weak_linkage);
-        target_option_val!(has_rpath);
-        target_option_val!(no_default_libraries);
-        target_option_val!(position_independent_executables);
-        target_option_val!(static_position_independent_executables);
-        target_option_val!(plt_by_default);
-        target_option_val!(relro_level);
-        target_option_val!(archive_format);
-        target_option_val!(allow_asm);
-        target_option_val!(main_needs_argc_argv);
-        target_option_val!(has_thread_local);
-        target_option_val!(obj_is_bitcode);
-        target_option_val!(bitcode_llvm_cmdline);
-        target_option_val!(min_atomic_width);
-        target_option_val!(max_atomic_width);
-        target_option_val!(atomic_cas);
-        target_option_val!(panic_strategy);
-        target_option_val!(crt_static_allows_dylibs);
-        target_option_val!(crt_static_default);
-        target_option_val!(crt_static_respected);
-        target_option_val!(stack_probes);
-        target_option_val!(min_global_align);
-        target_option_val!(default_codegen_units);
-        target_option_val!(trap_unreachable);
-        target_option_val!(requires_lto);
-        target_option_val!(singlethread);
-        target_option_val!(no_builtins);
-        target_option_val!(default_visibility);
-        target_option_val!(emit_debug_gdb_scripts);
-        target_option_val!(requires_uwtable);
-        target_option_val!(default_uwtable);
-        target_option_val!(simd_types_indirect);
-        target_option_val!(limit_rdylib_exports);
-        target_option_val!(override_export_symbols);
-        target_option_val!(merge_functions);
-        target_option_val!(mcount, "target-mcount");
-        target_option_val!(llvm_mcount_intrinsic);
-        target_option_val!(llvm_abiname);
-        target_option_val!(relax_elf_relocations);
-        target_option_val!(llvm_args);
-        target_option_val!(use_ctors_section);
-        target_option_val!(eh_frame_header);
-        target_option_val!(has_thumb_interworking);
-        target_option_val!(debuginfo_kind);
-        target_option_val!(split_debuginfo);
-        target_option_val!(supported_split_debuginfo);
-        target_option_val!(supported_sanitizers);
-        target_option_val!(c_enum_min_bits);
-        target_option_val!(generate_arange_section);
-        target_option_val!(supports_stack_protector);
-        target_option_val!(small_data_threshold_support);
-        target_option_val!(entry_name);
-        target_option_val!(entry_abi);
-        target_option_val!(supports_xray);
-
-        // Serializing `-Clink-self-contained` needs a dynamic key to support the
-        // backwards-compatible variants.
-        d.insert(self.link_self_contained.json_key().into(), self.link_self_contained.to_json());
-
-        Json::Object(d)
-    }
-}
-
 /// Either a target tuple string or a path to a JSON file.
 #[derive(Clone, Debug)]
 pub enum TargetTuple {
diff --git a/compiler/rustc_target/src/spec/tests/tests_impl.rs b/compiler/rustc_target/src/spec/tests/tests_impl.rs
deleted file mode 100644
index 3225cdc759d..00000000000
--- a/compiler/rustc_target/src/spec/tests/tests_impl.rs
+++ /dev/null
@@ -1,208 +0,0 @@
-use std::assert_matches::assert_matches;
-
-use rustc_data_structures::fx::FxHashSet;
-
-use super::super::*;
-
-// Test target self-consistency and JSON encoding/decoding roundtrip.
-pub(super) fn test_target(mut target: Target) {
-    let recycled_target = Target::from_json(target.to_json()).map(|(j, _)| j);
-    target.update_to_cli();
-    target.check_consistency();
-    assert_eq!(recycled_target, Ok(target));
-}
-
-impl Target {
-    fn check_consistency(&self) {
-        assert_eq!(self.is_like_osx, self.vendor == "apple");
-        assert_eq!(self.is_like_solaris, self.os == "solaris" || self.os == "illumos");
-        assert_eq!(self.is_like_windows, self.os == "windows" || self.os == "uefi");
-        assert_eq!(self.is_like_wasm, self.arch == "wasm32" || self.arch == "wasm64");
-        if self.is_like_msvc {
-            assert!(self.is_like_windows);
-        }
-        if self.os == "emscripten" {
-            assert!(self.is_like_wasm);
-        }
-
-        // Check that default linker flavor is compatible with some other key properties.
-        assert_eq!(self.is_like_osx, matches!(self.linker_flavor, LinkerFlavor::Darwin(..)));
-        assert_eq!(self.is_like_msvc, matches!(self.linker_flavor, LinkerFlavor::Msvc(..)));
-        assert_eq!(
-            self.is_like_wasm && self.os != "emscripten",
-            matches!(self.linker_flavor, LinkerFlavor::WasmLld(..))
-        );
-        assert_eq!(self.os == "emscripten", matches!(self.linker_flavor, LinkerFlavor::EmCc));
-        assert_eq!(self.arch == "bpf", matches!(self.linker_flavor, LinkerFlavor::Bpf));
-        assert_eq!(self.arch == "nvptx64", matches!(self.linker_flavor, LinkerFlavor::Ptx));
-
-        for args in [
-            &self.pre_link_args,
-            &self.late_link_args,
-            &self.late_link_args_dynamic,
-            &self.late_link_args_static,
-            &self.post_link_args,
-        ] {
-            for (&flavor, flavor_args) in args {
-                assert!(!flavor_args.is_empty());
-                // Check that flavors mentioned in link args are compatible with the default flavor.
-                match self.linker_flavor {
-                    LinkerFlavor::Gnu(..) => {
-                        assert_matches!(flavor, LinkerFlavor::Gnu(..));
-                    }
-                    LinkerFlavor::Darwin(..) => {
-                        assert_matches!(flavor, LinkerFlavor::Darwin(..))
-                    }
-                    LinkerFlavor::WasmLld(..) => {
-                        assert_matches!(flavor, LinkerFlavor::WasmLld(..))
-                    }
-                    LinkerFlavor::Unix(..) => {
-                        assert_matches!(flavor, LinkerFlavor::Unix(..));
-                    }
-                    LinkerFlavor::Msvc(..) => {
-                        assert_matches!(flavor, LinkerFlavor::Msvc(..))
-                    }
-                    LinkerFlavor::EmCc
-                    | LinkerFlavor::Bpf
-                    | LinkerFlavor::Ptx
-                    | LinkerFlavor::Llbc => {
-                        assert_eq!(flavor, self.linker_flavor)
-                    }
-                }
-
-                // Check that link args for cc and non-cc versions of flavors are consistent.
-                let check_noncc = |noncc_flavor| {
-                    if let Some(noncc_args) = args.get(&noncc_flavor) {
-                        for arg in flavor_args {
-                            if let Some(suffix) = arg.strip_prefix("-Wl,") {
-                                assert!(noncc_args.iter().any(|a| a == suffix));
-                            }
-                        }
-                    }
-                };
-
-                match self.linker_flavor {
-                    LinkerFlavor::Gnu(Cc::Yes, lld) => check_noncc(LinkerFlavor::Gnu(Cc::No, lld)),
-                    LinkerFlavor::WasmLld(Cc::Yes) => check_noncc(LinkerFlavor::WasmLld(Cc::No)),
-                    LinkerFlavor::Unix(Cc::Yes) => check_noncc(LinkerFlavor::Unix(Cc::No)),
-                    _ => {}
-                }
-            }
-
-            // Check that link args for lld and non-lld versions of flavors are consistent.
-            for cc in [Cc::No, Cc::Yes] {
-                assert_eq!(
-                    args.get(&LinkerFlavor::Gnu(cc, Lld::No)),
-                    args.get(&LinkerFlavor::Gnu(cc, Lld::Yes)),
-                );
-                assert_eq!(
-                    args.get(&LinkerFlavor::Darwin(cc, Lld::No)),
-                    args.get(&LinkerFlavor::Darwin(cc, Lld::Yes)),
-                );
-            }
-            assert_eq!(
-                args.get(&LinkerFlavor::Msvc(Lld::No)),
-                args.get(&LinkerFlavor::Msvc(Lld::Yes)),
-            );
-        }
-
-        if self.link_self_contained.is_disabled() {
-            assert!(
-                self.pre_link_objects_self_contained.is_empty()
-                    && self.post_link_objects_self_contained.is_empty()
-            );
-        }
-
-        // If your target really needs to deviate from the rules below,
-        // except it and document the reasons.
-        // Keep the default "unknown" vendor instead.
-        assert_ne!(self.vendor, "");
-        assert_ne!(self.os, "");
-        if !self.can_use_os_unknown() {
-            // Keep the default "none" for bare metal targets instead.
-            assert_ne!(self.os, "unknown");
-        }
-
-        // Check dynamic linking stuff
-        // BPF: when targeting user space vms (like rbpf), those can load dynamic libraries.
-        // hexagon: when targeting QuRT, that OS can load dynamic libraries.
-        // wasm{32,64}: dynamic linking is inherent in the definition of the VM.
-        if self.os == "none"
-            && (self.arch != "bpf"
-                && self.arch != "hexagon"
-                && self.arch != "wasm32"
-                && self.arch != "wasm64")
-        {
-            assert!(!self.dynamic_linking);
-        }
-        if self.only_cdylib
-            || self.crt_static_allows_dylibs
-            || !self.late_link_args_dynamic.is_empty()
-        {
-            assert!(self.dynamic_linking);
-        }
-        // Apparently PIC was slow on wasm at some point, see comments in wasm_base.rs
-        if self.dynamic_linking && !self.is_like_wasm {
-            assert_eq!(self.relocation_model, RelocModel::Pic);
-        }
-        if self.position_independent_executables {
-            assert_eq!(self.relocation_model, RelocModel::Pic);
-        }
-        // The UEFI targets do not support dynamic linking but still require PIC (#101377).
-        if self.relocation_model == RelocModel::Pic && self.os != "uefi" {
-            assert!(self.dynamic_linking || self.position_independent_executables);
-        }
-        if self.static_position_independent_executables {
-            assert!(self.position_independent_executables);
-        }
-        if self.position_independent_executables {
-            assert!(self.executables);
-        }
-
-        // Check crt static stuff
-        if self.crt_static_default || self.crt_static_allows_dylibs {
-            assert!(self.crt_static_respected);
-        }
-
-        // Check that RISC-V targets always specify which ABI they use.
-        match &*self.arch {
-            "riscv32" => {
-                assert_matches!(&*self.llvm_abiname, "ilp32" | "ilp32f" | "ilp32d" | "ilp32e")
-            }
-            "riscv64" => {
-                // Note that the `lp64e` is still unstable as it's not (yet) part of the ELF psABI.
-                assert_matches!(&*self.llvm_abiname, "lp64" | "lp64f" | "lp64d" | "lp64q" | "lp64e")
-            }
-            _ => {}
-        }
-
-        // Check that the given target-features string makes some basic sense.
-        if !self.features.is_empty() {
-            let mut features_enabled = FxHashSet::default();
-            let mut features_disabled = FxHashSet::default();
-            for feat in self.features.split(',') {
-                if let Some(feat) = feat.strip_prefix("+") {
-                    features_enabled.insert(feat);
-                    if features_disabled.contains(feat) {
-                        panic!("target feature `{feat}` is both enabled and disabled");
-                    }
-                } else if let Some(feat) = feat.strip_prefix("-") {
-                    features_disabled.insert(feat);
-                    if features_enabled.contains(feat) {
-                        panic!("target feature `{feat}` is both enabled and disabled");
-                    }
-                } else {
-                    panic!("target feature `{feat}` is invalid, must start with `+` or `-`");
-                }
-            }
-        }
-    }
-
-    // Add your target to the whitelist if it has `std` library
-    // and you certainly want "unknown" for the OS name.
-    fn can_use_os_unknown(&self) -> bool {
-        self.llvm_target == "wasm32-unknown-unknown"
-            || self.llvm_target == "wasm64-unknown-unknown"
-            || (self.env == "sgx" && self.vendor == "fortanix")
-    }
-}
diff --git a/tests/ui/codegen/mismatched-data-layout.json b/tests/ui/codegen/mismatched-data-layout.json
index 4cb0602dc75..7adc8832524 100644
--- a/tests/ui/codegen/mismatched-data-layout.json
+++ b/tests/ui/codegen/mismatched-data-layout.json
@@ -5,7 +5,7 @@
     "target-endian": "little",
     "target-pointer-width": "64",
     "target-c-int-width": "32",
-    "os": "unknown",
+    "os": "none",
     "linker-flavor": "ld.lld",
     "linker": "rust-lld",
     "executables": true
diff --git a/tests/ui/codegen/mismatched-data-layouts.stderr b/tests/ui/codegen/mismatched-data-layouts.stderr
index 1fe242266df..b7d5d82bee0 100644
--- a/tests/ui/codegen/mismatched-data-layouts.stderr
+++ b/tests/ui/codegen/mismatched-data-layouts.stderr
@@ -1,4 +1,4 @@
-error: data-layout for target `mismatched-data-layout-7814813422914914169`, `normalized data layout`, differs from LLVM target's `x86_64-unknown-none-gnu` default layout, `normalized data layout`
+error: data-layout for target `mismatched-data-layout-7193370089426056427`, `normalized data layout`, differs from LLVM target's `x86_64-unknown-none-gnu` default layout, `normalized data layout`
 
 error: aborting due to 1 previous error