Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame^] | 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 | /// ``` |
| 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(); |
| 106 | let mut line = String::new(); |
| 107 | while reader.read_line(&mut line).expect("Cannot read line") != 0 { |
| 108 | if let Some(bso) = BuildScriptOutput::new(&line) { |
| 109 | result.push(bso); |
| 110 | } |
| 111 | line.clear(); |
| 112 | } |
| 113 | result |
| 114 | } |
| 115 | |
| 116 | /// Take a [Command], execute it and converts its input into a vector of [BuildScriptOutput] |
| 117 | pub fn outputs_from_command( |
| 118 | cmd: &mut Command, |
| 119 | ) -> Result<(Vec<BuildScriptOutput>, Output), Output> { |
| 120 | let child_output = cmd.output().expect("Unable to start binary"); |
| 121 | if child_output.status.success() { |
| 122 | let reader = BufReader::new(child_output.stdout.as_slice()); |
| 123 | let output = Self::outputs_from_reader(reader); |
| 124 | Ok((output, child_output)) |
| 125 | } else { |
| 126 | Err(child_output) |
| 127 | } |
| 128 | } |
| 129 | |
| 130 | /// Convert a vector of [BuildScriptOutput] into a list of environment variables. |
| 131 | pub fn outputs_to_env(outputs: &[BuildScriptOutput], exec_root: &str) -> String { |
| 132 | outputs |
| 133 | .iter() |
| 134 | .filter_map(|x| { |
| 135 | if let BuildScriptOutput::Env(env) = x { |
| 136 | Some(Self::escape_for_serializing(Self::redact_exec_root( |
| 137 | env, exec_root, |
| 138 | ))) |
| 139 | } else { |
| 140 | None |
| 141 | } |
| 142 | }) |
| 143 | .collect::<Vec<_>>() |
| 144 | .join("\n") |
| 145 | } |
| 146 | |
| 147 | /// Convert a vector of [BuildScriptOutput] into a list of dependencies environment variables. |
| 148 | pub fn outputs_to_dep_env( |
| 149 | outputs: &[BuildScriptOutput], |
| 150 | crate_links: &str, |
| 151 | exec_root: &str, |
| 152 | ) -> String { |
| 153 | let prefix = format!("DEP_{}_", crate_links.replace('-', "_").to_uppercase()); |
| 154 | outputs |
| 155 | .iter() |
| 156 | .filter_map(|x| { |
| 157 | if let BuildScriptOutput::DepEnv(env) = x { |
| 158 | Some(format!( |
| 159 | "{}{}", |
| 160 | prefix, |
| 161 | Self::escape_for_serializing(Self::redact_exec_root(env, exec_root)) |
| 162 | )) |
| 163 | } else { |
| 164 | None |
| 165 | } |
| 166 | }) |
| 167 | .collect::<Vec<_>>() |
| 168 | .join("\n") |
| 169 | } |
| 170 | |
| 171 | /// Convert a vector of [BuildScriptOutput] into a flagfile. |
| 172 | pub fn outputs_to_flags(outputs: &[BuildScriptOutput], exec_root: &str) -> CompileAndLinkFlags { |
| 173 | let mut compile_flags = Vec::new(); |
| 174 | let mut link_flags = Vec::new(); |
| 175 | let mut link_search_paths = Vec::new(); |
| 176 | |
| 177 | for flag in outputs { |
| 178 | match flag { |
| 179 | BuildScriptOutput::Cfg(e) => compile_flags.push(format!("--cfg={}", e)), |
| 180 | BuildScriptOutput::Flags(e) => compile_flags.push(e.to_owned()), |
| 181 | BuildScriptOutput::LinkArg(e) => compile_flags.push(format!("-Clink-arg={}", e)), |
| 182 | BuildScriptOutput::LinkLib(e) => link_flags.push(format!("-l{}", e)), |
| 183 | BuildScriptOutput::LinkSearch(e) => link_search_paths.push(format!("-L{}", e)), |
| 184 | _ => {} |
| 185 | } |
| 186 | } |
| 187 | |
| 188 | CompileAndLinkFlags { |
| 189 | compile_flags: compile_flags.join("\n"), |
| 190 | link_flags: Self::redact_exec_root(&link_flags.join("\n"), exec_root), |
| 191 | link_search_paths: Self::redact_exec_root(&link_search_paths.join("\n"), exec_root), |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | fn redact_exec_root(value: &str, exec_root: &str) -> String { |
| 196 | value.replace(exec_root, "${pwd}") |
| 197 | } |
| 198 | |
| 199 | // The process-wrapper treats trailing backslashes as escapes for following newlines. |
| 200 | // If the env var ends with a backslash (and accordingly doesn't have a following newline), |
| 201 | // escape it so that it doesn't get turned into a newline by the process-wrapper. |
| 202 | // |
| 203 | // Note that this code doesn't handle newlines in strings - that's because Cargo treats build |
| 204 | // script output as single-line-oriented, so stops processing at the end of a line regardless. |
| 205 | fn escape_for_serializing(mut value: String) -> String { |
| 206 | if value.ends_with('\\') { |
| 207 | value.push('\\'); |
| 208 | } |
| 209 | value |
| 210 | } |
| 211 | } |
| 212 | |
| 213 | #[cfg(test)] |
| 214 | mod tests { |
| 215 | use super::*; |
| 216 | use std::io::Cursor; |
| 217 | |
| 218 | #[test] |
| 219 | fn test_from_read_buffer_to_env_and_flags() { |
| 220 | let buff = Cursor::new( |
| 221 | " |
| 222 | cargo:rustc-link-lib=sdfsdf |
| 223 | cargo:rustc-env=FOO=BAR |
| 224 | cargo:rustc-link-search=/some/absolute/path/bleh |
| 225 | cargo:rustc-env=BAR=FOO |
| 226 | cargo:rustc-flags=-Lblah |
| 227 | cargo:rerun-if-changed=ignored |
| 228 | cargo:rustc-cfg=feature=awesome |
| 229 | cargo:version=123 |
| 230 | cargo:version_number=1010107f |
| 231 | cargo:include_path=/some/absolute/path/include |
| 232 | cargo:rustc-env=SOME_PATH=/some/absolute/path/beep |
| 233 | cargo:rustc-link-arg=-weak_framework |
| 234 | cargo:rustc-link-arg=Metal |
| 235 | ", |
| 236 | ); |
| 237 | let reader = BufReader::new(buff); |
| 238 | let result = BuildScriptOutput::outputs_from_reader(reader); |
| 239 | assert_eq!(result.len(), 12); |
| 240 | assert_eq!(result[0], BuildScriptOutput::LinkLib("sdfsdf".to_owned())); |
| 241 | assert_eq!(result[1], BuildScriptOutput::Env("FOO=BAR".to_owned())); |
| 242 | assert_eq!( |
| 243 | result[2], |
| 244 | BuildScriptOutput::LinkSearch("/some/absolute/path/bleh".to_owned()) |
| 245 | ); |
| 246 | assert_eq!(result[3], BuildScriptOutput::Env("BAR=FOO".to_owned())); |
| 247 | assert_eq!(result[4], BuildScriptOutput::Flags("-Lblah".to_owned())); |
| 248 | assert_eq!( |
| 249 | result[5], |
| 250 | BuildScriptOutput::Cfg("feature=awesome".to_owned()) |
| 251 | ); |
| 252 | assert_eq!( |
| 253 | result[6], |
| 254 | BuildScriptOutput::DepEnv("VERSION=123".to_owned()) |
| 255 | ); |
| 256 | assert_eq!( |
| 257 | result[7], |
| 258 | BuildScriptOutput::DepEnv("VERSION_NUMBER=1010107f".to_owned()) |
| 259 | ); |
| 260 | assert_eq!( |
| 261 | result[9], |
| 262 | BuildScriptOutput::Env("SOME_PATH=/some/absolute/path/beep".to_owned()) |
| 263 | ); |
| 264 | assert_eq!( |
| 265 | result[10], |
| 266 | BuildScriptOutput::LinkArg("-weak_framework".to_owned()) |
| 267 | ); |
| 268 | assert_eq!(result[11], BuildScriptOutput::LinkArg("Metal".to_owned())); |
| 269 | |
| 270 | assert_eq!( |
| 271 | BuildScriptOutput::outputs_to_dep_env(&result, "ssh2", "/some/absolute/path"), |
| 272 | "DEP_SSH2_VERSION=123\nDEP_SSH2_VERSION_NUMBER=1010107f\nDEP_SSH2_INCLUDE_PATH=${pwd}/include".to_owned() |
| 273 | ); |
| 274 | assert_eq!( |
| 275 | BuildScriptOutput::outputs_to_env(&result, "/some/absolute/path"), |
| 276 | "FOO=BAR\nBAR=FOO\nSOME_PATH=${pwd}/beep".to_owned() |
| 277 | ); |
| 278 | assert_eq!( |
| 279 | BuildScriptOutput::outputs_to_flags(&result, "/some/absolute/path"), |
| 280 | CompileAndLinkFlags { |
| 281 | // -Lblah was output as a rustc-flags, so even though it probably _should_ be a link |
| 282 | // flag, we don't treat it like one. |
| 283 | compile_flags: |
| 284 | "-Lblah\n--cfg=feature=awesome\n-Clink-arg=-weak_framework\n-Clink-arg=Metal" |
| 285 | .to_owned(), |
| 286 | link_flags: "-lsdfsdf".to_owned(), |
| 287 | link_search_paths: "-L${pwd}/bleh".to_owned(), |
| 288 | } |
| 289 | ); |
| 290 | } |
| 291 | } |