diff --git a/compiler/rustc_codegen_ssa/src/back/link.rs b/compiler/rustc_codegen_ssa/src/back/link.rs index 8cbf3647f5630..b9e3ba4ab9ada 100644 --- a/compiler/rustc_codegen_ssa/src/back/link.rs +++ b/compiler/rustc_codegen_ssa/src/back/link.rs @@ -1073,27 +1073,43 @@ fn report_linker_output( escape_string(output.trim().as_bytes()) } + fn has_lnk_code(line: &str) -> bool { + // link.exe diagnostics are structured as `LINK : warning LNK####:` or + // `LINK : fatal error LNK####:`. The code is always followed by a `:` + // that is the second colon in the line, so matching that structure + // instead of scanning for `LNK####` anywhere avoids false positives on + // file names. + let Some((code_colon, _)) = line.match_indices(':').nth(1) else { + return false; + }; + let Some(code) = code_colon.checked_sub(7) else { + return false; + }; + let code = &line.as_bytes()[code..code_colon]; + code.starts_with(b"LNK") && code[3..].iter().all(u8::is_ascii_digit) + } + if is_msvc_link_exe(sess) { info!("inferred MSVC link.exe"); escaped_stdout = for_each(&stdout, |line, output| { - // Hide some progress messages from link.exe that we don't care about. - // See https://github.com/chromium/chromium/blob/bfa41e41145ffc85f041384280caf2949bb7bd72/build/toolchain/win/tool_wrapper.py#L144-L146 - // When incremental linking is enabled and an .ilk exists, but its associated .exe is - // missing, link.exe prints the path of the missing .exe followed by: + // Hide progress messages from link.exe that we don't care about. + // These include localized variants of the English messages (e.g. + // "Creating library ..."), which rustc cannot recognize by text + // without the English language pack. + // See https://github.com/rust-lang/rust/issues/159133 + // When incremental linking is enabled and an .ilk exists, but its + // associated .exe is missing, link.exe prints the path of the + // missing .exe followed by: let ilk_but_no_exe = "not found or not built by the last incremental link; performing full link"; - let trimmed = line.trim_start(); - if trimmed.starts_with("Creating library") - || trimmed.starts_with("Generating code") - || trimmed.starts_with("Finished generating code") - || trimmed.ends_with(ilk_but_no_exe) - { - linker_info += line; - linker_info += "\r\n"; - } else { + // LNK6004 is the one code-bearing line that is still informational. + if has_lnk_code(line) && !line.ends_with(ilk_but_no_exe) { *output += line; *output += "\r\n" + } else { + linker_info += line; + linker_info += "\r\n"; } }); } else if is_macos_linker(sess) { diff --git a/tests/run-make/msvc-localized-linker-output/fake-linker.rs b/tests/run-make/msvc-localized-linker-output/fake-linker.rs new file mode 100644 index 0000000000000..2cbb68c4518bf --- /dev/null +++ b/tests/run-make/msvc-localized-linker-output/fake-linker.rs @@ -0,0 +1,22 @@ +fn main() { + // Simulate a localized (e.g. Japanese) `link.exe`, as printed when the + // English language pack is not installed and `VSLANG=1033` has no effect. + // This is "Creating library foo.dll.lib and object foo.dll.exp" in Japanese. + println!("ライブラリ foo.dll.lib とオブジェクト foo.dll.exp を作成中"); + // A file name containing an `LNK####`-looking fragment must not be + // mistaken for a diagnostic, which is why the matcher requires the + // structured `LINK : warning LNK####:` form. + println!("LNK2001.lib: progress message, not a diagnostic"); + for arg in std::env::args() { + if arg == "run_make_lnk" { + // Real diagnostics are structured as `LINK : warning LNK####:`. + println!("LINK : warning LNK2001: unresolved external symbol foo"); + // The one code-bearing informational line has no `LINK : ` prefix + // and keeps the exception that classifies it as `linker_info`. + println!( + "LNK6004: 'foo.exe' not found or not built by the last incremental link; \ + performing full link" + ); + } + } +} diff --git a/tests/run-make/msvc-localized-linker-output/main.rs b/tests/run-make/msvc-localized-linker-output/main.rs new file mode 100644 index 0000000000000..f328e4d9d04c3 --- /dev/null +++ b/tests/run-make/msvc-localized-linker-output/main.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/tests/run-make/msvc-localized-linker-output/rmake.rs b/tests/run-make/msvc-localized-linker-output/rmake.rs new file mode 100644 index 0000000000000..3d0e4c0d7b61d --- /dev/null +++ b/tests/run-make/msvc-localized-linker-output/rmake.rs @@ -0,0 +1,67 @@ +//@ only-msvc +//@ ignore-cross-compile (need to run the fake link.exe on the host) + +//! Tests that localized (non-English) MSVC `link.exe` progress messages are +//! classified as `linker_info`, not `linker_messages`. +//! +//! `link.exe` is hardcoded by rustc to run with `VSLANG=1033`, which only works +//! when an English language pack is installed. Without it, messages like +//! "Creating library ..." are printed in another language, and the English +//! string matching that used to detect them fails. Since all real diagnostics +//! carry a locale-independent `LNK####` code, printed in the structured +//! `LINK : warning LNK####:` form, any line without one is informational, no +//! matter the language it was printed in. + +use run_make_support::{bare_rustc, rustc, target}; + +fn main() { + // rustc prepends the sysroot's tools bin directory to the linker's `PATH`, + // which bare names like `link.exe` are resolved against. Put the fake + // `link.exe` there so it wins over the real linker; `-L` below keeps std + // available from the real sysroot. + let fake_sysroot = std::env::current_dir().unwrap().join("fake-sysroot"); + let tools_bin = fake_sysroot.join(format!("lib/rustlib/{}/bin", target())); + std::fs::create_dir_all(&tools_bin).unwrap(); + rustc().arg("fake-linker.rs").output(tools_bin.join("link.exe")).run(); + + let real_libdir = rustc().print("target-libdir").run().stdout_utf8(); + let real_libdir = real_libdir.trim(); + + let fake_link = |extra: &[&str]| { + let mut r = bare_rustc(); + r.input("main.rs") + .output("main") + .arg(format!("--sysroot={}", fake_sysroot.display())) + .arg(format!("-L{real_libdir}")) + // Matched by name against the linker's `PATH`, so the fake in the + // tools bin directory is used instead of the real VS linker. + .arg("-Clinker=link.exe") + // Overrides `rust.lld=true` on CI. + .arg("-Clinker-flavor=msvc"); + for a in extra { + r.arg(a); + } + r + }; + + // The localized progress line must not warn by default. + fake_link(&[]) + .run() + .assert_stderr_not_contains("linker stdout") + .assert_stderr_not_contains("ライブラリ foo.dll.lib とオブジェクト foo.dll.exp を作成中"); + + // It is still visible through `linker_info`, and must not be misclassified + // as `linker_messages`. + fake_link(&["-Wlinker_info", "-Dlinker_messages"]) // Fail if the message is misclassified. + .run() + .assert_stderr_contains("ライブラリ foo.dll.lib とオブジェクト foo.dll.exp を作成中"); + + // Real diagnostics keep their `LNK####` code and still warn. + fake_link(&["-Clink-arg=run_make_lnk"]) + .run() + .assert_stderr_contains( + "warning: linker stdout: LINK : warning LNK2001: unresolved external symbol foo", + ) + // The informational LNK6004 line stays hidden. + .assert_stderr_not_contains("LNK6004"); +}