xref: /aosp_15_r20/external/bazelbuild-rules_rust/cargo/cargo_build_script_runner/lib.rs (revision d4726bddaa87cc4778e7472feed243fa4b6c267f)
1 // Copyright 2018 The Bazel Authors. All rights reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //    http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 //! Parse the output of a cargo build.rs script and generate a list of flags and
16 //! environment variable for the build.
17 use std::io::{BufRead, BufReader, Read};
18 use std::process::{Command, Output};
19 
20 #[derive(Debug, PartialEq, Eq)]
21 pub struct CompileAndLinkFlags {
22     pub compile_flags: String,
23     pub link_flags: String,
24     pub link_search_paths: String,
25 }
26 
27 /// Enum containing all the considered return value from the script
28 #[derive(Debug, Clone, PartialEq, Eq)]
29 pub enum BuildScriptOutput {
30     /// cargo:rustc-link-lib
31     LinkLib(String),
32     /// cargo:rustc-link-search
33     LinkSearch(String),
34     /// cargo:rustc-cfg
35     Cfg(String),
36     /// cargo:rustc-flags
37     Flags(String),
38     /// cargo:rustc-link-arg
39     LinkArg(String),
40     /// cargo:rustc-env
41     Env(String),
42     /// cargo:VAR=VALUE
43     DepEnv(String),
44 }
45 
46 impl BuildScriptOutput {
47     /// Converts a line into a [BuildScriptOutput] enum.
48     ///
49     /// Examples
50     /// ```rust
51     /// assert_eq!(BuildScriptOutput::new("cargo:rustc-link-lib=lib"), Some(BuildScriptOutput::LinkLib("lib".to_owned())));
52     /// ```
new(line: &str) -> Option<BuildScriptOutput>53     fn new(line: &str) -> Option<BuildScriptOutput> {
54         let split = line.splitn(2, '=').collect::<Vec<_>>();
55         if split.len() <= 1 {
56             // Not a cargo directive.
57             return None;
58         }
59         let param = split[1].trim().to_owned();
60         let key_split = split[0].splitn(2, ':').collect::<Vec<_>>();
61         if key_split.len() <= 1 || key_split[0] != "cargo" {
62             // Not a cargo directive.
63             return None;
64         }
65 
66         match key_split[1] {
67             "rustc-link-lib" => Some(BuildScriptOutput::LinkLib(param)),
68             "rustc-link-search" => Some(BuildScriptOutput::LinkSearch(param)),
69             "rustc-cfg" => Some(BuildScriptOutput::Cfg(param)),
70             "rustc-flags" => Some(BuildScriptOutput::Flags(param)),
71             "rustc-link-arg" => Some(BuildScriptOutput::LinkArg(param)),
72             "rustc-env" => Some(BuildScriptOutput::Env(param)),
73             "rerun-if-changed" | "rerun-if-env-changed" =>
74             // Ignored because Bazel will re-run if those change all the time.
75             {
76                 None
77             }
78             "warning" => {
79                 eprint!("Build Script Warning: {}", split[1]);
80                 None
81             }
82             "rustc-cdylib-link-arg" | "rustc-link-arg-bin" | "rustc-link-arg-bins" => {
83                 // cargo:rustc-cdylib-link-arg=FLAG — Passes custom flags to a linker for cdylib crates.
84                 // cargo:rustc-link-arg-bin=BIN=FLAG – Passes custom flags to a linker for the binary BIN.
85                 // cargo:rustc-link-arg-bins=FLAG – Passes custom flags to a linker for binaries.
86                 eprint!(
87                     "Warning: build script returned unsupported directive `{}`",
88                     split[0]
89                 );
90                 None
91             }
92             _ => {
93                 // cargo:KEY=VALUE — Metadata, used by links scripts.
94                 Some(BuildScriptOutput::DepEnv(format!(
95                     "{}={}",
96                     key_split[1].to_uppercase().replace('-', "_"),
97                     param
98                 )))
99             }
100         }
101     }
102 
103     /// Converts a [BufReader] into a vector of [BuildScriptOutput] enums.
outputs_from_reader<T: Read>(mut reader: BufReader<T>) -> Vec<BuildScriptOutput>104     fn outputs_from_reader<T: Read>(mut reader: BufReader<T>) -> Vec<BuildScriptOutput> {
105         let mut result = Vec::<BuildScriptOutput>::new();
106         let mut buf = Vec::new();
107         while reader
108             .read_until(b'\n', &mut buf)
109             .expect("Cannot read line")
110             != 0
111         {
112             // like cargo, ignore any lines that are not valid utf8
113             if let Ok(line) = String::from_utf8(buf.clone()) {
114                 if let Some(bso) = BuildScriptOutput::new(&line) {
115                     result.push(bso);
116                 }
117             }
118             buf.clear();
119         }
120         result
121     }
122 
123     /// Take a [Command], execute it and converts its input into a vector of [BuildScriptOutput]
outputs_from_command( cmd: &mut Command, ) -> Result<(Vec<BuildScriptOutput>, Output), Output>124     pub fn outputs_from_command(
125         cmd: &mut Command,
126     ) -> Result<(Vec<BuildScriptOutput>, Output), Output> {
127         let child_output = cmd.output().expect("Unable to start binary");
128         if child_output.status.success() {
129             let reader = BufReader::new(child_output.stdout.as_slice());
130             let output = Self::outputs_from_reader(reader);
131             Ok((output, child_output))
132         } else {
133             Err(child_output)
134         }
135     }
136 
137     /// Convert a vector of [BuildScriptOutput] into a list of environment variables.
outputs_to_env(outputs: &[BuildScriptOutput], exec_root: &str) -> String138     pub fn outputs_to_env(outputs: &[BuildScriptOutput], exec_root: &str) -> String {
139         outputs
140             .iter()
141             .filter_map(|x| {
142                 if let BuildScriptOutput::Env(env) = x {
143                     Some(Self::escape_for_serializing(Self::redact_exec_root(
144                         env, exec_root,
145                     )))
146                 } else {
147                     None
148                 }
149             })
150             .collect::<Vec<_>>()
151             .join("\n")
152     }
153 
154     /// Convert a vector of [BuildScriptOutput] into a list of dependencies environment variables.
outputs_to_dep_env( outputs: &[BuildScriptOutput], crate_links: &str, exec_root: &str, ) -> String155     pub fn outputs_to_dep_env(
156         outputs: &[BuildScriptOutput],
157         crate_links: &str,
158         exec_root: &str,
159     ) -> String {
160         let prefix = format!("DEP_{}_", crate_links.replace('-', "_").to_uppercase());
161         outputs
162             .iter()
163             .filter_map(|x| {
164                 if let BuildScriptOutput::DepEnv(env) = x {
165                     Some(format!(
166                         "{}{}",
167                         prefix,
168                         Self::escape_for_serializing(Self::redact_exec_root(env, exec_root))
169                     ))
170                 } else {
171                     None
172                 }
173             })
174             .collect::<Vec<_>>()
175             .join("\n")
176     }
177 
178     /// Convert a vector of [BuildScriptOutput] into a flagfile.
outputs_to_flags(outputs: &[BuildScriptOutput], exec_root: &str) -> CompileAndLinkFlags179     pub fn outputs_to_flags(outputs: &[BuildScriptOutput], exec_root: &str) -> CompileAndLinkFlags {
180         let mut compile_flags = Vec::new();
181         let mut link_flags = Vec::new();
182         let mut link_search_paths = Vec::new();
183 
184         for flag in outputs {
185             match flag {
186                 BuildScriptOutput::Cfg(e) => compile_flags.push(format!("--cfg={e}")),
187                 BuildScriptOutput::Flags(e) => compile_flags.push(e.to_owned()),
188                 BuildScriptOutput::LinkArg(e) => compile_flags.push(format!("-Clink-arg={e}")),
189                 BuildScriptOutput::LinkLib(e) => link_flags.push(format!("-l{e}")),
190                 BuildScriptOutput::LinkSearch(e) => link_search_paths.push(format!("-L{e}")),
191                 _ => {}
192             }
193         }
194 
195         CompileAndLinkFlags {
196             compile_flags: compile_flags.join("\n"),
197             link_flags: Self::redact_exec_root(&link_flags.join("\n"), exec_root),
198             link_search_paths: Self::redact_exec_root(&link_search_paths.join("\n"), exec_root),
199         }
200     }
201 
redact_exec_root(value: &str, exec_root: &str) -> String202     fn redact_exec_root(value: &str, exec_root: &str) -> String {
203         value.replace(exec_root, "${pwd}")
204     }
205 
206     // The process-wrapper treats trailing backslashes as escapes for following newlines.
207     // If the env var ends with a backslash (and accordingly doesn't have a following newline),
208     // escape it so that it doesn't get turned into a newline by the process-wrapper.
209     //
210     // Note that this code doesn't handle newlines in strings - that's because Cargo treats build
211     // script output as single-line-oriented, so stops processing at the end of a line regardless.
escape_for_serializing(mut value: String) -> String212     fn escape_for_serializing(mut value: String) -> String {
213         if value.ends_with('\\') {
214             value.push('\\');
215         }
216         value
217     }
218 }
219 
220 #[cfg(test)]
221 mod tests {
222     use super::*;
223     use std::io::Cursor;
224 
225     #[test]
test_from_read_buffer_to_env_and_flags()226     fn test_from_read_buffer_to_env_and_flags() {
227         let buff = Cursor::new(
228             "
229 cargo:rustc-link-lib=sdfsdf
230 cargo:rustc-env=FOO=BAR
231 cargo:rustc-link-search=/some/absolute/path/bleh
232 cargo:rustc-env=BAR=FOO
233 cargo:rustc-flags=-Lblah
234 cargo:rerun-if-changed=ignored
235 cargo:rustc-cfg=feature=awesome
236 cargo:version=123
237 cargo:version_number=1010107f
238 cargo:include_path=/some/absolute/path/include
239 cargo:rustc-env=SOME_PATH=/some/absolute/path/beep
240 cargo:rustc-link-arg=-weak_framework
241 cargo:rustc-link-arg=Metal
242 cargo:rustc-env=no_trailing_newline=true",
243         );
244         let reader = BufReader::new(buff);
245         let result = BuildScriptOutput::outputs_from_reader(reader);
246         assert_eq!(result.len(), 13);
247         assert_eq!(result[0], BuildScriptOutput::LinkLib("sdfsdf".to_owned()));
248         assert_eq!(result[1], BuildScriptOutput::Env("FOO=BAR".to_owned()));
249         assert_eq!(
250             result[2],
251             BuildScriptOutput::LinkSearch("/some/absolute/path/bleh".to_owned())
252         );
253         assert_eq!(result[3], BuildScriptOutput::Env("BAR=FOO".to_owned()));
254         assert_eq!(result[4], BuildScriptOutput::Flags("-Lblah".to_owned()));
255         assert_eq!(
256             result[5],
257             BuildScriptOutput::Cfg("feature=awesome".to_owned())
258         );
259         assert_eq!(
260             result[6],
261             BuildScriptOutput::DepEnv("VERSION=123".to_owned())
262         );
263         assert_eq!(
264             result[7],
265             BuildScriptOutput::DepEnv("VERSION_NUMBER=1010107f".to_owned())
266         );
267         assert_eq!(
268             result[9],
269             BuildScriptOutput::Env("SOME_PATH=/some/absolute/path/beep".to_owned())
270         );
271         assert_eq!(
272             result[10],
273             BuildScriptOutput::LinkArg("-weak_framework".to_owned())
274         );
275         assert_eq!(result[11], BuildScriptOutput::LinkArg("Metal".to_owned()));
276         assert_eq!(
277             result[12],
278             BuildScriptOutput::Env("no_trailing_newline=true".to_owned())
279         );
280         assert_eq!(
281             BuildScriptOutput::outputs_to_dep_env(&result, "ssh2", "/some/absolute/path"),
282             "DEP_SSH2_VERSION=123\nDEP_SSH2_VERSION_NUMBER=1010107f\nDEP_SSH2_INCLUDE_PATH=${pwd}/include".to_owned()
283         );
284         assert_eq!(
285             BuildScriptOutput::outputs_to_env(&result, "/some/absolute/path"),
286             "FOO=BAR\nBAR=FOO\nSOME_PATH=${pwd}/beep\nno_trailing_newline=true".to_owned()
287         );
288         assert_eq!(
289             BuildScriptOutput::outputs_to_flags(&result, "/some/absolute/path"),
290             CompileAndLinkFlags {
291                 // -Lblah was output as a rustc-flags, so even though it probably _should_ be a link
292                 // flag, we don't treat it like one.
293                 compile_flags:
294                     "-Lblah\n--cfg=feature=awesome\n-Clink-arg=-weak_framework\n-Clink-arg=Metal"
295                         .to_owned(),
296                 link_flags: "-lsdfsdf".to_owned(),
297                 link_search_paths: "-L${pwd}/bleh".to_owned(),
298             }
299         );
300     }
301 
302     #[test]
invalid_utf8()303     fn invalid_utf8() {
304         let buff = Cursor::new(
305             b"
306 cargo:rustc-env=valid1=1
307 cargo:rustc-env=invalid=\xc3\x28
308 cargo:rustc-env=valid2=2
309 ",
310         );
311         let reader = BufReader::new(buff);
312         let result = BuildScriptOutput::outputs_from_reader(reader);
313         assert_eq!(result.len(), 2);
314         assert_eq!(
315             &BuildScriptOutput::outputs_to_env(&result, "/some/absolute/path"),
316             "valid1=1\nvalid2=2"
317         );
318     }
319 }
320