blob: eb2630b77b98b319dca72c390fe8ce33ae206621 [file] [log] [blame]
// Copyright 2021 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::{Tool, ToolCommand, ToolCommandLog, ToolProvider};
use anyhow::{anyhow, Context, Result};
use ffx_config::{global_env_context, sdk::Sdk};
use futures::executor::block_on;
use std::path::PathBuf;
use std::process::Command;
use utf8_path::PathToStringExt;
/// Implementation of ToolProvider that fetches tools from the SDK.
pub struct SdkToolProvider {
/// The SDK object that can find the tools based on a manifest.
sdk: Sdk,
/// The log of the commands run.
log: ToolCommandLog,
}
impl SdkToolProvider {
/// Attempt to create a new SdkToolProvider. This will return an Err if the manifest cannot be
/// found, parsed, or is invalid.
pub fn try_new() -> Result<Self> {
let ctx = global_env_context().context("loading global environment context")?;
Ok(Self {
sdk: block_on(ctx.get_sdk()).context("Reading the SDK")?,
log: ToolCommandLog::default(),
})
}
}
impl ToolProvider for SdkToolProvider {
fn get_tool(&self, name: &str) -> Result<Box<dyn Tool>> {
let path = self
.sdk
.get_host_tool(name.as_ref())
.context(format!("Getting host tool from the SDK: {}", name))?;
self.get_tool_with_path(path)
}
/// Use |path| to create a new tool.
fn get_tool_with_path(&self, path: PathBuf) -> Result<Box<dyn Tool>> {
let tool = SdkTool::new(path, self.log.clone());
Ok(Box::new(tool))
}
fn log(&self) -> &ToolCommandLog {
&self.log
}
}
/// A tool in the SDK that can be executed with a list of arguments.
#[derive(Debug)]
struct SdkTool {
/// Path to the tool.
path: PathBuf,
/// A reference to the log inside the parent SdkToolProvider. When `run()` is called, the tool
/// appends to the `log`.
log: ToolCommandLog,
}
impl SdkTool {
/// Construct a SdkTool.
fn new(path: PathBuf, log: ToolCommandLog) -> Self {
Self { path, log }
}
}
impl Tool for SdkTool {
fn run(&self, args: &[String]) -> Result<()> {
let path = self.path.path_to_string()?;
self.log.add(ToolCommand::new(path.clone(), args.into()));
let output = Command::new(&self.path)
.args(args)
.output()
.context(format!("Failed to run the tool: {}", path))?;
if !output.status.success() {
let command = format!("{} {}", path, args.join(" "));
return Err(anyhow!("{} exited with status: {}", path, output.status)
.context(format!("stderr: {}", String::from_utf8_lossy(&output.stderr)))
.context(command));
}
Ok(())
}
}
#[cfg(test)]
mod test {
use crate::tool::{Tool, ToolCommandLog};
use super::SdkTool;
use assembly_test_util::generate_fake_tool;
use tempfile::tempdir;
#[test]
fn test_sdk_tool() {
// Generate a fake tool script that fails unless "pass" is the first argument.
let dir = tempdir().unwrap();
let tool_path = dir.path().join("tool.sh");
generate_fake_tool(
&tool_path,
r#"#!/bin/bash
if [[ "$1" != "pass" ]]; then
echo "error message" 1>&2
exit 1
fi
exit 0
"#,
);
// Create an SdkTool with a log.
let log = ToolCommandLog::default();
let tool = SdkTool::new(tool_path, log);
// Test a few scenarios.
assert!(tool.run(&[]).is_err());
assert!(tool.run(&["pass".into()]).is_ok());
let err = tool.run(&["fail".into(), "with".into(), "args".into()]).unwrap_err();
let expected =
vec!["tool.sh fail with args", "stderr: error message", "tool.sh exited with status:"];
expected.iter().zip(err.chain()).for_each(|(exp, act)| {
let act = act.to_string();
assert!(act.contains(exp), "expected: {}, actual: {}", exp, act);
});
}
}