blob: 215d1730c63a6040ea94fcb290879f25f2aaf995 [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::args::StartCommand;
use crate::graphic_utils::get_default_graphics;
use crate::images::Images;
use crate::tools::Tools;
use anyhow::{anyhow, Result};
use errors::ffx_bail;
use ffx_config::sdk::{Sdk, SdkVersion};
use home::home_dir;
use mockall::automock;
use std::convert::From;
use std::env;
use std::fmt;
use std::fs::{create_dir, read_dir, 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!("{} is not a valid environment variable", var))
}
#[automock]
pub trait FuchsiaPaths {
fn find_fuchsia_root(&mut self) -> Result<PathBuf>;
fn find_fuchsia_build_dir(&mut self) -> Result<PathBuf>;
fn get_tool_path(&mut self, name: &str) -> Result<PathBuf>;
fn get_image_path<'a>(&mut self, names: Vec<&'a str>, image_type: &str) -> Result<PathBuf>;
}
pub struct InTreePaths {
pub root_dir: Option<PathBuf>,
pub build_dir: Option<PathBuf>,
}
impl FuchsiaPaths for InTreePaths {
/// Walks the current execution path and its parent directories to find the path
/// that contains .jiri_root directory.
fn find_fuchsia_root(&mut self) -> Result<PathBuf> {
if self.root_dir.is_none() {
for ancester in std::env::current_exe()?.ancestors() {
if let Ok(entries) = read_dir(ancester) {
for entry in entries {
if let Ok(entry) = entry {
if entry.path().ends_with(".jiri_root") {
self.root_dir.replace(ancester.to_path_buf());
println!(
"[fvdl] Found Fuchsia root directory {:?}",
self.root_dir.as_ref().unwrap().display()
);
return Ok(ancester.to_path_buf());
}
}
}
}
}
}
self.root_dir
.as_ref()
.map(|c| c.clone())
.ok_or(anyhow!("Cannot locate Fuchsia binaries.\n\
SDK users, make sure you include the --sdk flag and run the program from inside the Fuchsia SDK directory.\n\
Non-SDK users, make sure you're running the program from inside the Fuchsia source directory tree."))
}
fn find_fuchsia_build_dir(&mut self) -> Result<PathBuf> {
if self.build_dir.is_none() {
match read_env_path("FUCHSIA_BUILD_DIR") {
Ok(val) => self.build_dir.replace(val),
_ => {
let root = self.find_fuchsia_root()?;
let build_dir = File::open(root.join(".fx-build-dir"))?;
let build_dir_file = BufReader::new(build_dir);
let dir = build_dir_file
.lines()
.nth(0)
.ok_or(anyhow!("cannot read file {:?}", root.join(".fx-build-dir")))?;
self.build_dir.replace(root.join(dir?))
}
};
}
self.build_dir
.as_ref()
.map(|c| c.clone())
.ok_or(anyhow!("Cannot read path info from build_dir {:?}", self.build_dir))
}
fn get_tool_path(&mut self, name: &str) -> Result<PathBuf> {
let build_dir = self.find_fuchsia_build_dir()?;
let tools = Tools::from_build_dir(build_dir.clone())?;
tools.find_path(name).map(|p| build_dir.join(p))
}
fn get_image_path<'a>(&mut self, names: Vec<&'a str>, image_type: &str) -> Result<PathBuf> {
let build_dir = self.find_fuchsia_build_dir()?;
let images = Images::from_build_dir(build_dir.clone())?;
images.find_path(names, image_type).map(|p| build_dir.join(p))
}
}
/// 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(std::env::current_exe()?
.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)
}
/// Reads sdk version from manifest.json.
/// This method assumes that user is invoking the binary from GN SDK, not in fuchsia repo.
/// TODO(fxb/69689) Use ffx::config to obtain host_tools location.
pub fn get_sdk_version_from_manifest() -> Result<String> {
let sdk = Sdk::from_sdk_dir(get_fuchsia_sdk_dir()?)?;
match sdk.get_version() {
SdkVersion::Version(v) => Ok(v.to_string()),
SdkVersion::InTree => ffx_bail!("This should only be used in SDK"),
SdkVersion::Unknown => ffx_bail!("Cannot determine SDK version"),
}
}
#[derive(Clone)]
pub struct ImageFiles {
pub amber_files: Option<PathBuf>,
pub build_args: Option<PathBuf>,
pub fvm: Option<PathBuf>,
pub kernel: PathBuf,
pub zbi: PathBuf,
}
impl fmt::Debug for ImageFiles {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "[fvdl] image package {:?}", self.amber_files)?;
writeln!(f, "[fvdl] image build_args {:?}", self.build_args)?;
writeln!(f, "[fvdl] image fvm {:?}", self.fvm)?;
writeln!(f, "[fvdl] image kernel {:?}", self.kernel)?;
write!(f, "[fvdl] image zbi {:?}", self.zbi)
}
}
impl ImageFiles {
/// Initialize fuchsia image and package files for in-tree usage.
///
/// First checks for environment variable FUCHSIA_BUILD_DIR. If not specify looks into
/// <repo_root>/.fx-build-dir. For example ~/fuchsia/.fx-build-dir
pub fn from_tree_env(f: &mut impl FuchsiaPaths) -> Result<Self> {
let fuchsia_build_dir = f.find_fuchsia_build_dir()?;
println!("[fvdl] Using fuchsia build dir: {:?}", fuchsia_build_dir.display());
Ok(Self {
amber_files: {
let f = fuchsia_build_dir.join("amber-files");
if f.exists() {
Some(f)
} else {
None
}
},
build_args: f.get_image_path(vec!["buildargs"], "gn").ok(),
fvm: f
.get_image_path(vec!["storage-full", "storage-sparse", "fvm.fastboot"], "blk")
.ok(),
kernel: f.get_image_path(vec!["qemu-kernel"], "kernel")?,
zbi: f.get_image_path(vec!["zircon-a"], "zbi")?,
})
}
/// When running from SDK (running with --sdk), we will either fetch images from GCS or use cached-image files.
/// If fetching from GCS, these image files will be ignored by device_launcher.
/// If using cached images, call update_paths_from_cache() to populate the image file paths.
pub fn from_sdk_env() -> Result<Self> {
Ok(Self {
amber_files: None,
build_args: None,
fvm: None,
kernel: PathBuf::new(),
zbi: PathBuf::new(),
})
}
/// Checks that all essential files exist. Note: amber_files, build_args, and fvm are optional.
pub fn check(&self) -> Result<()> {
if let Some(a) = &self.amber_files {
if !a.exists() {
ffx_bail!("amber-files at {:?} does not exist", a);
}
}
if let Some(b) = &self.build_args {
if !b.exists() {
ffx_bail!("build_args file at {:?} does not exist", b);
}
}
if let Some(f) = &self.fvm {
if !f.exists() {
ffx_bail!("fvm file at {:?} does not exist", f);
}
}
if !self.kernel.exists() {
ffx_bail!("kernel file at {:?} does not exist", self.kernel);
}
if !self.zbi.exists() {
ffx_bail!("zbi file at {:?} does not exist", self.zbi);
}
Ok(())
}
pub fn images_exist(&self) -> bool {
return self.kernel.exists()
&& self.zbi.exists()
&& self.build_args.as_ref().map_or(true, |b| b.exists())
&& self.amber_files.as_ref().map_or(true, |a| a.exists())
&& self.fvm.as_ref().map_or(true, |f| f.exists());
}
pub fn update_paths_from_cache(&mut self, cache_root: &PathBuf) {
self.amber_files = Some(cache_root.join("package_archive"));
self.build_args = Some(cache_root.join("images").join("buildargs"));
self.fvm = Some(cache_root.join("images").join("femu-fvm"));
self.kernel = cache_root.join("images").join("femu-kernel");
self.zbi = cache_root.join("images").join("zircon-a.zbi");
}
pub fn update_paths_from_args(&mut self, start_command: &StartCommand) {
if let Some(image) = &start_command.amber_files {
self.amber_files = Some(PathBuf::from(image))
}
if let Some(image) = &start_command.fvm_image {
self.fvm = Some(PathBuf::from(image))
}
if let Some(image) = &start_command.kernel_image {
self.kernel = PathBuf::from(image)
}
if let Some(image) = &start_command.zbi_image {
self.zbi = PathBuf::from(image)
}
}
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();
if let Some(f) = &self.fvm {
let vdl_fvm_dest = dir.join("femu_fvm");
let vdl_fvm_src = f.as_path();
unix::fs::symlink(&vdl_fvm_src, &vdl_fvm_dest)?;
self.fvm = Some(vdl_fvm_dest.to_path_buf());
}
if let Some(f) = &self.build_args {
let vdl_args_dest = dir.join("femu_buildargs");
let vdl_args_src = f.as_path();
unix::fs::symlink(&vdl_args_src, &vdl_args_dest)?;
self.build_args = Some(vdl_args_dest.to_path_buf());
}
Ok(())
}
}
#[derive(Clone)]
pub struct SSHKeys {
pub authorized_keys: PathBuf,
pub private_key: PathBuf,
}
impl fmt::Debug for SSHKeys {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "[fvdl] private_key {:?}", self.private_key)?;
write!(f, "[fvdl] authorized_keys {:?}", self.authorized_keys)
}
}
impl SSHKeys {
/// Initialize SSH key files for in-tree usage.
///
/// Requires the environment variable FUCHSIA_BUILD_DIR to be specified.
pub fn from_tree_env(f: &mut impl FuchsiaPaths) -> Result<Self> {
let ssh_file = File::open(f.find_fuchsia_root()?.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 authorized_keys = PathBuf::from(lines.next().unwrap()?);
Ok(Self { authorized_keys: authorized_keys, 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<Self> {
Ok(Self {
authorized_keys: home_dir().unwrap_or_default().join(".ssh/fuchsia_authorized_keys"),
private_key: home_dir().unwrap_or_default().join(".ssh/fuchsia_ed25519"),
})
}
pub fn check(&self) -> Result<()> {
if !self.private_key.exists() {
ffx_bail!("private_key file at {:?} does not exist", self.private_key);
}
if !self.authorized_keys.exists() {
ffx_bail!("authorized_keys file at {:?} does not exist", self.authorized_keys);
}
Ok(())
}
pub fn update_paths_from_args(&mut self, start_command: &StartCommand) {
if let Some(path) = &start_command.ssh {
let ssh_path = PathBuf::from(path);
self.authorized_keys = ssh_path.join("fuchsia_authorized_keys").to_path_buf();
self.private_key = ssh_path.join("fuchsia_ed25519").to_path_buf();
}
}
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_authorized_keys_dest = dir.join("fuchsia_authorized_keys");
let vdl_authorized_keys_src = self.authorized_keys.as_path();
unix::fs::symlink(&vdl_authorized_keys_src, &vdl_authorized_keys_dest)?;
self.authorized_keys = vdl_authorized_keys_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 start_package_server: bool,
pub packages_to_serve: String,
pub device_proto: String,
pub gpu: String,
pub gcs_bucket: String,
pub gcs_image_archive: String,
pub sdk_version: String,
pub cache_root: PathBuf,
pub extra_kerel_args: String,
pub amber_unpack_root: String,
pub package_server_port: String,
pub acceleration: bool,
pub image_architecture: String,
pub isolated_ffx_config_path: String,
}
impl From<&StartCommand> for VDLArgs {
fn from(cmd: &StartCommand) -> Self {
let mut gpu = get_default_graphics();
if cmd.host_gpu {
gpu = "host".to_string();
} else if cmd.software_gpu {
gpu = "swiftshader_indirect".to_string();
}
let mut enable_grpcwebproxy = false;
let mut grpcwebproxy_port = "0".to_string();
match cmd.grpcwebproxy {
Some(port) => {
enable_grpcwebproxy = true;
grpcwebproxy_port = format!("{}", port);
}
_ => (),
}
let gcs_image = cmd.image_name.as_ref().unwrap_or(&String::from("qemu-x64")).to_string();
let sdk_version = match &cmd.sdk_version {
Some(version) => version.to_string(),
None => match get_sdk_version_from_manifest() {
Ok(version) => version,
Err(_) => String::from(""),
},
};
let mut cache_path = PathBuf::new();
if cmd.cache_image {
cache_path = get_sdk_data_dir().unwrap_or_default().join(&gcs_image).join(&sdk_version);
}
VDLArgs {
headless: cmd.headless,
tuntap: cmd.tuntap,
enable_hidpi_scaling: cmd.hidpi_scaling,
upscript: cmd.upscript.as_ref().unwrap_or(&String::from("")).to_string(),
start_package_server: cmd.start_package_server,
packages_to_serve: cmd
.packages_to_serve
.as_ref()
.unwrap_or(&String::from(""))
.to_string(),
device_proto: cmd.device_proto.as_ref().unwrap_or(&String::from("")).to_string(),
gpu: gpu,
enable_grpcwebproxy: enable_grpcwebproxy,
grpcwebproxy_port: grpcwebproxy_port,
gcs_bucket: cmd.gcs_bucket.as_ref().unwrap_or(&String::from("fuchsia")).to_string(),
gcs_image_archive: gcs_image,
sdk_version: sdk_version,
cache_root: cache_path,
extra_kerel_args: cmd.kernel_args.as_ref().unwrap_or(&String::from("")).to_string(),
amber_unpack_root: cmd
.amber_unpack_root
.as_ref()
.unwrap_or(&String::from(""))
.to_string(),
package_server_port: cmd.package_server_port.as_ref().unwrap_or(&0).to_string(),
acceleration: !cmd.noacceleration,
image_architecture: cmd
.image_architecture
.as_ref()
.unwrap_or(&String::from(""))
.to_string(),
isolated_ffx_config_path: cmd
.isolated_ffx_config_path
.as_ref()
.unwrap_or(&String::from(""))
.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
use std::io::Write;
use tempfile::Builder;
#[test]
fn test_convert_start_cmd_to_vdl() {
let start_command = &StartCommand {
tuntap: true,
upscript: Some("/path/to/upscript".to_string()),
packages_to_serve: Some("pkg1.far,pkg2.far".to_string()),
host_gpu: true,
aemu_version: Some("git_revision:da1cc2ee512714a176f08b8b5fec035994ca305d".to_string()),
sdk_version: Some("0.20201130.3.1".to_string()),
image_name: Some("qemu-x64".to_string()),
package_server_log: Some("/a/b/c/server.log".to_string()),
envs: Vec::new(),
cache_image: true,
isolated_ffx_config_path: Some("/a/b/c/isolated_config.txt".to_string()),
..Default::default()
};
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.device_proto, "");
assert_eq!(vdl_args.gpu, "host");
assert_eq!(vdl_args.start_package_server, false);
assert_eq!(vdl_args.acceleration, true);
assert_eq!(vdl_args.package_server_port, "0");
assert_eq!(vdl_args.amber_unpack_root, "");
assert_eq!(vdl_args.isolated_ffx_config_path, "/a/b/c/isolated_config.txt");
assert!(vdl_args.cache_root.as_path().ends_with("qemu-x64/0.20201130.3.1"));
}
#[test]
#[serial]
fn test_image_intree_files() -> Result<()> {
env::remove_var("FUCHSIA_BUILD_DIR");
let mut mock = MockFuchsiaPaths::new();
let data = format!(
"/build/out
",
);
let tmp_dir = Builder::new().prefix("fvdl_tests_").tempdir()?;
let a = tmp_dir.into_path();
let b = a.join("/build/out");
File::create(a.join(".fx-build-dir"))?.write_all(data.as_bytes())?;
mock.expect_find_fuchsia_root().returning(move || Ok(a.clone()));
mock.expect_find_fuchsia_build_dir().returning(move || Ok(b.clone()));
mock.expect_get_image_path().returning(|name: Vec<&str>, _: &str| {
let mut p = PathBuf::from("/build/out");
p.push(name[0]);
Ok(p)
});
let mut image_files = ImageFiles::from_tree_env(&mut mock)?;
assert_eq!(image_files.zbi.to_str().unwrap(), "/build/out/zircon-a");
assert_eq!(image_files.kernel.to_str().unwrap(), "/build/out/qemu-kernel");
assert_eq!(image_files.fvm.as_ref().unwrap().to_str().unwrap(), "/build/out/storage-full");
assert_eq!(
image_files.build_args.as_ref().unwrap().to_str().unwrap(),
"/build/out/buildargs"
);
// amber_files are optional and is only specified if the file exists. For unit test
// the file always does not exit.
assert_eq!(image_files.amber_files, None);
image_files.update_paths_from_args(&StartCommand {
fvm_image: Some("/path/to/new_fvm".to_string()),
zbi_image: Some("/path/to/new_zbi".to_string()),
kernel_image: Some("/path/to/new_kernel".to_string()),
amber_files: Some("/path/to/amber_files".to_string()),
..Default::default()
});
assert_eq!(image_files.zbi.to_str().unwrap(), "/path/to/new_zbi");
assert_eq!(image_files.kernel.to_str().unwrap(), "/path/to/new_kernel");
assert_eq!(image_files.fvm.as_ref().unwrap().to_str().unwrap(), "/path/to/new_fvm");
assert_eq!(
image_files.amber_files.as_ref().unwrap().to_str().unwrap(),
"/path/to/amber_files"
);
assert_eq!(
image_files.build_args.as_ref().unwrap().to_str().unwrap(),
"/build/out/buildargs"
);
let tmp_dir = Builder::new().prefix("fvdl_tests_").tempdir()?;
image_files.stage_files(&tmp_dir.path().to_owned())?;
assert_eq!(image_files.kernel.to_str(), tmp_dir.path().join("femu_kernel").to_str());
assert_eq!(
image_files.fvm.as_ref().unwrap().to_str(),
tmp_dir.path().join("femu_fvm").to_str()
);
Ok(())
}
#[test]
#[serial]
fn test_image_sdk_files() -> Result<()> {
let mut image_files = ImageFiles::from_sdk_env()?;
image_files.update_paths_from_args(&StartCommand {
amber_files: Some("/path/to/amber_files".to_string()),
fvm_image: Some("/path/to/new_fvm".to_string()),
kernel_image: Some("/path/to/new_kernel".to_string()),
zbi_image: Some("/path/to/new_zbi".to_string()),
..Default::default()
});
assert_eq!(
image_files.amber_files.as_ref().unwrap().to_str().unwrap(),
"/path/to/amber_files"
);
assert_eq!(image_files.build_args, None);
assert_eq!(image_files.fvm.as_ref().unwrap().to_str().unwrap(), "/path/to/new_fvm");
assert_eq!(image_files.kernel.to_str().unwrap(), "/path/to/new_kernel");
assert_eq!(image_files.zbi.to_str().unwrap(), "/path/to/new_zbi");
let tmp_dir = Builder::new().prefix("fvdl_tests_").tempdir()?;
image_files.stage_files(&tmp_dir.path().to_owned())?;
assert_eq!(image_files.kernel.to_str(), tmp_dir.path().join("femu_kernel").to_str());
assert_eq!(
image_files.fvm.as_ref().unwrap().to_str(),
tmp_dir.path().join("femu_fvm").to_str()
);
assert_eq!(image_files.build_args, None);
Ok(())
}
#[test]
#[serial]
fn test_ssh_files() -> Result<()> {
let mut mock = MockFuchsiaPaths::new();
let data = format!(
"/usr/local/home/foo/.ssh/fuchsia_ed25519
/usr/local/home/foo/.ssh/fuchsia_authorized_keys
",
);
let tmp_dir = Builder::new().prefix("fvdl_tests_").tempdir()?;
let a = tmp_dir.into_path();
File::create(a.join(".fx-ssh-path"))?.write_all(data.as_bytes())?;
mock.expect_find_fuchsia_root().returning(move || Ok(a.clone()));
let mut ssh_files = SSHKeys::from_tree_env(&mut mock)?;
assert_eq!(
ssh_files.private_key.to_str().unwrap(),
"/usr/local/home/foo/.ssh/fuchsia_ed25519"
);
assert_eq!(
ssh_files.authorized_keys.to_str().unwrap(),
"/usr/local/home/foo/.ssh/fuchsia_authorized_keys"
);
let tmp_dir = Builder::new().prefix("fvdl_test_ssh_").tempdir()?;
ssh_files.stage_files(&tmp_dir.path().to_owned())?;
assert!(ssh_files.private_key.ends_with("id_ed25519"));
Ok(())
}
#[test]
#[serial]
fn test_ssh_dir() -> Result<()> {
let mut ssh_files = SSHKeys::from_sdk_env()?;
assert_eq!(
ssh_files.private_key,
home_dir().unwrap_or_default().join(".ssh/fuchsia_ed25519")
);
assert_eq!(
ssh_files.authorized_keys,
home_dir().unwrap_or_default().join(".ssh/fuchsia_authorized_keys")
);
ssh_files.update_paths_from_args(&StartCommand {
ssh: Some("/path/to/ssh".to_string()),
..Default::default()
});
assert_eq!(ssh_files.private_key.to_str().unwrap(), "/path/to/ssh/fuchsia_ed25519");
assert_eq!(
ssh_files.authorized_keys.to_str().unwrap(),
"/path/to/ssh/fuchsia_authorized_keys"
);
Ok(())
}
#[test]
#[serial]
fn test_sdk_data_dir() -> Result<()> {
let tmp_dir = Builder::new().prefix("fvdl_test_sdk_data_dir_").tempdir()?;
env::set_var("FUCHSIA_SDK_DATA_DIR", tmp_dir.path());
let p = get_sdk_data_dir()?;
assert_eq!(p.to_str(), tmp_dir.path().to_str());
Ok(())
}
}