blob: 8a1b765532a5b1d46b7500373b18519c571225dd [file] [log] [blame]
Brian Silvermancc09f182022-03-09 15:40:20 -08001// 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.
17use std::io::{BufRead, BufReader, Read};
18use std::process::{Command, Output};
19
20#[derive(Debug, PartialEq, Eq)]
21pub 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)]
29pub 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
46impl 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 /// ```
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(),
97 param
98 )))
99 }
100 }
101 }
102
103 /// Converts a [BufReader] into a vector of [BuildScriptOutput] enums.
104 fn outputs_from_reader<T: Read>(mut reader: BufReader<T>) -> Vec<BuildScriptOutput> {
105 let mut result = Vec::<BuildScriptOutput>::new();
Adam Snaider1c095c92023-07-08 02:09:58 -0400106 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 }
Brian Silvermancc09f182022-03-09 15:40:20 -0800117 }
Adam Snaider1c095c92023-07-08 02:09:58 -0400118 buf.clear();
Brian Silvermancc09f182022-03-09 15:40:20 -0800119 }
120 result
121 }
122
123 /// Take a [Command], execute it and converts its input into a vector of [BuildScriptOutput]
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.
138 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.
155 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.
179 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 {
Adam Snaider1c095c92023-07-08 02:09:58 -0400186 BuildScriptOutput::Cfg(e) => compile_flags.push(format!("--cfg={e}")),
Brian Silvermancc09f182022-03-09 15:40:20 -0800187 BuildScriptOutput::Flags(e) => compile_flags.push(e.to_owned()),
Adam Snaider1c095c92023-07-08 02:09:58 -0400188 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}")),
Brian Silvermancc09f182022-03-09 15:40:20 -0800191 _ => {}
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
202 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.
212 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)]
221mod tests {
222 use super::*;
223 use std::io::Cursor;
224
225 #[test]
226 fn test_from_read_buffer_to_env_and_flags() {
227 let buff = Cursor::new(
228 "
229cargo:rustc-link-lib=sdfsdf
230cargo:rustc-env=FOO=BAR
231cargo:rustc-link-search=/some/absolute/path/bleh
232cargo:rustc-env=BAR=FOO
233cargo:rustc-flags=-Lblah
234cargo:rerun-if-changed=ignored
235cargo:rustc-cfg=feature=awesome
236cargo:version=123
237cargo:version_number=1010107f
238cargo:include_path=/some/absolute/path/include
239cargo:rustc-env=SOME_PATH=/some/absolute/path/beep
240cargo:rustc-link-arg=-weak_framework
241cargo:rustc-link-arg=Metal
Adam Snaider1c095c92023-07-08 02:09:58 -0400242cargo:rustc-env=no_trailing_newline=true",
Brian Silvermancc09f182022-03-09 15:40:20 -0800243 );
244 let reader = BufReader::new(buff);
245 let result = BuildScriptOutput::outputs_from_reader(reader);
Adam Snaider1c095c92023-07-08 02:09:58 -0400246 assert_eq!(result.len(), 13);
Brian Silvermancc09f182022-03-09 15:40:20 -0800247 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()));
Adam Snaider1c095c92023-07-08 02:09:58 -0400276 assert_eq!(
277 result[12],
278 BuildScriptOutput::Env("no_trailing_newline=true".to_owned())
279 );
Brian Silvermancc09f182022-03-09 15:40:20 -0800280 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"),
Adam Snaider1c095c92023-07-08 02:09:58 -0400286 "FOO=BAR\nBAR=FOO\nSOME_PATH=${pwd}/beep\nno_trailing_newline=true".to_owned()
Brian Silvermancc09f182022-03-09 15:40:20 -0800287 );
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 }
Adam Snaider1c095c92023-07-08 02:09:58 -0400301
302 #[test]
303 fn invalid_utf8() {
304 let buff = Cursor::new(
305 b"
306cargo:rustc-env=valid1=1
307cargo:rustc-env=invalid=\xc3\x28
308cargo: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 }
Brian Silvermancc09f182022-03-09 15:40:20 -0800319}