blob: 5827adfea121c69829df600945164e884a7af7d6 [file] [log] [blame] [edit]
// 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::args::StartCommand;
use crate::cipd;
use ansi_term::Colour::*;
use anyhow::{anyhow, format_err, Result};
use fuchsia_async::Executor;
use home::home_dir;
use hyper::{StatusCode, Uri};
use std::convert::From;
use std::env;
use std::fs::{create_dir, create_dir_all, read_to_string, remove_dir_all, remove_file, File};
use std::io::{BufRead, BufReader};
use std::os::unix;
use std::path::PathBuf;
pub fn read_env_path(var: &str) -> Result<PathBuf> {
env::var_os(var).map(PathBuf::from).ok_or(anyhow!("{} contained invalid Unicode", var))
}
/// Returns GN SDK tools directory. This assumes that fvdl is located in
/// <sdk_root>/tools/[x64|arm64]/fvdl, and will return path to <sdk_root>/tools/[x64|arm64]
pub fn get_fuchsia_sdk_tools_dir() -> Result<PathBuf> {
Ok(env::args_os()
.nth(0)
.map(PathBuf::from)
.ok_or(anyhow!("Cannot get invoking binary path (location of fvdl)."))?
.parent()
.ok_or(anyhow!("Cannot get parent path to 'fvdl'."))?
.to_path_buf())
}
/// Returns GN SDK tools directory. This assumes that fvdl is located in
/// <sdk_root>/tools/[x64|arm64]/fvdl, and will return path to <sdk_root>
pub fn get_fuchsia_sdk_dir() -> Result<PathBuf> {
Ok(get_fuchsia_sdk_tools_dir()? // ex: <sdk_root>/tools/x64/
.parent() // ex: <sdk_root>/tools/
.ok_or(anyhow!("Cannot get parent path to 'tools' directory."))?
.parent() // ex: <sdk_root>
.ok_or(anyhow!("Cannot get path to sdk root."))?
.to_path_buf())
}
/// Returns either the path specified in the environment variable FUCHSIA_SDK_DATA_DIR or
/// $HOME/.fuchsia
pub fn get_sdk_data_dir() -> Result<PathBuf> {
let sdk_data_dir = match read_env_path("FUCHSIA_SDK_DATA_DIR") {
Ok(dir) => dir,
_ => {
let default = home_dir().unwrap_or_default().join(".fuchsia");
if !default.exists() {
create_dir(&default)?;
}
default
}
};
Ok(sdk_data_dir)
}
pub struct HostTools {
pub aemu: PathBuf,
pub device_finder: PathBuf,
pub far: PathBuf,
pub fvm: PathBuf,
pub grpcwebproxy: PathBuf,
pub pm: PathBuf,
pub vdl: PathBuf,
pub zbi: PathBuf,
pub is_sdk: bool,
}
impl HostTools {
/// Initialize host tools for in-tree usage.
///
/// Requires the environment variable HOST_OUT_DIR to be specified
/// PREBUILT_AEMU_DIR, PREBUILT_GRPCWEBPROXY_DIR, PREBUILT_VDL_DIR are optional.
/// See: //tools/devshell/vdl
pub fn from_tree_env() -> Result<HostTools> {
let host_out_dir = read_env_path("HOST_OUT_DIR")?;
Ok(HostTools {
// prebuilt binaries that can be optionally fetched from cipd.
aemu: match read_env_path("PREBUILT_AEMU_DIR") {
Ok(val) => val.join("emulator"),
_ => PathBuf::new(),
},
grpcwebproxy: match read_env_path("PREBUILT_GRPCWEBPROXY_DIR") {
Ok(val) => val.join("grpcwebproxy"),
_ => PathBuf::new(),
},
vdl: match read_env_path("PREBUILT_VDL_DIR") {
Ok(val) => val.join("device_launcher"),
_ => PathBuf::new(),
},
device_finder: host_out_dir.join("device-finder"),
far: host_out_dir.join("far"),
fvm: host_out_dir.join("fvm"),
pm: host_out_dir.join("pm"),
zbi: host_out_dir.join("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<HostTools> {
let sdk_tool_dir = match read_env_path("TOOL_DIR") {
Ok(dir) => dir,
_ => get_fuchsia_sdk_tools_dir()?,
};
Ok(HostTools {
// 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.
device_finder: sdk_tool_dir.join("device-finder"),
far: sdk_tool_dir.join("far"),
fvm: sdk_tool_dir.join("fvm"),
pm: 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 appeneded 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 = Executor::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.
"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)?;
Ok(unzipped_root)
} else {
Err(format_err!(
"Cannot download file from cipd path {}. Got status code {}",
url,
status.as_str(),
))
}
})
}
}
pub struct ImageFiles {
pub amber_files: PathBuf,
pub build_args: PathBuf,
pub fvm: PathBuf,
pub kernel: PathBuf,
pub zbi: PathBuf,
}
impl ImageFiles {
/// Initialize fuchsia image and package files for in-tree usage.
///
/// Requires the environment variable FUCHSIA_BUILD_DIR to be specified.
pub fn from_tree_env() -> Result<ImageFiles> {
let fuchsia_build_dir = read_env_path("FUCHSIA_BUILD_DIR")?;
Ok(ImageFiles {
amber_files: fuchsia_build_dir.join("amber-files"),
build_args: fuchsia_build_dir.join("args.gn"),
fvm: fuchsia_build_dir.join(read_env_path("IMAGE_FVM_RAW")?),
kernel: fuchsia_build_dir.join(read_env_path("IMAGE_QEMU_KERNEL_RAW")?),
zbi: fuchsia_build_dir.join(read_env_path("IMAGE_ZIRCONA_ZBI")?),
})
}
/// Initialize fuchsia image and package files for GN SDK usage.
///
/// First check the existence of environment variable IMAGE_DIR, if not specified
/// make a best effort guess in other known paths by calling get_sdk_data_dir().
///
/// If --sdk_version is specified, fuchsia image will be downloaded from GCS.
pub fn from_sdk_env() -> Result<ImageFiles> {
let fuchsia_build_dir = match read_env_path("IMAGE_DIR") {
Ok(dir) => dir,
_ => get_sdk_data_dir()?,
};
Ok(ImageFiles {
amber_files: fuchsia_build_dir.join("amber-files"),
build_args: fuchsia_build_dir.join("buildargs.gn"),
fvm: fuchsia_build_dir.join("storage-full.blk"),
kernel: fuchsia_build_dir.join("femu-kernel.kernel"),
zbi: fuchsia_build_dir.join("zircon-a.zbi"),
})
}
pub fn stage_files(&mut self, dir: &PathBuf) -> Result<()> {
let vdl_kernel_dest = dir.join("femu_kernel");
let vdl_kernel_src = self.kernel.as_path();
unix::fs::symlink(&vdl_kernel_src, &vdl_kernel_dest)?;
self.kernel = vdl_kernel_dest.to_path_buf();
let vdl_fvm_dest = dir.join("femu_fvm");
let vdl_fvm_src = self.fvm.as_path();
unix::fs::symlink(&vdl_fvm_src, &vdl_fvm_dest)?;
self.fvm = vdl_fvm_dest.to_path_buf();
let vdl_args_dest = dir.join("femu_buildargs");
let vdl_args_src = self.build_args.as_path();
unix::fs::symlink(&vdl_args_src, &vdl_args_dest)?;
self.build_args = vdl_args_dest.to_path_buf();
Ok(())
}
}
pub struct SSHKeys {
pub auth_key: PathBuf,
pub private_key: PathBuf,
}
impl SSHKeys {
#[allow(dead_code)]
pub fn print(&self) {
println!("private_key {:?}", self.private_key);
println!("auth_key {:?}", self.auth_key);
}
/// Initialize SSH key files for in-tree usage.
///
/// Requires the environment variable FUCHSIA_DIR & FUCHSIA_BUILD_DIR to be specified.
pub fn from_tree_env() -> Result<SSHKeys> {
let ssh_file = File::open(read_env_path("FUCHSIA_DIR")?.join(".fx-ssh-path"))?;
let ssh_file = BufReader::new(ssh_file);
let mut lines = ssh_file.lines();
let private_key = PathBuf::from(lines.next().unwrap()?);
let auth_key = PathBuf::from(lines.next().unwrap()?);
Ok(SSHKeys { auth_key: auth_key, private_key: private_key })
}
/// Initialize SSH key files for GN SDK usage.
///
/// Requires SSH keys to have been generated and stored in $HOME/.ssh/...
pub fn from_sdk_env() -> Result<SSHKeys> {
let keys = SSHKeys {
auth_key: home_dir().unwrap_or_default().join(".ssh/fuchsia_authorized_keys"),
private_key: home_dir().unwrap_or_default().join(".ssh/fuchsia_ed25519"),
};
Ok(keys)
}
pub fn stage_files(&mut self, dir: &PathBuf) -> Result<()> {
let vdl_priv_key_dest = dir.join("id_ed25519");
let vdl_priv_key_src = self.private_key.as_path();
unix::fs::symlink(&vdl_priv_key_src, &vdl_priv_key_dest)?;
self.private_key = vdl_priv_key_dest.to_path_buf();
let vdl_auth_key_dest = dir.join("id_ed25519.pub");
let vdl_auth_key_src = self.auth_key.as_path();
unix::fs::symlink(&vdl_auth_key_src, &vdl_auth_key_dest)?;
self.auth_key = vdl_auth_key_dest.to_path_buf();
Ok(())
}
}
#[derive(Debug)]
pub struct VDLArgs {
pub headless: bool,
pub tuntap: bool,
pub enable_grpcwebproxy: bool,
pub enable_hidpi_scaling: bool,
pub grpcwebproxy_port: String,
pub upscript: String,
pub packages_to_serve: String,
pub image_size: String,
pub device_proto: String,
pub gpu: String,
pub pointing_device: String,
pub gcs_bucket: String,
pub gcs_build_id: String,
pub gcs_image_archive: String,
}
impl From<&StartCommand> for VDLArgs {
fn from(cmd: &StartCommand) -> Self {
let mut gpu = "swiftshader_indirect";
if cmd.host_gpu {
gpu = "host";
}
if cmd.software_gpu {
gpu = "swiftshader_indirect";
}
let mut enable_grpcwebproxy = false;
let mut grpcwebproxy_port = "0".to_string();
match cmd.grpcwebproxy {
Some(port) => {
enable_grpcwebproxy = true;
grpcwebproxy_port = format!("{}", port);
}
_ => (),
}
VDLArgs {
headless: cmd.headless,
tuntap: cmd.tuntap,
enable_hidpi_scaling: cmd.hidpi_scaling,
upscript: cmd.upscript.as_ref().unwrap_or(&String::from("")).to_string(),
packages_to_serve: cmd
.packages_to_serve
.as_ref()
.unwrap_or(&String::from(""))
.to_string(),
image_size: cmd.image_size.as_ref().unwrap_or(&String::from("2G")).to_string(),
device_proto: cmd.device_proto.as_ref().unwrap_or(&String::from("")).to_string(),
gpu: gpu.to_string(),
pointing_device: cmd
.pointing_device
.as_ref()
.unwrap_or(&String::from("touch"))
.to_string(),
enable_grpcwebproxy: enable_grpcwebproxy,
grpcwebproxy_port: grpcwebproxy_port,
gcs_bucket: cmd.gcs_bucket.as_ref().unwrap_or(&String::from("fuchsia")).to_string(),
gcs_build_id: cmd.sdk_version.as_ref().unwrap_or(&String::from("")).to_string(),
gcs_image_archive: cmd
.image_name
.as_ref()
.unwrap_or(&String::from("qemu-x64"))
.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
use std::fs::read_dir;
use std::io::Write;
use tempfile::TempDir;
#[test]
fn test_convert_start_cmd_to_vdl() {
let start_command = &StartCommand {
headless: false,
tuntap: true,
hidpi_scaling: false,
upscript: Some("/path/to/upscript".to_string()),
packages_to_serve: Some("pkg1.far,pkg2.far".to_string()),
image_size: None,
device_proto: None,
aemu_path: None,
vdl_path: None,
host_gpu: true,
software_gpu: false,
window_width: 1280,
window_height: 800,
grpcwebproxy: None,
grpcwebproxy_path: None,
pointing_device: Some("mouse".to_string()),
aemu_version: Some("git_revision:da1cc2ee512714a176f08b8b5fec035994ca305d".to_string()),
gcs_bucket: None,
grpcwebproxy_version: None,
sdk_version: Some("0.20201130.3.1".to_string()),
image_name: Some("qemu-x64".to_string()),
vdl_version: None,
emulator_log: None,
};
let vdl_args: VDLArgs = start_command.into();
assert_eq!(vdl_args.headless, false);
assert_eq!(vdl_args.tuntap, true);
assert_eq!(vdl_args.upscript, "/path/to/upscript");
assert_eq!(vdl_args.packages_to_serve, "pkg1.far,pkg2.far");
assert_eq!(vdl_args.image_size, "2G");
assert_eq!(vdl_args.device_proto, "");
assert_eq!(vdl_args.gpu, "host");
}
#[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()?;
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.to_str().unwrap(), "/host/out/far");
assert_eq!(host_tools.fvm.to_str().unwrap(), "/host/out/fvm");
assert_eq!(host_tools.pm.to_str().unwrap(), "/host/out/pm");
assert_eq!(host_tools.device_finder.to_str().unwrap(), "/host/out/device-finder");
assert_eq!(host_tools.zbi.to_str().unwrap(), "/host/out/zbi");
Ok(())
}
#[test]
#[serial]
fn test_host_tools_no_env_var() -> Result<()> {
env::set_var("HOST_OUT_DIR", "/host/out");
env::remove_var("PREBUILT_AEMU_DIR");
env::remove_var("PREBUILT_VDL_DIR");
env::remove_var("PREBUILT_GRPCWEBPROXY_DIR");
let host_tools = HostTools::from_tree_env()?;
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.to_str().unwrap(), "/host/out/far");
assert_eq!(host_tools.fvm.to_str().unwrap(), "/host/out/fvm");
assert_eq!(host_tools.pm.to_str().unwrap(), "/host/out/pm");
assert_eq!(host_tools.device_finder.to_str().unwrap(), "/host/out/device-finder");
assert_eq!(host_tools.zbi.to_str().unwrap(), "/host/out/zbi");
Ok(())
}
#[test]
fn test_image_files() -> Result<()> {
env::set_var("FUCHSIA_BUILD_DIR", "/build/out");
env::set_var("IMAGE_ZIRCONA_ZBI", "zircona");
env::set_var("IMAGE_QEMU_KERNEL_RAW", "kernel");
env::set_var("IMAGE_FVM_RAW", "fvm");
let mut image_files = ImageFiles::from_tree_env()?;
assert_eq!(image_files.zbi.to_str().unwrap(), "/build/out/zircona");
assert_eq!(image_files.kernel.to_str().unwrap(), "/build/out/kernel");
assert_eq!(image_files.fvm.to_str().unwrap(), "/build/out/fvm");
assert_eq!(image_files.build_args.to_str().unwrap(), "/build/out/args.gn");
assert_eq!(image_files.amber_files.to_str().unwrap(), "/build/out/amber-files");
let tmp_dir = TempDir::new()?.into_path();
image_files.stage_files(&tmp_dir)?;
assert_eq!(image_files.kernel.to_str(), tmp_dir.join("femu_kernel").to_str());
Ok(())
}
#[test]
fn test_ssh_files() -> Result<()> {
let data = format!(
"/usr/local/home/foo/.ssh/fuchsia_ed25519
/usr/local/home/foo/.ssh/fuchsia_authorized_keys
",
);
let tmp_dir = TempDir::new()?.into_path();
File::create(tmp_dir.join(".fx-ssh-path"))?.write_all(data.as_bytes())?;
env::set_var("FUCHSIA_DIR", tmp_dir.to_str().unwrap());
env::set_var("FUCHSIA_BUILD_DIR", "/build/out");
let mut ssh_files = SSHKeys::from_tree_env()?;
assert_eq!(
ssh_files.private_key.to_str().unwrap(),
"/usr/local/home/foo/.ssh/fuchsia_ed25519"
);
assert_eq!(
ssh_files.auth_key.to_str().unwrap(),
"/usr/local/home/foo/.ssh/fuchsia_authorized_keys"
);
ssh_files.stage_files(&tmp_dir)?;
assert_eq!(ssh_files.private_key.to_str(), tmp_dir.join("id_ed25519").to_str());
Ok(())
}
#[test]
fn test_sdk_data_dir() -> Result<()> {
let tmp_dir = TempDir::new()?.into_path();
env::set_var("FUCHSIA_SDK_DATA_DIR", tmp_dir.to_str().unwrap());
let p = get_sdk_data_dir()?;
assert_eq!(p.to_str(), tmp_dir.to_str());
Ok(())
}
#[test]
fn test_download_and_extract() -> Result<()> {
let tmp_dir = TempDir::new()?.into_path();
env::set_var("FEMU_DOWNLOAD_DIR", tmp_dir.to_str().unwrap());
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(())
}
}