Skip to content

Commit a6a9d4c

Browse files
committed
Auto merge of #44094 - alexcrichton:long-linkers, r=michaelwoerister
rustc: Attempt to handle super long linker invocations This commit adds logic to the compiler to attempt to handle super long linker invocations by falling back to the `@`-file syntax if the invoked command is too large. Each OS has a limit on how many arguments and how large the arguments can be when spawning a new process, and linkers tend to be one of those programs that can hit the limit! The logic implemented here is to unconditionally attempt to spawn a linker and then if it fails to spawn with an error from the OS that indicates the command line is too big we attempt a fallback. The fallback is roughly the same for all linkers where an argument pointing to a file, prepended with `@`, is passed. This file then contains all the various arguments that we want to pass to the linker. Closes #41190
2 parents 05e3c96 + ed938f0 commit a6a9d4c

File tree

7 files changed

+316
-6
lines changed

7 files changed

+316
-6
lines changed

src/librustc_trans/back/command.rs

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright 2017 The Rust Project Developers. See the COPYRIGHT
2+
// file at the top-level directory of this distribution and at
3+
// http://rust-lang.org/COPYRIGHT.
4+
//
5+
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6+
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7+
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8+
// option. This file may not be copied, modified, or distributed
9+
// except according to those terms.
10+
11+
//! A thin wrapper around `Command` in the standard library which allows us to
12+
//! read the arguments that are built up.
13+
14+
use std::ffi::{OsStr, OsString};
15+
use std::fmt;
16+
use std::io;
17+
use std::process::{self, Output, Child};
18+
19+
pub struct Command {
20+
program: OsString,
21+
args: Vec<OsString>,
22+
env: Vec<(OsString, OsString)>,
23+
}
24+
25+
impl Command {
26+
pub fn new<P: AsRef<OsStr>>(program: P) -> Command {
27+
Command::_new(program.as_ref())
28+
}
29+
30+
fn _new(program: &OsStr) -> Command {
31+
Command {
32+
program: program.to_owned(),
33+
args: Vec::new(),
34+
env: Vec::new(),
35+
}
36+
}
37+
38+
pub fn arg<P: AsRef<OsStr>>(&mut self, arg: P) -> &mut Command {
39+
self._arg(arg.as_ref());
40+
self
41+
}
42+
43+
pub fn args<I>(&mut self, args: I) -> &mut Command
44+
where I: IntoIterator,
45+
I::Item: AsRef<OsStr>,
46+
{
47+
for arg in args {
48+
self._arg(arg.as_ref());
49+
}
50+
self
51+
}
52+
53+
fn _arg(&mut self, arg: &OsStr) {
54+
self.args.push(arg.to_owned());
55+
}
56+
57+
pub fn env<K, V>(&mut self, key: K, value: V) -> &mut Command
58+
where K: AsRef<OsStr>,
59+
V: AsRef<OsStr>
60+
{
61+
self._env(key.as_ref(), value.as_ref());
62+
self
63+
}
64+
65+
pub fn envs<I, K, V>(&mut self, envs: I) -> &mut Command
66+
where I: IntoIterator<Item=(K, V)>,
67+
K: AsRef<OsStr>,
68+
V: AsRef<OsStr>
69+
{
70+
for (key, value) in envs {
71+
self._env(key.as_ref(), value.as_ref());
72+
}
73+
self
74+
}
75+
76+
fn _env(&mut self, key: &OsStr, value: &OsStr) {
77+
self.env.push((key.to_owned(), value.to_owned()));
78+
}
79+
80+
pub fn output(&mut self) -> io::Result<Output> {
81+
self.command().output()
82+
}
83+
84+
pub fn spawn(&mut self) -> io::Result<Child> {
85+
self.command().spawn()
86+
}
87+
88+
pub fn command(&self) -> process::Command {
89+
let mut ret = process::Command::new(&self.program);
90+
ret.args(&self.args);
91+
ret.envs(self.env.clone());
92+
return ret
93+
}
94+
95+
// extensions
96+
97+
pub fn get_program(&self) -> &OsStr {
98+
&self.program
99+
}
100+
101+
pub fn get_args(&self) -> &[OsString] {
102+
&self.args
103+
}
104+
105+
pub fn get_env(&self) -> &[(OsString, OsString)] {
106+
&self.env
107+
}
108+
}
109+
110+
impl fmt::Debug for Command {
111+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
112+
self.command().fmt(f)
113+
}
114+
}

src/librustc_trans/back/link.rs

+106-5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ extern crate rustc_trans_utils;
1212

1313
use super::archive::{ArchiveBuilder, ArchiveConfig};
1414
use super::linker::Linker;
15+
use super::command::Command;
1516
use super::rpath::RPathConfig;
1617
use super::rpath;
1718
use metadata::METADATA_FILENAME;
@@ -38,11 +39,12 @@ use std::ascii;
3839
use std::char;
3940
use std::env;
4041
use std::ffi::OsString;
41-
use std::fs;
42-
use std::io::{self, Read, Write};
42+
use std::fmt;
43+
use std::fs::{self, File};
44+
use std::io::{self, Read, Write, BufWriter};
4345
use std::mem;
4446
use std::path::{Path, PathBuf};
45-
use std::process::Command;
47+
use std::process::{Output, Stdio};
4648
use std::str;
4749
use flate2::Compression;
4850
use flate2::write::DeflateEncoder;
@@ -125,8 +127,13 @@ pub fn msvc_link_exe_cmd(sess: &Session) -> (Command, Vec<(OsString, OsString)>)
125127
let tool = windows_registry::find_tool(target, "link.exe");
126128

127129
if let Some(tool) = tool {
130+
let mut cmd = Command::new(tool.path());
131+
cmd.args(tool.args());
132+
for &(ref k, ref v) in tool.env() {
133+
cmd.env(k, v);
134+
}
128135
let envs = tool.env().to_vec();
129-
(tool.to_command(), envs)
136+
(cmd, envs)
130137
} else {
131138
debug!("Failed to locate linker.");
132139
(Command::new("link.exe"), vec![])
@@ -797,7 +804,9 @@ fn link_natively(sess: &Session,
797804
let mut i = 0;
798805
loop {
799806
i += 1;
800-
prog = time(sess.time_passes(), "running linker", || cmd.output());
807+
prog = time(sess.time_passes(), "running linker", || {
808+
exec_linker(sess, &mut cmd, tmpdir)
809+
});
801810
if !retry_on_segfault || i > 3 {
802811
break
803812
}
@@ -875,6 +884,98 @@ fn link_natively(sess: &Session,
875884
}
876885
}
877886

887+
fn exec_linker(sess: &Session, cmd: &mut Command, tmpdir: &Path)
888+
-> io::Result<Output>
889+
{
890+
// When attempting to spawn the linker we run a risk of blowing out the
891+
// size limits for spawning a new process with respect to the arguments
892+
// we pass on the command line.
893+
//
894+
// Here we attempt to handle errors from the OS saying "your list of
895+
// arguments is too big" by reinvoking the linker again with an `@`-file
896+
// that contains all the arguments. The theory is that this is then
897+
// accepted on all linkers and the linker will read all its options out of
898+
// there instead of looking at the command line.
899+
match cmd.command().stdout(Stdio::piped()).stderr(Stdio::piped()).spawn() {
900+
Ok(child) => return child.wait_with_output(),
901+
Err(ref e) if command_line_too_big(e) => {}
902+
Err(e) => return Err(e)
903+
}
904+
905+
let file = tmpdir.join("linker-arguments");
906+
let mut cmd2 = Command::new(cmd.get_program());
907+
cmd2.arg(format!("@{}", file.display()));
908+
for &(ref k, ref v) in cmd.get_env() {
909+
cmd2.env(k, v);
910+
}
911+
let mut f = BufWriter::new(File::create(&file)?);
912+
for arg in cmd.get_args() {
913+
writeln!(f, "{}", Escape {
914+
arg: arg.to_str().unwrap(),
915+
is_like_msvc: sess.target.target.options.is_like_msvc,
916+
})?;
917+
}
918+
f.into_inner()?;
919+
return cmd2.output();
920+
921+
#[cfg(unix)]
922+
fn command_line_too_big(err: &io::Error) -> bool {
923+
err.raw_os_error() == Some(::libc::E2BIG)
924+
}
925+
926+
#[cfg(windows)]
927+
fn command_line_too_big(err: &io::Error) -> bool {
928+
const ERROR_FILENAME_EXCED_RANGE: i32 = 206;
929+
err.raw_os_error() == Some(ERROR_FILENAME_EXCED_RANGE)
930+
}
931+
932+
struct Escape<'a> {
933+
arg: &'a str,
934+
is_like_msvc: bool,
935+
}
936+
937+
impl<'a> fmt::Display for Escape<'a> {
938+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
939+
if self.is_like_msvc {
940+
// This is "documented" at
941+
// https://msdn.microsoft.com/en-us/library/4xdcbak7.aspx
942+
//
943+
// Unfortunately there's not a great specification of the
944+
// syntax I could find online (at least) but some local
945+
// testing showed that this seemed sufficient-ish to catch
946+
// at least a few edge cases.
947+
write!(f, "\"")?;
948+
for c in self.arg.chars() {
949+
match c {
950+
'"' => write!(f, "\\{}", c)?,
951+
c => write!(f, "{}", c)?,
952+
}
953+
}
954+
write!(f, "\"")?;
955+
} else {
956+
// This is documented at https://linux.die.net/man/1/ld, namely:
957+
//
958+
// > Options in file are separated by whitespace. A whitespace
959+
// > character may be included in an option by surrounding the
960+
// > entire option in either single or double quotes. Any
961+
// > character (including a backslash) may be included by
962+
// > prefixing the character to be included with a backslash.
963+
//
964+
// We put an argument on each line, so all we need to do is
965+
// ensure the line is interpreted as one whole argument.
966+
for c in self.arg.chars() {
967+
match c {
968+
'\\' |
969+
' ' => write!(f, "\\{}", c)?,
970+
c => write!(f, "{}", c)?,
971+
}
972+
}
973+
}
974+
Ok(())
975+
}
976+
}
977+
}
978+
878979
fn link_args(cmd: &mut Linker,
879980
sess: &Session,
880981
crate_type: config::CrateType,

src/librustc_trans/back/linker.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ use std::fs::{self, File};
1414
use std::io::prelude::*;
1515
use std::io::{self, BufWriter};
1616
use std::path::{Path, PathBuf};
17-
use std::process::Command;
1817

1918
use context::SharedCrateContext;
2019

2120
use back::archive;
21+
use back::command::Command;
2222
use back::symbol_export::ExportedSymbols;
2323
use rustc::middle::dependency_format::Linkage;
2424
use rustc::hir::def_id::{LOCAL_CRATE, CrateNum};

src/librustc_trans/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ pub use llvm_util::{init, target_features, print_version, print_passes, print, e
6868

6969
pub mod back {
7070
mod archive;
71+
mod command;
7172
pub(crate) mod linker;
7273
pub mod link;
7374
mod lto;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-include ../tools.mk
2+
3+
all:
4+
$(RUSTC) foo.rs -g
5+
RUSTC="$(RUSTC_ORIGINAL)" $(call RUN,foo)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright 2017 The Rust Project Developers. See the COPYRIGHT
2+
// file at the top-level directory of this distribution and at
3+
// http://rust-lang.org/COPYRIGHT.
4+
//
5+
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6+
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7+
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8+
// option. This file may not be copied, modified, or distributed
9+
// except according to those terms.
10+
11+
// This is a test which attempts to blow out the system limit with how many
12+
// arguments can be passed to a process. This'll successively call rustc with
13+
// larger and larger argument lists in an attempt to find one that's way too
14+
// big for the system at hand. This file itself is then used as a "linker" to
15+
// detect when the process creation succeeds.
16+
//
17+
// Eventually we should see an argument that looks like `@` as we switch from
18+
// passing literal arguments to passing everything in the file.
19+
20+
use std::env;
21+
use std::fs::{self, File};
22+
use std::io::{BufWriter, Write, Read};
23+
use std::path::PathBuf;
24+
use std::process::Command;
25+
26+
fn main() {
27+
let tmpdir = PathBuf::from(env::var_os("TMPDIR").unwrap());
28+
let ok = tmpdir.join("ok");
29+
if env::var("YOU_ARE_A_LINKER").is_ok() {
30+
if let Some(file) = env::args().find(|a| a.contains("@")) {
31+
fs::copy(&file[1..], &ok).unwrap();
32+
}
33+
return
34+
}
35+
36+
let rustc = env::var_os("RUSTC").unwrap_or("rustc".into());
37+
let me_as_linker = format!("linker={}", env::current_exe().unwrap().display());
38+
for i in (1..).map(|i| i * 100) {
39+
println!("attempt: {}", i);
40+
let file = tmpdir.join("bar.rs");
41+
let mut f = BufWriter::new(File::create(&file).unwrap());
42+
let mut lib_name = String::new();
43+
for _ in 0..i {
44+
lib_name.push_str("foo");
45+
}
46+
for j in 0..i {
47+
writeln!(f, "#[link(name = \"{}{}\")]", lib_name, j).unwrap();
48+
}
49+
writeln!(f, "extern {{}}\nfn main() {{}}").unwrap();
50+
f.into_inner().unwrap();
51+
52+
drop(fs::remove_file(&ok));
53+
let output = Command::new(&rustc)
54+
.arg(&file)
55+
.arg("-C").arg(&me_as_linker)
56+
.arg("--out-dir").arg(&tmpdir)
57+
.env("YOU_ARE_A_LINKER", "1")
58+
.output()
59+
.unwrap();
60+
61+
if !output.status.success() {
62+
let stderr = String::from_utf8_lossy(&output.stderr);
63+
panic!("status: {}\nstdout:\n{}\nstderr:\n{}",
64+
output.status,
65+
String::from_utf8_lossy(&output.stdout),
66+
stderr.lines().map(|l| {
67+
if l.len() > 200 {
68+
format!("{}...\n", &l[..200])
69+
} else {
70+
format!("{}\n", l)
71+
}
72+
}).collect::<String>());
73+
}
74+
75+
if !ok.exists() {
76+
continue
77+
}
78+
79+
let mut contents = String::new();
80+
File::open(&ok).unwrap().read_to_string(&mut contents).unwrap();
81+
82+
for j in 0..i {
83+
assert!(contents.contains(&format!("{}{}", lib_name, j)));
84+
}
85+
86+
break
87+
}
88+
}

src/test/run-make/tools.mk

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ HOST_RPATH_ENV = \
55
TARGET_RPATH_ENV = \
66
$(LD_LIB_PATH_ENVVAR)="$(TMPDIR):$(TARGET_RPATH_DIR):$($(LD_LIB_PATH_ENVVAR))"
77

8+
RUSTC_ORIGINAL := $(RUSTC)
89
BARE_RUSTC := $(HOST_RPATH_ENV) '$(RUSTC)'
910
RUSTC := $(BARE_RUSTC) --out-dir $(TMPDIR) -L $(TMPDIR) $(RUSTFLAGS)
1011
#CC := $(CC) -L $(TMPDIR)

0 commit comments

Comments
 (0)