| // Copyright 2018 The Bazel Authors. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| //! Parse the output of a cargo build.rs script and generate a list of flags and |
| //! environment variable for the build. |
| use std::io::{BufRead, BufReader, Read}; |
| use std::process::{Command, Output}; |
| |
| #[derive(Debug, PartialEq, Eq)] |
| pub struct CompileAndLinkFlags { |
| pub compile_flags: String, |
| pub link_flags: String, |
| pub link_search_paths: String, |
| } |
| |
| /// Enum containing all the considered return value from the script |
| #[derive(Debug, Clone, PartialEq, Eq)] |
| pub enum BuildScriptOutput { |
| /// cargo:rustc-link-lib |
| LinkLib(String), |
| /// cargo:rustc-link-search |
| LinkSearch(String), |
| /// cargo:rustc-cfg |
| Cfg(String), |
| /// cargo:rustc-flags |
| Flags(String), |
| /// cargo:rustc-link-arg |
| LinkArg(String), |
| /// cargo:rustc-env |
| Env(String), |
| /// cargo:VAR=VALUE |
| DepEnv(String), |
| } |
| |
| impl BuildScriptOutput { |
| /// Converts a line into a [BuildScriptOutput] enum. |
| /// |
| /// Examples |
| /// ```rust |
| /// assert_eq!(BuildScriptOutput::new("cargo:rustc-link-lib=lib"), Some(BuildScriptOutput::LinkLib("lib".to_owned()))); |
| /// ``` |
| fn new(line: &str) -> Option<BuildScriptOutput> { |
| let split = line.splitn(2, '=').collect::<Vec<_>>(); |
| if split.len() <= 1 { |
| // Not a cargo directive. |
| return None; |
| } |
| let param = split[1].trim().to_owned(); |
| let key_split = split[0].splitn(2, ':').collect::<Vec<_>>(); |
| if key_split.len() <= 1 || key_split[0] != "cargo" { |
| // Not a cargo directive. |
| return None; |
| } |
| |
| match key_split[1] { |
| "rustc-link-lib" => Some(BuildScriptOutput::LinkLib(param)), |
| "rustc-link-search" => Some(BuildScriptOutput::LinkSearch(param)), |
| "rustc-cfg" => Some(BuildScriptOutput::Cfg(param)), |
| "rustc-flags" => Some(BuildScriptOutput::Flags(param)), |
| "rustc-link-arg" => Some(BuildScriptOutput::LinkArg(param)), |
| "rustc-env" => Some(BuildScriptOutput::Env(param)), |
| "rerun-if-changed" | "rerun-if-env-changed" => |
| // Ignored because Bazel will re-run if those change all the time. |
| { |
| None |
| } |
| "warning" => { |
| eprint!("Build Script Warning: {}", split[1]); |
| None |
| } |
| "rustc-cdylib-link-arg" | "rustc-link-arg-bin" | "rustc-link-arg-bins" => { |
| // cargo:rustc-cdylib-link-arg=FLAG — Passes custom flags to a linker for cdylib crates. |
| // cargo:rustc-link-arg-bin=BIN=FLAG – Passes custom flags to a linker for the binary BIN. |
| // cargo:rustc-link-arg-bins=FLAG – Passes custom flags to a linker for binaries. |
| eprint!( |
| "Warning: build script returned unsupported directive `{}`", |
| split[0] |
| ); |
| None |
| } |
| _ => { |
| // cargo:KEY=VALUE — Metadata, used by links scripts. |
| Some(BuildScriptOutput::DepEnv(format!( |
| "{}={}", |
| key_split[1].to_uppercase(), |
| param |
| ))) |
| } |
| } |
| } |
| |
| /// Converts a [BufReader] into a vector of [BuildScriptOutput] enums. |
| fn outputs_from_reader<T: Read>(mut reader: BufReader<T>) -> Vec<BuildScriptOutput> { |
| let mut result = Vec::<BuildScriptOutput>::new(); |
| let mut line = String::new(); |
| while reader.read_line(&mut line).expect("Cannot read line") != 0 { |
| if let Some(bso) = BuildScriptOutput::new(&line) { |
| result.push(bso); |
| } |
| line.clear(); |
| } |
| result |
| } |
| |
| /// Take a [Command], execute it and converts its input into a vector of [BuildScriptOutput] |
| pub fn outputs_from_command( |
| cmd: &mut Command, |
| ) -> Result<(Vec<BuildScriptOutput>, Output), Output> { |
| let child_output = cmd.output().expect("Unable to start binary"); |
| if child_output.status.success() { |
| let reader = BufReader::new(child_output.stdout.as_slice()); |
| let output = Self::outputs_from_reader(reader); |
| Ok((output, child_output)) |
| } else { |
| Err(child_output) |
| } |
| } |
| |
| /// Convert a vector of [BuildScriptOutput] into a list of environment variables. |
| pub fn outputs_to_env(outputs: &[BuildScriptOutput], exec_root: &str) -> String { |
| outputs |
| .iter() |
| .filter_map(|x| { |
| if let BuildScriptOutput::Env(env) = x { |
| Some(Self::escape_for_serializing(Self::redact_exec_root( |
| env, exec_root, |
| ))) |
| } else { |
| None |
| } |
| }) |
| .collect::<Vec<_>>() |
| .join("\n") |
| } |
| |
| /// Convert a vector of [BuildScriptOutput] into a list of dependencies environment variables. |
| pub fn outputs_to_dep_env( |
| outputs: &[BuildScriptOutput], |
| crate_links: &str, |
| exec_root: &str, |
| ) -> String { |
| let prefix = format!("DEP_{}_", crate_links.replace('-', "_").to_uppercase()); |
| outputs |
| .iter() |
| .filter_map(|x| { |
| if let BuildScriptOutput::DepEnv(env) = x { |
| Some(format!( |
| "{}{}", |
| prefix, |
| Self::escape_for_serializing(Self::redact_exec_root(env, exec_root)) |
| )) |
| } else { |
| None |
| } |
| }) |
| .collect::<Vec<_>>() |
| .join("\n") |
| } |
| |
| /// Convert a vector of [BuildScriptOutput] into a flagfile. |
| pub fn outputs_to_flags(outputs: &[BuildScriptOutput], exec_root: &str) -> CompileAndLinkFlags { |
| let mut compile_flags = Vec::new(); |
| let mut link_flags = Vec::new(); |
| let mut link_search_paths = Vec::new(); |
| |
| for flag in outputs { |
| match flag { |
| BuildScriptOutput::Cfg(e) => compile_flags.push(format!("--cfg={}", e)), |
| BuildScriptOutput::Flags(e) => compile_flags.push(e.to_owned()), |
| BuildScriptOutput::LinkArg(e) => compile_flags.push(format!("-Clink-arg={}", e)), |
| BuildScriptOutput::LinkLib(e) => link_flags.push(format!("-l{}", e)), |
| BuildScriptOutput::LinkSearch(e) => link_search_paths.push(format!("-L{}", e)), |
| _ => {} |
| } |
| } |
| |
| CompileAndLinkFlags { |
| compile_flags: compile_flags.join("\n"), |
| link_flags: Self::redact_exec_root(&link_flags.join("\n"), exec_root), |
| link_search_paths: Self::redact_exec_root(&link_search_paths.join("\n"), exec_root), |
| } |
| } |
| |
| fn redact_exec_root(value: &str, exec_root: &str) -> String { |
| value.replace(exec_root, "${pwd}") |
| } |
| |
| // The process-wrapper treats trailing backslashes as escapes for following newlines. |
| // If the env var ends with a backslash (and accordingly doesn't have a following newline), |
| // escape it so that it doesn't get turned into a newline by the process-wrapper. |
| // |
| // Note that this code doesn't handle newlines in strings - that's because Cargo treats build |
| // script output as single-line-oriented, so stops processing at the end of a line regardless. |
| fn escape_for_serializing(mut value: String) -> String { |
| if value.ends_with('\\') { |
| value.push('\\'); |
| } |
| value |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use std::io::Cursor; |
| |
| #[test] |
| fn test_from_read_buffer_to_env_and_flags() { |
| let buff = Cursor::new( |
| " |
| cargo:rustc-link-lib=sdfsdf |
| cargo:rustc-env=FOO=BAR |
| cargo:rustc-link-search=/some/absolute/path/bleh |
| cargo:rustc-env=BAR=FOO |
| cargo:rustc-flags=-Lblah |
| cargo:rerun-if-changed=ignored |
| cargo:rustc-cfg=feature=awesome |
| cargo:version=123 |
| cargo:version_number=1010107f |
| cargo:include_path=/some/absolute/path/include |
| cargo:rustc-env=SOME_PATH=/some/absolute/path/beep |
| cargo:rustc-link-arg=-weak_framework |
| cargo:rustc-link-arg=Metal |
| ", |
| ); |
| let reader = BufReader::new(buff); |
| let result = BuildScriptOutput::outputs_from_reader(reader); |
| assert_eq!(result.len(), 12); |
| assert_eq!(result[0], BuildScriptOutput::LinkLib("sdfsdf".to_owned())); |
| assert_eq!(result[1], BuildScriptOutput::Env("FOO=BAR".to_owned())); |
| assert_eq!( |
| result[2], |
| BuildScriptOutput::LinkSearch("/some/absolute/path/bleh".to_owned()) |
| ); |
| assert_eq!(result[3], BuildScriptOutput::Env("BAR=FOO".to_owned())); |
| assert_eq!(result[4], BuildScriptOutput::Flags("-Lblah".to_owned())); |
| assert_eq!( |
| result[5], |
| BuildScriptOutput::Cfg("feature=awesome".to_owned()) |
| ); |
| assert_eq!( |
| result[6], |
| BuildScriptOutput::DepEnv("VERSION=123".to_owned()) |
| ); |
| assert_eq!( |
| result[7], |
| BuildScriptOutput::DepEnv("VERSION_NUMBER=1010107f".to_owned()) |
| ); |
| assert_eq!( |
| result[9], |
| BuildScriptOutput::Env("SOME_PATH=/some/absolute/path/beep".to_owned()) |
| ); |
| assert_eq!( |
| result[10], |
| BuildScriptOutput::LinkArg("-weak_framework".to_owned()) |
| ); |
| assert_eq!(result[11], BuildScriptOutput::LinkArg("Metal".to_owned())); |
| |
| assert_eq!( |
| BuildScriptOutput::outputs_to_dep_env(&result, "ssh2", "/some/absolute/path"), |
| "DEP_SSH2_VERSION=123\nDEP_SSH2_VERSION_NUMBER=1010107f\nDEP_SSH2_INCLUDE_PATH=${pwd}/include".to_owned() |
| ); |
| assert_eq!( |
| BuildScriptOutput::outputs_to_env(&result, "/some/absolute/path"), |
| "FOO=BAR\nBAR=FOO\nSOME_PATH=${pwd}/beep".to_owned() |
| ); |
| assert_eq!( |
| BuildScriptOutput::outputs_to_flags(&result, "/some/absolute/path"), |
| CompileAndLinkFlags { |
| // -Lblah was output as a rustc-flags, so even though it probably _should_ be a link |
| // flag, we don't treat it like one. |
| compile_flags: |
| "-Lblah\n--cfg=feature=awesome\n-Clink-arg=-weak_framework\n-Clink-arg=Metal" |
| .to_owned(), |
| link_flags: "-lsdfsdf".to_owned(), |
| link_search_paths: "-L${pwd}/bleh".to_owned(), |
| } |
| ); |
| } |
| } |