blob: 77472d0f4518c92ef1fb6ca74fc3b5c03fcbabf0 [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::cipd;
use crate::types::{
get_fuchsia_sdk_dir, get_fuchsia_sdk_tools_dir, get_sdk_data_dir, read_env_path, FuchsiaPaths,
};
use {
ansi_term::Colour::{Red, Yellow},
anyhow::{anyhow, format_err, Result},
errors::ffx_error,
fuchsia_async::LocalExecutor,
hyper::{StatusCode, Uri},
serde::Deserialize,
std::{
env, fmt, fs,
fs::{create_dir_all, read_to_string, remove_dir_all, remove_file},
io::BufReader,
path::PathBuf,
},
walkdir::WalkDir,
};
#[derive(Default, Deserialize)]
pub struct Tools(Vec<Tool>);
#[derive(Default, Deserialize)]
pub struct Tool {
pub cpu: String,
pub label: String,
pub name: String,
pub os: String,
pub path: String,
}
impl Tools {
pub fn from_build_dir(path: std::path::PathBuf) -> Result<Self> {
let manifest_path = path.join("tool_paths.json");
fs::File::open(manifest_path.clone())
.map_err(|e| ffx_error!("Cannot open file {:?} \nerror: {:?}", manifest_path, e))
.map(BufReader::new)
.map(serde_json::from_reader)?
.map_err(|e| anyhow!("json parsing errored {}", e))
}
#[cfg(test)]
pub fn from_string(content: &str) -> Result<Self> {
serde_json::from_str(content).map_err(|e| anyhow!("json parsing errored {}", e))
}
pub fn find_path(&self, name: &str) -> Result<String> {
self.find_path_with_arch(
name,
match std::env::consts::ARCH {
"x86_64" => "x64",
"aarch64" => "arm64",
_ => "unsupported",
},
)
}
pub fn find_path_with_arch(&self, name: &str, host_cpu: &str) -> Result<String> {
self.0
.iter()
.find(|x| x.name == name && x.cpu == host_cpu)
.map(|i| i.path.clone())
.ok_or(anyhow!("cannot find matching tool for name {}, arch {}", name, host_cpu))
}
}
#[derive(Clone)]
pub struct HostTools {
pub aemu: PathBuf,
pub far: Option<PathBuf>,
pub ffx: Option<PathBuf>,
pub fvm: Option<PathBuf>,
pub grpcwebproxy: PathBuf,
pub pm: Option<PathBuf>,
pub vdl: PathBuf,
pub zbi: PathBuf,
pub is_sdk: bool,
}
impl fmt::Debug for HostTools {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "[fvdl] tool aemu {:?}", self.aemu)?;
writeln!(f, "[fvdl] tool far {:?}", self.far)?;
writeln!(f, "[fvdl] tool ffx {:?}", self.ffx)?;
writeln!(f, "[fvdl] tool fvm {:?}", self.fvm)?;
writeln!(f, "[fvdl] tool grpcwebproxy {:?}", self.grpcwebproxy)?;
writeln!(f, "[fvdl] tool pm {:?}", self.pm)?;
writeln!(f, "[fvdl] tool vdl {:?}", self.vdl)?;
write!(f, "[fvdl] tool zbi {:?}", self.zbi)
}
}
impl HostTools {
/// Initialize host tools for in-tree usage via fx vdl.
///
/// Environment variable HOST_OUT_DIR, PREBUILT_AEMU_DIR,
/// REBUILT_GRPCWEBPROXY_DIR, and PREBUILT_VDL_DIR are optional.
pub fn from_tree_env(f: &mut impl FuchsiaPaths) -> Result<Self> {
Ok(Self {
// prebuilt binaries that can be optionally fetched from cipd.
aemu: match read_env_path("PREBUILT_AEMU_DIR") {
Ok(val) => val.join("emulator"),
_ => {
let fuchsia_root = f.find_fuchsia_root()?;
WalkDir::new(fuchsia_root.join("prebuilt/third_party/android/aemu/release"))
.into_iter()
.filter_map(|e| e.ok())
.find(|e| e.file_name() == "emulator")
.ok_or(anyhow!(
"Cannot find emulator executable from {:?}",
fuchsia_root
.join("prebuilt/third_party/android/aemu/release")
.display()
))?
.path()
.to_path_buf()
}
},
grpcwebproxy: match read_env_path("PREBUILT_GRPCWEBPROXY_DIR") {
Ok(val) => val.join("grpcwebproxy"),
_ => {
let fuchsia_root = f.find_fuchsia_root()?;
WalkDir::new(fuchsia_root.join("prebuilt/third_party/grpcwebproxy"))
.into_iter()
.filter_map(|e| e.ok())
.find(|e| e.file_name() == "grpcwebproxy" && e.file_type().is_file())
.ok_or(anyhow!(
"Cannot find grpcwebproxy executable from {:?}",
fuchsia_root.join("prebuilt/third_party/grpcwebproxy").display()
))?
.path()
.to_path_buf()
}
},
vdl: match read_env_path("PREBUILT_VDL_DIR") {
Ok(val) => val.join("device_launcher"),
_ => f.find_fuchsia_root()?.join("prebuilt/vdl/device_launcher"),
},
far: match read_env_path("HOST_OUT_DIR") {
Ok(val) => Some(val.join("far")),
_ => f.get_tool_path("far").ok(),
},
ffx: match read_env_path("HOST_OUT_DIR") {
Ok(val) => Some(val.join("ffx")),
_ => f.get_tool_path("ffx").ok(),
},
fvm: match read_env_path("HOST_OUT_DIR") {
Ok(val) => Some(val.join("fvm")),
_ => f.get_tool_path("fvm").ok(),
},
pm: match read_env_path("HOST_OUT_DIR") {
Ok(val) => Some(val.join("pm")),
_ => f.get_tool_path("pm").ok(),
},
zbi: match read_env_path("HOST_OUT_DIR") {
Ok(val) => val.join("zbi"),
_ => f.get_tool_path("zbi")?,
},
is_sdk: false,
})
}
/// Initialize host tools for GN SDK usage.
///
/// First check the existence of environment variable TOOL_DIR, if not specified
/// look for host tools in the program's containing directory.
pub fn from_sdk_env() -> Result<Self> {
let sdk_tool_dir = match read_env_path("TOOL_DIR") {
Ok(dir) => dir,
_ => get_fuchsia_sdk_tools_dir()?,
};
Ok(Self {
// prebuilt binaries that can be optionally fetched from cipd.
aemu: PathBuf::new(),
grpcwebproxy: PathBuf::new(),
vdl: PathBuf::new(),
// in-tree tools that are packaged with GN SDK.
far: Some(sdk_tool_dir.join("far")),
ffx: Some(sdk_tool_dir.join("ffx")),
fvm: Some(sdk_tool_dir.join("fvm")),
pm: Some(sdk_tool_dir.join("pm")),
zbi: sdk_tool_dir.join("zbi"),
is_sdk: true,
})
}
/// Reads the <prebuild>.version file stored in <sdk_root>/bin/<prebuild>.version
///
/// # Arguments
///
/// * `file_name` - <prebuild>.version file name.
/// ex: 'aemu.version', this file is expected to be found under <sdk_root>/bin
pub fn read_prebuild_version(&self, file_name: &str) -> Result<String> {
if self.is_sdk {
let version_file = get_fuchsia_sdk_dir()?.join("bin").join(file_name);
if version_file.exists() {
println!(
"{}",
Yellow.paint(format!(
"[fvdl] reading prebuild version file from: {}",
version_file.display()
))
);
return Ok(read_to_string(version_file)?);
};
println!(
"{}",
Red.paint(format!(
"[fvdl] prebuild version file: {} does not exist.",
version_file.display()
))
);
return Err(format_err!(
"reading prebuilt version errored: file {:?} does not exist.",
version_file
));
}
return Err(format_err!("reading prebuild version file is only support with --sdk flag."));
}
/// Downloads & extract aemu.zip from CIPD, and returns the path containing the emulator executable.
///
/// # Arguments
///
/// * `label` - cipd label that specified a particular aemu version
/// * `cipd_pkg` - this is appended to cipd url https://chrome-infra-packages.appspot.com/dl/fuchsia/third_party/.
pub fn download_and_extract(&self, label: String, cipd_pkg: String) -> Result<PathBuf> {
let mut executor = LocalExecutor::new().unwrap();
executor.run_singlethreaded(async move {
let root_path = match read_env_path("FEMU_DOWNLOAD_DIR") {
Ok(path) => path,
_ => {
let default_path = get_sdk_data_dir()?.join("femu");
if !default_path.exists() {
create_dir_all(&default_path)?;
}
default_path
}
};
let arch = match env::consts::OS {
"macos" => "mac-amd64",
_ => "linux-amd64",
};
let url = format!(
"https://chrome-infra-packages.appspot.com/dl/fuchsia/{}/{}/+/{}",
cipd_pkg, arch, label
)
.parse::<Uri>()?;
let name = cipd_pkg
.split('/')
.last()
.ok_or(anyhow!("Cannot identify filename from {}", cipd_pkg))?;
let cipd_zip = root_path.join(format!("{}-{}.zip", name, label.replace(":", "-")));
let unzipped_root = root_path.join(format!("{}-{}", name, label.replace(":", "-")));
match label.as_str() {
// "latest" and "integration" labels always point to the newest release.
// We cannot assume that the binary is the same as last fetched. Therefore
// we will always re-download and unzip when used.
//
// TODO(yuanzhi) When using fuchsia/third_party/android/aemu/release/...
// we no longer add "integration" ref to any cipd instances. Keeping the "integration" check
// for now for any backward compatibility we may have to support.
"latest" | "integration" => {
if cipd_zip.exists() {
remove_file(&cipd_zip)?;
}
if unzipped_root.exists() {
remove_dir_all(&unzipped_root)?;
}
}
_ => {
if unzipped_root.exists() {
return Ok(unzipped_root);
}
}
};
let status = cipd::download(url.clone(), &cipd_zip).await?;
if status == StatusCode::OK {
cipd::extract_zip(&cipd_zip, &unzipped_root, false /* debug */)?;
Ok(unzipped_root)
} else {
Err(format_err!(
"Cannot download file from cipd path {}. Got status code {}",
url,
status.as_str(),
))
}
})
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::types::InTreePaths;
use crate::types::MockFuchsiaPaths;
use serial_test::serial;
use std::fs::{read_dir, File};
use std::io::Write;
use tempfile::Builder;
const TOOLS_JSON: &str = r#"[
{
"cpu": "x64",
"label": "//:tool_paths.llvm-tools(//build/toolchain/fuchsia:x64)",
"name": "clang-doc",
"os": "linux",
"path": "../../prebuilt/third_party/clang/linux-x64/bin/clang-doc"
},
{
"cpu": "x64",
"label": "//src/developer/ffx:ffx_bin(//build/toolchain:host_x64)",
"name": "ffx",
"os": "linux",
"path": "host_x64/ffx"
},
{
"cpu": "x64",
"label": "//zircon/tools/zbi:zbi(//build/toolchain:host_x64)",
"name": "zbi",
"os": "linux",
"path": "host_x64/zbi"
},
{
"cpu": "arm64",
"label": "//src/storage/bin/fvm:fvm(//build/toolchain:host_arm64)",
"name": "fvm",
"os": "linux",
"path": "host_arm64/fvm"
},
{
"cpu": "x64",
"label": "//src/storage/bin/fvm:fvm(//build/toolchain:host_x64)",
"name": "fvm",
"os": "linux",
"path": "host_x64/fvm"
},
{
"cpu": "x64",
"label": "//:tool_paths.llvm-tools(//build/toolchain/fuchsia:x64)",
"name": "clang-tidy",
"os": "linux",
"path": "../../prebuilt/third_party/clang/linux-x64/bin/clang-tidy"
},
{
"cpu": "x64",
"label": "//:tool_paths.llvm-tools(//build/toolchain/fuchsia:x64)",
"name": "clangd",
"os": "linux",
"path": "../../prebuilt/third_party/clang/linux-x64/bin/clangd"
}
]"#;
#[test]
fn test_tools_parse() -> Result<()> {
let tools = Tools::from_string(TOOLS_JSON)?;
assert_eq!(tools.find_path_with_arch("ffx", "x64")?, "host_x64/ffx");
assert_eq!(tools.find_path_with_arch("zbi", "x64")?, "host_x64/zbi");
assert_eq!(tools.find_path_with_arch("fvm", "x64")?, "host_x64/fvm");
Ok(())
}
#[test]
#[serial]
fn test_host_tools() -> Result<()> {
env::set_var("HOST_OUT_DIR", "/host/out");
env::set_var("PREBUILT_AEMU_DIR", "/host/out/aemu");
env::set_var("PREBUILT_VDL_DIR", "/host/out/vdl");
env::set_var("PREBUILT_GRPCWEBPROXY_DIR", "/host/out/grpcwebproxy");
let host_tools =
HostTools::from_tree_env(&mut InTreePaths { root_dir: None, build_dir: None })?;
assert_eq!(host_tools.aemu.to_str().unwrap(), "/host/out/aemu/emulator");
assert_eq!(host_tools.vdl.to_str().unwrap(), "/host/out/vdl/device_launcher");
assert_eq!(host_tools.far.as_ref().unwrap().to_str().unwrap(), "/host/out/far");
assert_eq!(host_tools.ffx.as_ref().unwrap().to_str().unwrap(), "/host/out/ffx");
assert_eq!(host_tools.fvm.as_ref().unwrap().to_str().unwrap(), "/host/out/fvm");
assert_eq!(host_tools.pm.as_ref().unwrap().to_str().unwrap(), "/host/out/pm");
assert_eq!(host_tools.zbi.to_str().unwrap(), "/host/out/zbi");
Ok(())
}
#[test]
#[serial]
fn test_host_tools_no_env_var() -> Result<()> {
env::remove_var("HOST_OUT_DIR");
env::remove_var("PREBUILT_AEMU_DIR");
env::remove_var("PREBUILT_VDL_DIR");
env::remove_var("PREBUILT_GRPCWEBPROXY_DIR");
let mut mock = MockFuchsiaPaths::new();
let tmp_dir = Builder::new().tempdir()?;
let a = tmp_dir.into_path();
create_dir_all(a.join("prebuilt/third_party/android/aemu/release"))?;
File::create(a.join("prebuilt/third_party/android/aemu/release/emulator"))?
.write_all("foo bar".as_bytes())?;
create_dir_all(a.join("prebuilt/third_party/grpcwebproxy"))?;
File::create(a.join("prebuilt/third_party/grpcwebproxy/grpcwebproxy"))?
.write_all("apple banana".as_bytes())?;
create_dir_all(a.join("prebuilt/vdl"))?;
File::create(a.join("prebuilt/vdl/device_launcher"))?.write_all("deadbeef".as_bytes())?;
mock.expect_find_fuchsia_root().returning(move || Ok(a.clone()));
mock.expect_get_tool_path().returning(|x: &str| {
let mut p = PathBuf::from("/host/out");
p.push(x);
Ok(p)
});
let host_tools = HostTools::from_tree_env(&mut mock)?;
assert!(!host_tools.aemu.as_os_str().is_empty());
assert!(!host_tools.vdl.as_os_str().is_empty());
assert!(!host_tools.grpcwebproxy.as_os_str().is_empty());
assert_eq!(host_tools.far.as_ref().unwrap().to_str().unwrap(), "/host/out/far");
assert_eq!(host_tools.ffx.as_ref().unwrap().to_str().unwrap(), "/host/out/ffx");
assert_eq!(host_tools.fvm.as_ref().unwrap().to_str().unwrap(), "/host/out/fvm");
assert_eq!(host_tools.pm.as_ref().unwrap().to_str().unwrap(), "/host/out/pm");
assert_eq!(host_tools.zbi.to_str().unwrap(), "/host/out/zbi");
Ok(())
}
#[test]
#[serial]
fn test_download_and_extract() -> Result<()> {
let tmp_dir = Builder::new().prefix("fvdl_test_download_").tempdir()?;
env::set_var("FEMU_DOWNLOAD_DIR", tmp_dir.path());
let host_tools = HostTools::from_sdk_env()?;
let mut unzipped_root =
host_tools.download_and_extract("latest".to_string(), "vdl".to_string())?;
let mut has_extract = false;
for path in read_dir(&unzipped_root)? {
let entry = path?;
let p = entry.path();
println!("Found path {}", p.display());
if p.ends_with("device_launcher") {
has_extract = true;
}
}
assert!(has_extract);
// Download "latest" again should trigger a cleanup and re-download
unzipped_root = host_tools.download_and_extract("latest".to_string(), "vdl".to_string())?;
has_extract = false;
for path in read_dir(&unzipped_root)? {
let entry = path?;
let p = entry.path();
println!("Found path {}", p.display());
if p.ends_with("device_launcher") {
has_extract = true;
}
}
assert!(has_extract);
Ok(())
}
}