cargo_5730/
lib.rs

1use std::io::Read;
2use std::{env, fs, path, process};
3
4/// A scoped wrapper for the directory where we'll compile and run the build script.
5struct BuildDir {
6    pub path: path::PathBuf,
7}
8
9impl BuildDir {
10    fn new() -> Self {
11        let mut random_data = [0u8; 16];
12        let mut file = fs::File::open("/dev/urandom").expect("failed to open /dev/urandom");
13        file.read_exact(&mut random_data)
14            .expect("failed to read /dev/urandom");
15
16        let mut hex_str = String::new();
17        for digit in random_data.iter() {
18            hex_str = hex_str + &format!("{:x}", digit)
19        }
20
21        let mut dir = env::temp_dir();
22        dir.push(format!("build-script-{}", hex_str));
23
24        BuildDir { path: dir }
25    }
26}
27
28impl Drop for BuildDir {
29    fn drop(&mut self) {
30        // some paranoia before running 'rm -rf'
31        assert!(self.path.starts_with(env::temp_dir()));
32
33        println!("Removing build crate staging dir: {}", self.path.display());
34        fs::remove_dir_all(&self.path).expect(&format!(
35            "Couldn't clean up build dir: {}",
36            self.path.display()
37        ));
38    }
39}
40
41// All this toml stuff is completely manual to avoid introducing any
42// dependencies in this library, since the whole point is to work around
43// dependency issues.
44
45fn qualify_cargo_toml_paths_in_text(cargo_toml_content: &str, base_dir: &path::Path) -> String {
46    // Lacking a real parser due to constraints, look for a couple of common
47    // patterns. TODO: Roll a little parser for this.
48    let mut cargo_toml = cargo_toml_content.to_owned();
49    cargo_toml = cargo_toml.replace("path = \"", &format!("path = \"{}/", base_dir.display()));
50    cargo_toml = cargo_toml.replace("path=\"", &format!("path=\"{}/", base_dir.display()));
51    cargo_toml = cargo_toml.replace("path = '", &format!("path = '{}/", base_dir.display()));
52    cargo_toml = cargo_toml.replace("path='", &format!("path='{}/", base_dir.display()));
53    cargo_toml
54}
55
56fn read_toml_section(toml_path: &path::Path, section_name: &str) -> String {
57    let content = fs::read_to_string(toml_path)
58        .expect(&format!("Can't read toml from {}", toml_path.display()));
59
60    let mut section = String::new();
61    let mut in_section = false;
62    for line in content.lines() {
63        if in_section {
64            if line.starts_with("[") {
65                break;
66            } else {
67                section.push_str(line);
68                section.push_str("\n");
69            }
70        } else if line == format!("[{}]", section_name) {
71            in_section = true
72        }
73    }
74
75    section
76}
77
78fn compile_build_crate(
79    build_dir: &BuildDir,
80    cargo: &str,
81    path: &str,
82    ssh_auth_sock: &str,
83    rustup_home: &str,
84    rustup_toolchain: &str,
85    build_crate_src: &path::Path,
86) {
87    let res = process::Command::new(cargo)
88        .args(&["build", "-vv"])
89        .env_clear()
90        .env("PATH", path)
91        .env("SSH_AUTH_SOCK", ssh_auth_sock)
92        .env("RUSTUP_HOME", rustup_home)
93        .env("RUSTUP_TOOLCHAIN", rustup_toolchain)
94        .env("CARGO_TARGET_DIR", build_crate_src.join("build-script-target"))
95        .env("RUSTFLAGS", "--cfg workaround_build")
96        .current_dir(&build_dir.path)
97        .stdout(process::Stdio::inherit())
98        .stderr(process::Stdio::inherit())
99        .output()
100        .expect("failed to compile build-script crate");
101
102    assert!(
103        res.status.success(),
104        "Failed to run compile build crate at {} with {:#?}",
105        build_dir.path.display(),
106        res
107    );
108}
109
110fn run_compiled_build_script(executable_name: &str, working_dir: &path::Path) {
111    // run the build script
112    let build_script_path = working_dir
113        .join("build-script-target")
114        .join("debug")
115        .join(executable_name);
116
117    let res = process::Command::new(&build_script_path)
118        .current_dir(&working_dir)
119        .stdout(process::Stdio::inherit())
120        .stderr(process::Stdio::inherit())
121        .output()
122        .expect(&format!(
123            "failed to run build script at {}",
124            build_script_path.display()
125        ));
126
127    assert!(
128        res.status.success(),
129        "Failed to run build script at {} with {:#?}",
130        build_script_path.display(),
131        res
132    );
133}
134
135/// Compile and run build.rs from cwd
136/// - use 'workaround-build-dependencies' from Cargo.toml
137/// - Build it with the workaround_build configuration
138/// - Copy Cargo.lock back here to build.Cargo.lock
139pub fn run_build_script() {
140    let base_dir = env::var("CARGO_MANIFEST_DIR").expect("Can't get CARGO_MANIFEST_DIR from env");
141    let base_dir = path::Path::new(&base_dir);
142
143    let build_rs = base_dir.join("build.rs");
144    let cargo_toml = base_dir.join("Cargo.toml");
145    let cargo_build_lock = base_dir.join("Cargo.build.lock");
146
147    let build_dir = BuildDir::new();
148
149    let cargo = env::var("CARGO").expect("Can't get CARGO from env");
150    let path = env::var("PATH").expect("Can't get PATH from env");
151    let ssh_auth_sock = env::var("SSH_AUTH_SOCK").unwrap_or_default();
152
153    let rustup_home = env::var("RUSTUP_HOME").unwrap_or_default();
154    let rustup_toolchain = env::var("RUSTUP_TOOLCHAIN").unwrap_or_default();
155
156    // Build in /tmp to avoid the influence of .cargo/config settings in the
157    // build crate's parent, which cargo gives us no way to ignore.
158    fs::create_dir(&build_dir.path).expect(&format!(
159        "Couldn't create temp build dir at {}",
160        build_dir.path.display()
161    ));
162
163    // copy in build.rs
164    fs::create_dir(build_dir.path.join("src")).expect(&format!(
165        "Couldn't create directory {}/src",
166        build_dir.path.display()
167    ));
168
169    fs::copy(&build_rs, build_dir.path.join("src").join("main.rs")).expect(&format!(
170        "Error copying build script {} to {}/src/main.rs",
171        build_rs.display(),
172        build_dir.path.display()
173    ));
174
175    // synthesize Cargo.toml
176    let deps_section = read_toml_section(&cargo_toml, "workaround-build-dependencies");
177
178    // Fix any relative paths that were in the Cargo.toml
179    let deps_section = qualify_cargo_toml_paths_in_text(&deps_section, &base_dir);
180    let cargo_toml_content = format!(
181        r#"
182[package]
183name = "workaround-build-script"
184version = "0.1.0"
185authors = ["The cargo-5730 crate"]
186edition = "2018"
187
188[dependencies]
189{}
190"#,
191        deps_section
192    );
193
194    fs::write(build_dir.path.join("Cargo.toml"), cargo_toml_content).expect(&format!(
195        "Error writing synthesized Cargo manifest to {}/Cargo.toml",
196        build_dir.path.display()
197    ));
198
199    // copy in Cargo.build.lock, if we have one
200    if cargo_build_lock.exists() {
201        fs::copy(&cargo_build_lock, build_dir.path.join("Cargo.lock")).expect(&format!(
202            "Error copying build manifest lockfile from {} to {}/Cargo.lock",
203            cargo_build_lock.display(),
204            build_dir.path.display()
205        ));
206    }
207
208    compile_build_crate(
209        &build_dir,
210        &cargo,
211        &path,
212        &ssh_auth_sock,
213        &rustup_home,
214        &rustup_toolchain,
215        &base_dir,
216    );
217
218    // copy back the 'Cargo.lock' file, to speed up subsequent compilations
219    fs::copy(build_dir.path.join("Cargo.lock"), &cargo_build_lock).expect(&format!(
220        "Error copying out Cargo.lock from {}/Cargo.lock to {}",
221        build_dir.path.display(),
222        cargo_build_lock.display()
223    ));
224
225    // Run the build script with its original source directory as the working
226    // dir.
227    run_compiled_build_script("workaround-build-script", &base_dir);
228}
229
230#[cfg(test)]
231mod test {
232    use super::*;
233
234    #[test]
235    fn test_path_fixup_1() {
236        let input = r#"
237[dependencies]
238lib-crate = { path = "../../lib-crate" }
239"#;
240        let expected = r#"
241[dependencies]
242lib-crate = { path = "/basedir/../../lib-crate" }
243"#;
244
245        assert_eq!(
246            qualify_cargo_toml_paths_in_text(input, path::Path::new("/basedir")),
247            expected.to_string()
248        );
249    }
250
251    #[test]
252    fn test_path_fixup_2() {
253        let input = r#"
254[dependencies]
255lib-crate = { path="../../lib-crate" }
256"#;
257        let expected = r#"
258[dependencies]
259lib-crate = { path="/basedir/../../lib-crate" }
260"#;
261
262        assert_eq!(
263            qualify_cargo_toml_paths_in_text(input, path::Path::new("/basedir")),
264            expected.to_string()
265        );
266    }
267
268    #[test]
269    fn test_path_fixup_3() {
270        let input = r#"
271[dependencies]
272lib-crate = { path = '../../lib-crate' }
273"#;
274        let expected = r#"
275[dependencies]
276lib-crate = { path = '/basedir/../../lib-crate' }
277"#;
278
279        assert_eq!(
280            qualify_cargo_toml_paths_in_text(input, path::Path::new("/basedir")),
281            expected.to_string()
282        );
283    }
284
285    #[test]
286    fn test_path_fixup_4() {
287        let input = r#"
288[dependencies]
289lib-crate = { path='../../lib-crate' }
290"#;
291        let expected = r#"
292[dependencies]
293lib-crate = { path='/basedir/../../lib-crate' }
294"#;
295
296        assert_eq!(
297            qualify_cargo_toml_paths_in_text(input, path::Path::new("/basedir")),
298            expected.to_string()
299        );
300    }
301}