blob: c72e7fc3fcf32f0dfbac3ef281a4a2ae8626e1b2 [file] [log] [blame]
// Copyright 2022 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 std::{
collections::HashMap,
fs::{create_dir_all, read_to_string, File},
io::{BufWriter, Write},
path::Path,
process::{Command, Output},
};
use anyhow::{bail, Context as _, Result};
use argh::FromArgs;
use serde::{Deserialize, Serialize};
const OVERRIDE_ARGS: &str = "\
restat_rust = false
check_output_dir_leaks = false
";
trait CommandExt {
fn success(&mut self) -> Result<()> {
self.success_output().map(|_| ())
}
fn success_output(&mut self) -> Result<Output>;
}
impl CommandExt for Command {
fn success_output(&mut self) -> Result<Output> {
let output = self.output().context(format!("failed to run {self:?}"))?;
if !output.status.success() {
let stdout = std::str::from_utf8(&output.stdout)?;
let stderr = std::str::from_utf8(&output.stderr)?;
bail!("command failed: {self:?}\nstdout: {stdout}\nstderr: {stderr}");
}
Ok(output)
}
}
#[derive(Debug, Serialize, Deserialize)]
struct RustcBuildStep {
env: HashMap<String, String>,
args: Vec<String>,
}
impl RustcBuildStep {
fn parse(input: &str) -> Result<Self> {
let mut env = HashMap::new();
let mut args = Vec::new();
let mut pieces = input.split(' ').peekable();
// Parse environment first
while let Some(piece) = pieces.peek() {
if let Some((key, value)) = piece.split_once('=') {
env.insert(key.to_string(), value.to_string());
pieces.next();
} else {
break;
}
}
// Check that rustc command follows
let Some(rustc_path) = pieces.next() else {
bail!("unexpected end after environment variables");
};
if Path::new(rustc_path).file_name().context("expected path after environment variables")?
!= "rustc"
{
bail!("expected rustc path after environment variables");
}
// Everything else before an `&&` is an arg
args.extend(
pieces.filter(|p| !p.is_empty()).take_while(|p| *p != "&&").map(str::to_string),
);
Ok(Self { env, args })
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum BuildStep {
Rustc(RustcBuildStep),
}
impl BuildStep {
fn filter_parse(input: &str) -> Option<Result<Self>> {
let input = input.trim();
let (first, _) = input.split_once(' ')?;
match first {
"../../build/scripts/no_op.sh" => None,
"touch" => None,
_ => match RustcBuildStep::parse(input) {
Ok(build_step) => Some(Ok(Self::Rustc(build_step))),
Err(e) => Some(Err(e)),
},
}
}
}
#[derive(Debug, FromArgs)]
/// Extract a reproduction for a failing Rust build from the Fuchsia tree
struct Args {
/// the ninja target to extract Rust build steps for
#[argh(positional)]
target: String,
/// the output path to write the Rust build steps to
#[argh(option, short = 'o')]
output: String,
}
const OUT_DIR: &str = "out/rust_extract";
fn write_gn_args() -> Result<()> {
let gn_args = read_to_string("out/default/args.gn").context("failed to open GN args file")?;
create_dir_all(OUT_DIR).context("failed to create new out directory")?;
let new_args_gn_path = Path::new(OUT_DIR).join("args.gn");
let mut new_gn_args = BufWriter::new(
File::create(new_args_gn_path).context("failed to create new GN args file")?,
);
writeln!(new_gn_args, "{gn_args}\n# rust_extract configuration\n{OVERRIDE_ARGS}")?;
Ok(())
}
fn main() -> Result<()> {
let args = argh::from_env::<Args>();
write_gn_args()?;
Command::new("fx").args(["gn", "gen", OUT_DIR]).success()?;
let ninja_output = Command::new("fx")
.args(["ninja", "-t", "commands", &args.target])
.current_dir(OUT_DIR)
.success_output()?;
let ninja_stdout = std::str::from_utf8(&ninja_output.stdout)?;
let build_steps =
ninja_stdout.lines().filter_map(BuildStep::filter_parse).collect::<Result<Vec<_>>>()?;
let output =
BufWriter::new(File::create(&args.output).context("failed to create output file")?);
serde_json::to_writer(output, &build_steps)
.context("failed to serialize build steps to JSON")?;
Ok(())
}