blob: 2e553e3272e108af8a0ccc13490ab9b1a55e715e [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
crate::target::GnTarget,
anyhow::{anyhow, Result},
std::process::Command,
std::{path::PathBuf, str::FromStr},
};
pub struct BuildScriptOutput {
pub rustflags: Vec<String>,
pub rustenv: Vec<String>,
}
impl BuildScriptOutput {
pub fn parse_from_file(file: PathBuf) -> Result<Self> {
let contents = std::fs::read_to_string(file)?;
let configs: Vec<&str> = contents.split('\n').collect();
Ok(Self::parse(configs))
}
pub fn parse(lines: Vec<&str>) -> Self {
let mut bs = BuildScriptOutput { rustflags: vec![], rustenv: vec![] };
for line in lines {
if line.is_empty() {
continue;
}
let keyval = line.strip_prefix("cargo:");
if keyval.is_none() {
eprintln!("warning: build script output includes non-cargo lines: {}", line);
continue;
}
// More information on the available cargo instructions is here:
// https://doc.rust-lang.org/cargo/reference/build-scripts.html#outputs-of-the-build-script
//
// The following cases are currently unsupported. They all have to do with specific
// artifact type link arguments. The more general cargo:rustc-link-arg is covered.
//
// 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.
//
// cargo:rustc-link-arg-tests=FLAG — Passes custom flags to a linker for tests.
//
// cargo:rustc-link-arg-examples=FLAG — Passes custom flags to a linker for
// examples.
//
// cargo:rustc-link-arg-benches=FLAG — Passes custom flags to a linker for
// benchmarks.
//
// cargo:rustc-cdylib-link-arg=FLAG — Passes custom flags to a linker for cdylib crates.
let (key, value) = keyval.unwrap().split_once('=').unwrap();
match key {
"rerun-if-changed" => continue, // ignored because these are always vendored
"rerun-if-env-changed" => continue, // ignored because these are always vendored
"rustc-cfg" => bs.rustflags.push(format!("\"--cfg={}\"", value)),
"rustc-env" => bs.rustenv.push(format!("\"{}\"", value.to_string())),
"rustc-flags" => bs.rustflags.push(format!("\"{}\"", value.to_string())),
"rustc-link-arg" => bs.rustflags.push(format!("\"-C link-arg={}\"", value)),
"rustc-link-lib" => bs.rustflags.push(format!("\"-l {}\"", value)),
// "rustc-link-search" => bs.rustflags.push(format!("-L {}", value)),
"warning" => eprintln!("warning: {}", value),
&_ => eprintln!("warning: unable to parse build script output: {}", value),
}
}
bs
}
}
pub struct BuildScript {}
impl BuildScript {
pub fn execute(target: &GnTarget<'_>) -> Result<BuildScriptOutput> {
// Previously, this code would compile build.rs scripts directly with rustc. However, build
// scripts can have dependencies of their own (e.g. build-dependencies in Cargo.toml).
// Rather than resolve the entire dependency chain ourselves, we call out to cargo to build
// the crate, performing the build.rs compilation along the way. This does more work than
// compiling the build.rs directly but we don't want to get into the dependency resolution
// business when cargo can do this already.
//
// Unfortunately, there is no way to tell cargo to build only the build script. We could
// link in the cargo library itself and call the correct functions to do only that part of
// the build but that seems overkill.
let cargo = std::env::var_os("CARGO").unwrap_or_else(|| std::ffi::OsString::from("cargo"));
let command = Command::new(cargo)
.current_dir(target.package_root())
.arg("check")
.arg("--message-format=json-render-diagnostics")
.output()
.expect("failed to execute cargo crate build");
if !command.status.success() {
return Err(anyhow!(
"Failed to compile {}:\n{}",
target.gn_target_name(),
String::from_utf8_lossy(&command.stderr)
));
}
let stdout = String::from_utf8(command.stdout)?;
let messages: Vec<&str> = stdout.trim().split('\n').collect();
for message in messages {
let json: serde_json::Value =
serde_json::from_str(message).expect("JSON was not well-formatted");
let reason = json.get("reason").unwrap();
if reason != "build-script-executed" {
continue;
}
let package_id = json.get("package_id").unwrap().to_string().replace('"', "");
let package_elements: Vec<&str> = package_id.split(' ').collect();
let crate_name = package_elements[0];
// this build script execution was for the crate that we are expecting to have a
// build script (avoids issues where a dependency has a build script and we use
// that one by mistake)
if crate_name != target.target_name {
continue;
}
// cargo executes the build script for us and stores the output in its cache, we just
// need to parse it
let out_dir = json.get("out_dir").unwrap().as_str().unwrap();
let output = PathBuf::from_str(out_dir).unwrap().parent().unwrap().join("output");
return BuildScriptOutput::parse_from_file(output);
}
Err(anyhow!(
"We detected a build script in {} but cargo didn't compile it",
target.gn_target_name()
))
}
}