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