diff --git a/prdoc/pr_12726.prdoc b/prdoc/pr_12726.prdoc new file mode 100644 index 000000000000..a7ee7754ff8e --- /dev/null +++ b/prdoc/pr_12726.prdoc @@ -0,0 +1,22 @@ +title: 'Probe cargo for `-Z json-target-spec` support instead of guessing from the version' + +doc: + - audience: Runtime Dev + description: | + Both PolkaVM build paths compile against a `.json` target spec produced by + `polkavm-linker` and have to decide whether to pass cargo `-Z json-target-spec`: + newer cargo requires the flag, older cargo rejects it as unknown. + + Both decided by comparing a version against 1.95: the fixture builder against the + *rustc* version, `wasm-builder` against the cargo version. Neither works. The flag + landed partway through the 1.95 cycle, so a nightly from early in that cycle reports + 1.95 from both binaries while its cargo does not know the flag yet, and the fixture + build fails with `error: unknown -Z flag specified: json-target-spec`. + + Ask cargo which unstable flags it accepts instead of inferring it from a version. + +crates: + - name: pallet-revive-fixtures + bump: patch + - name: substrate-wasm-builder + bump: patch diff --git a/substrate/frame/revive/fixtures/src/builder.rs b/substrate/frame/revive/fixtures/src/builder.rs index 74f33b3e1324..19128028189b 100644 --- a/substrate/frame/revive/fixtures/src/builder.rs +++ b/substrate/frame/revive/fixtures/src/builder.rs @@ -198,18 +198,25 @@ pub fn invoke_build(current_dir: &Path) -> Result<()> { let toolchain = env::var(OVERRIDE_RUSTUP_TOOLCHAIN_ENV_VAR).or_else(|_| env::var("RUSTUP_TOOLCHAIN")); - // Detect rustc major.minor for the version-gated flags below. - let (major, minor) = { - let mut cmd = Command::new("rustc"); + // Ask a tool of the toolchain we are about to build with what it supports. + let probe = |program: &str, args: &[&str]| -> Result { + let mut cmd = Command::new(program); + // `-Z` flags are nightly-only, so make a stable toolchain answer as a nightly would. + cmd.env("RUSTC_BOOTSTRAP", "1"); if let Ok(toolchain) = &toolchain { cmd.env("RUSTUP_TOOLCHAIN", toolchain); } - let out = cmd.arg("--version").output().context("rustc --version failed")?; - let ver = String::from_utf8(out.stdout).context("utf8 from rustc --version failed")?; + let out = cmd.args(args).output().with_context(|| format!("{program} {args:?} failed"))?; + String::from_utf8(out.stdout).with_context(|| format!("{program} output is not utf8")) + }; + + // Detect the major.minor of a tool for the version-gated flags below. + let version = |program: &str| -> Result<(u32, u32)> { + let ver = probe(program, &["--version"])?; let ver_num = ver .split_whitespace() .nth(1) - .ok_or_else(|| anyhow::anyhow!("unexpected rustc --version output: {ver}"))?; + .ok_or_else(|| anyhow::anyhow!("unexpected {program} --version output: {ver}"))?; let mut parts = ver_num.split('.'); let major: u32 = parts .next() @@ -221,17 +228,24 @@ pub fn invoke_build(current_dir: &Path) -> Result<()> { .ok_or_else(|| anyhow::anyhow!("missing minor version"))? .parse() .context("invalid minor version")?; - (major, minor) + Ok((major, minor)) }; + // `rustc` and `cargo` ship together but a flag can land in one before the other, so gate + // each flag on the version of the binary that parses it. // 1.92+: `-Cpanic=immediate-abort` is a stable rustflag. - let new_immediate_abort = major > 1 || (major == 1 && minor >= 92); - // 1.95+: `-Z json-target-spec` was added; later rustc requires it for any - // `--target=*.json` invocation. Pre-1.95 doesn't know the flag. - let needs_json_target_spec = major > 1 || (major == 1 && minor >= 95); + let rustc_immediate_abort = version("rustc")? >= (1, 92); + // Older toolchains request the same from cargo when it builds `core` instead. + let cargo_immediate_abort = version("cargo")? < (1, 92); + + // `-Z json-target-spec` is a *cargo* flag: newer cargo requires it to be opted into for + // any `--target=*.json` invocation, older cargo rejects it as unknown. It landed partway + // through the 1.95 cycle, so a toolchain from early in that cycle reports 1.95 while its + // cargo does not know the flag yet: no version comparison can express this. + let supports_json_target_spec = probe("cargo", &["-Z", "help"])?.contains("json-target-spec"); // on newer version this is a stable compiler flag - let encoded_rustflags = if new_immediate_abort { + let encoded_rustflags = if rustc_immediate_abort { ["-Dwarnings", "-Zunstable-options", "-Cpanic=immediate-abort"].join("\x1f") } else { ["-Dwarnings"].join("\x1f") @@ -252,13 +266,13 @@ pub fn invoke_build(current_dir: &Path) -> Result<()> { .arg("--target") .arg(polkavm_linker::target_json_path(args).unwrap()); - // 1.95+: opt into JSON target specs explicitly (required by recent rustc). - if needs_json_target_spec { + // opt into JSON target specs explicitly where cargo understands the flag + if supports_json_target_spec { build_command.arg("-Zjson-target-spec"); } // on older versions this is a unstable cargo cli argument - if !new_immediate_abort { + if cargo_immediate_abort { build_command.arg("-Zbuild-std-features=panic_immediate_abort"); } diff --git a/substrate/utils/wasm-builder/src/lib.rs b/substrate/utils/wasm-builder/src/lib.rs index cfa8863ab623..3e21bf45f78c 100644 --- a/substrate/utils/wasm-builder/src/lib.rs +++ b/substrate/utils/wasm-builder/src/lib.rs @@ -334,6 +334,21 @@ impl CargoCommand { version.major > 1 || (version.major == 1 && version.minor >= 68) || version.is_nightly } + /// Returns whether this cargo accepts the `-Z json-target-spec` flag. + /// + /// Newer cargo requires the flag to be opted into for any `--target=*.json` invocation, + /// older cargo rejects it as unknown. It landed partway through the 1.95 cycle, so a + /// toolchain from early in that cycle reports 1.95 while its cargo does not know the + /// flag yet: no version comparison can express this, hence the probe. + fn supports_json_target_spec(&self) -> bool { + // `-Z` flags are nightly-only, so make a stable cargo answer as a nightly would. + self.command() + .env("RUSTC_BOOTSTRAP", "1") + .args(["-Z", "help"]) + .output() + .is_ok_and(|out| String::from_utf8_lossy(&out.stdout).contains("json-target-spec")) + } + /// Returns whether this version of the toolchain supports the `wasm32v1-none` target. fn supports_wasm32v1_none_target(&self) -> bool { self.version.map_or(false, |version| { diff --git a/substrate/utils/wasm-builder/src/wasm_project.rs b/substrate/utils/wasm-builder/src/wasm_project.rs index 5ce7e1aa6dea..14ab87358de3 100644 --- a/substrate/utils/wasm-builder/src/wasm_project.rs +++ b/substrate/utils/wasm-builder/src/wasm_project.rs @@ -1023,15 +1023,9 @@ fn build_bloaty_blob( } // `--target` for the Riscv runtime points at a JSON target spec produced by - // `polkavm-linker`. Rust 1.95 added `-Z json-target-spec`; later versions - // require it to be opted into explicitly. Older versions don't know the flag, - // so guard on the version. `RUSTC_BOOTSTRAP=1` is already set by the - // `-Z build-std` block above (Riscv always opts into `build-std`). - if matches!(target, RuntimeTarget::Riscv) && - cargo_cmd - .version() - .is_some_and(|v| v.major > 1 || (v.major == 1 && v.minor >= 95)) - { + // `polkavm-linker`. Newer cargo requires opting into those explicitly, older cargo + // does not know the flag at all, so ask this cargo which of the two it is. + if matches!(target, RuntimeTarget::Riscv) && cargo_cmd.supports_json_target_spec() { build_cmd.arg("-Z").arg("json-target-spec"); }