blob: 9a689e78d7d90c3af794175c268cbc950f63726d [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::api::{
get_config,
query::ConfigQuery,
validate_type,
value::{ConfigValue, ValueStrategy},
ConfigError,
},
crate::cache::load_config,
crate::environment::Environment,
crate::mapping::{
cache::cache, config::config, data::data, env_var::env_var, file_check::file_check,
home::home, runtime::runtime,
},
crate::nested::RecursiveMap,
crate::paths::get_default_user_file_path,
crate::storage::Config,
analytics::{is_opted_in, set_opt_in_status},
anyhow::{anyhow, bail, Context, Result},
serde_json::Value,
std::{
convert::{From, TryFrom, TryInto},
fs::File,
io::Write,
path::PathBuf,
},
};
pub use config_macros::FfxConfigBacked;
pub mod api;
pub mod environment;
pub mod logging;
pub mod sdk;
mod cache;
mod mapping;
mod nested;
mod paths;
mod runtime;
mod storage;
pub use cache::{env_file, init, test_env_file, test_init};
pub use paths::default_env_path;
const SDK_TYPE_IN_TREE: &str = "in-tree";
const SDK_NOT_FOUND_HELP: &str = "\
SDK directory could not be found. Please set with
`ffx sdk set root <PATH_TO_SDK_DIR>`\n
If you are developing in the fuchsia tree, ensure \
that you are running the `ffx` command (in $FUCHSIA_DIR/.jiri_root) or `fx ffx`, not a built binary.
Running the binary directly is not supported in the fuchsia tree.\n\n";
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum ConfigLevel {
Default,
Build,
Global,
User,
Runtime,
}
impl argh::FromArgValue for ConfigLevel {
fn from_arg_value(val: &str) -> Result<Self, String> {
match val {
"u" | "user" => Ok(ConfigLevel::User),
"b" | "build" => Ok(ConfigLevel::Build),
"g" | "global" => Ok(ConfigLevel::Global),
_ => Err(String::from(
"Unrecognized value. Possible values are \"user\",\"build\",\"global\".",
)),
}
}
}
pub async fn raw<'a, T, U>(query: U) -> std::result::Result<T, T::Error>
where
T: TryFrom<ConfigValue> + ValueStrategy,
<T as std::convert::TryFrom<ConfigValue>>::Error: std::convert::From<ConfigError>,
U: Into<ConfigQuery<'a>>,
{
let converted_query = query.into();
T::validate_query(&converted_query)?;
get_config(converted_query)
.await
.map_err(|e| e.into())?
.recursive_map(&validate_type::<T>)
.try_into()
}
pub async fn get<'a, T, U>(query: U) -> std::result::Result<T, T::Error>
where
T: TryFrom<ConfigValue> + ValueStrategy,
<T as std::convert::TryFrom<ConfigValue>>::Error: std::convert::From<ConfigError>,
U: Into<ConfigQuery<'a>>,
{
let converted_query = query.into();
T::validate_query(&converted_query)?;
get_config(converted_query)
.await
.map_err(|e| e.into())?
.recursive_map(&runtime)
.recursive_map(&cache)
.recursive_map(&data)
.recursive_map(&config)
.recursive_map(&home)
.recursive_map(&env_var)
.recursive_map(&T::handle_arrays)
.recursive_map(&validate_type::<T>)
.try_into()
}
pub async fn file<'a, T, U>(query: U) -> std::result::Result<T, T::Error>
where
T: TryFrom<ConfigValue> + ValueStrategy,
<T as std::convert::TryFrom<ConfigValue>>::Error: std::convert::From<ConfigError>,
U: Into<ConfigQuery<'a>>,
{
let converted_query = query.into();
T::validate_query(&converted_query)?;
get_config(converted_query)
.await
.map_err(|e| e.into())?
.recursive_map(&runtime)
.recursive_map(&cache)
.recursive_map(&data)
.recursive_map(&config)
.recursive_map(&home)
.recursive_map(&env_var)
.recursive_map(&T::handle_arrays)
.recursive_map(&file_check)
.try_into()
}
pub async fn set<'a, U: Into<ConfigQuery<'a>>>(query: U, value: Value) -> Result<()> {
let config_query: ConfigQuery<'a> = query.into();
let level = if let Some(l) = config_query.level {
l
} else {
bail!("level of configuration is required to set a value");
};
check_config_files(&level, &config_query.build_dir.map(String::from))?;
let config = load_config(&config_query.build_dir.map(String::from)).await?;
let mut write_guard = config.write().await;
let config_changed = (*write_guard).set(&config_query, value)?;
// FIXME(81502): There is a race between the ffx CLI and the daemon service
// in updating the config. We can lose changes if both try to change the
// config at the same time. We can reduce the rate of races by only writing
// to the config if the value actually changed.
if config_changed {
save_config(&mut *write_guard, &config_query.build_dir.map(String::from))
} else {
Ok(())
}
}
fn check_config_files(level: &ConfigLevel, build_dir: &Option<String>) -> Result<()> {
let e = env_file().ok_or(anyhow!("Could not find environment file"))?;
let mut environment = Environment::load(&e)?;
match level {
ConfigLevel::User => {
if let None = environment.user {
let default_path = get_default_user_file_path();
// This will override the config file if it exists. This would happen anyway
// because of the cache.
let mut file = File::create(&default_path).context("opening write buffer")?;
file.write_all(b"{}").context("writing default user configuration file")?;
file.sync_all().context("syncing default user configuration file to filesystem")?;
environment.user = Some(
default_path
.to_str()
.map(|s| s.to_string())
.context("home path is not proper unicode")?,
);
environment.save(&e)?;
}
}
ConfigLevel::Global => {
if let None = environment.global {
bail!(
"Global configuration not set. Use 'ffx config env set' command \
to setup the environment."
);
}
}
ConfigLevel::Build => match build_dir {
Some(b_dir) => match environment.build {
None => bail!(
"Build configuration not set for '{}'. Use 'ffx config env set' command \
to setup the environment.",
b_dir
),
Some(b) => {
if let None = b.get(b_dir) {
bail!(
"Build configuration not set for '{}'. Use 'ffx config env \
set' command to setup the environment.",
b_dir
);
}
}
},
None => bail!("Cannot set a build configuration without a build directory."),
},
_ => bail!("This config level is not writable."),
}
Ok(())
}
pub async fn remove<'a, U: Into<ConfigQuery<'a>>>(query: U) -> Result<()> {
let config_query: ConfigQuery<'a> = query.into();
let config = load_config(&config_query.build_dir.map(String::from)).await?;
let mut write_guard = config.write().await;
(*write_guard).remove(&config_query)?;
save_config(&mut *write_guard, &config_query.build_dir.map(String::from))
}
pub async fn add<'a, U: Into<ConfigQuery<'a>>>(query: U, value: Value) -> Result<()> {
let config_query: ConfigQuery<'a> = query.into();
let level = if let Some(l) = config_query.level {
l
} else {
bail!("level of configuration is required to add a value");
};
check_config_files(&level, &config_query.build_dir.map(String::from))?;
let config = load_config(&config_query.build_dir.map(String::from)).await?;
let mut write_guard = config.write().await;
let config_changed = if let Some(mut current) = (*write_guard).get(&config_query) {
if current.is_object() {
bail!("cannot add a value to a subtree");
} else {
match current.as_array_mut() {
Some(v) => {
v.push(value);
(*write_guard).set(&config_query, Value::Array(v.to_vec()))?
}
None => (*write_guard).set(&config_query, Value::Array(vec![current, value]))?,
}
}
} else {
(*write_guard).set(&config_query, value)?
};
// FIXME(81502): There is a race between the ffx CLI and the daemon service
// in updating the config. We can lose changes if both try to change the
// config at the same time. We can reduce the rate of races by only writing
// to the config if the value actually changed.
if config_changed {
save_config(&mut *write_guard, &config_query.build_dir.map(String::from))
} else {
Ok(())
}
}
pub fn save_config(config: &mut Config, build_dir: &Option<String>) -> Result<()> {
let e = env_file().ok_or(anyhow!("Could not find environment file"))?;
let env = Environment::load(&e)?;
let build = build_dir.as_ref().and_then(|b| env.build.as_ref().and_then(|c| c.get(b)));
config.save(env.global.as_ref(), build, env.user.as_ref())
}
pub async fn print_config<W: Write>(mut writer: W, build_dir: &Option<String>) -> Result<()> {
let config = load_config(build_dir).await?;
let read_guard = config.read().await;
writeln!(writer, "{}", *read_guard).context("displaying config")
}
pub async fn get_log_dirs() -> Result<Vec<String>> {
match get("log.dir").await {
Ok(log_dirs) => Ok(log_dirs),
Err(e) => errors::ffx_bail!("Failed to load host log directories from ffx config: {:?}", e),
}
}
pub async fn get_sdk() -> Result<sdk::Sdk> {
match (get("sdk.root").await, get("sdk.type").await.unwrap_or("".to_string())) {
(Ok(manifest), sdk_type) => {
if sdk_type == SDK_TYPE_IN_TREE {
let module_manifest: Option<String> = get("sdk.module").await.ok();
sdk::Sdk::from_build_dir(manifest, module_manifest)
} else {
sdk::Sdk::from_sdk_dir(manifest)
}
}
(Err(e), sdk_type) => {
if sdk_type != SDK_TYPE_IN_TREE {
let path = std::env::current_exe().map_err(|e| {
errors::ffx_error!(
"{}Error was: failed to get current ffx exe path for SDK root: {:?}",
SDK_NOT_FOUND_HELP,
e
)
})?;
match find_sdk_root(path) {
Ok(Some(root)) => return sdk::Sdk::from_sdk_dir(root),
Ok(None) => {
errors::ffx_bail!(
"{}Could not find an SDK manifest in any parent of ffx's directory.",
SDK_NOT_FOUND_HELP,
);
}
Err(e) => {
errors::ffx_bail!("{}Error was: {:?}", SDK_NOT_FOUND_HELP, e);
}
}
}
errors::ffx_bail!("{}Error was: {:?}", SDK_NOT_FOUND_HELP, e);
}
}
}
fn find_sdk_root(start_path: PathBuf) -> Result<Option<PathBuf>> {
let mut path = std::fs::canonicalize(start_path.clone())
.context(format!("canonicalizing ffx path {:?}", start_path))?;
loop {
path =
if let Some(parent) = path.parent() { parent.to_path_buf() } else { return Ok(None) };
if path.join("meta").join("manifest.json").exists() {
return Ok(Some(path));
}
}
}
pub async fn set_metrics_status(value: bool) -> Result<()> {
set_opt_in_status(value).await
}
pub async fn show_metrics_status<W: Write>(mut writer: W) -> Result<()> {
let state = match is_opted_in().await {
true => "enabled",
false => "disabled",
};
writeln!(&mut writer, "Analytics data collection is {}", state)?;
Ok(())
}
////////////////////////////////////////////////////////////////////////////////
// tests
#[cfg(test)]
mod test {
use super::*;
// This is to get the FfxConfigBacked derive to compile, as it
// creates a token stream referencing `ffx_config` on the inside.
use crate as ffx_config;
use serde_json::json;
use std::path::PathBuf;
use tempfile::tempdir;
#[test]
fn test_check_config_files_fails() {
let levels = vec![
ConfigLevel::Runtime,
ConfigLevel::Default,
ConfigLevel::Global,
ConfigLevel::Build,
];
let build_dir = None;
levels.iter().for_each(|level| {
let result = check_config_files(&level, &build_dir);
assert!(result.is_err());
});
}
#[test]
fn test_validating_types() {
assert!(validate_type::<String>(json!("test")).is_some());
assert!(validate_type::<String>(json!(1)).is_none());
assert!(validate_type::<String>(json!(false)).is_none());
assert!(validate_type::<String>(json!(true)).is_none());
assert!(validate_type::<String>(json!({"test": "whatever"})).is_none());
assert!(validate_type::<String>(json!(["test", "test2"])).is_none());
assert!(validate_type::<bool>(json!(true)).is_some());
assert!(validate_type::<bool>(json!(false)).is_some());
assert!(validate_type::<bool>(json!("true")).is_some());
assert!(validate_type::<bool>(json!("false")).is_some());
assert!(validate_type::<bool>(json!(1)).is_none());
assert!(validate_type::<bool>(json!("test")).is_none());
assert!(validate_type::<bool>(json!({"test": "whatever"})).is_none());
assert!(validate_type::<bool>(json!(["test", "test2"])).is_none());
assert!(validate_type::<u64>(json!(2)).is_some());
assert!(validate_type::<u64>(json!(100)).is_some());
assert!(validate_type::<u64>(json!("100")).is_some());
assert!(validate_type::<u64>(json!("0")).is_some());
assert!(validate_type::<u64>(json!(true)).is_none());
assert!(validate_type::<u64>(json!("test")).is_none());
assert!(validate_type::<u64>(json!({"test": "whatever"})).is_none());
assert!(validate_type::<u64>(json!(["test", "test2"])).is_none());
assert!(validate_type::<PathBuf>(json!("/")).is_some());
assert!(validate_type::<PathBuf>(json!("test")).is_some());
assert!(validate_type::<PathBuf>(json!(true)).is_none());
assert!(validate_type::<PathBuf>(json!({"test": "whatever"})).is_none());
assert!(validate_type::<PathBuf>(json!(["test", "test2"])).is_none());
}
#[test]
fn test_converting_array() -> Result<()> {
let c = |val: Value| -> ConfigValue { ConfigValue(Some(val)) };
let conv_elem: Vec<String> = c(json!("test")).try_into()?;
assert_eq!(1, conv_elem.len());
let conv_string: Vec<String> = c(json!(["test", "test2"])).try_into()?;
assert_eq!(2, conv_string.len());
let conv_bool: Vec<bool> = c(json!([true, "false", false])).try_into()?;
assert_eq!(3, conv_bool.len());
let conv_bool_2: Vec<bool> = c(json!([36, "false", false])).try_into()?;
assert_eq!(2, conv_bool_2.len());
let conv_num: Vec<u64> = c(json!([3, "36", 1000])).try_into()?;
assert_eq!(3, conv_num.len());
let conv_num_2: Vec<u64> = c(json!([3, "false", 1000])).try_into()?;
assert_eq!(2, conv_num_2.len());
let bad_elem: std::result::Result<Vec<u64>, ConfigError> = c(json!("test")).try_into();
assert!(bad_elem.is_err());
let bad_elem_2: std::result::Result<Vec<u64>, ConfigError> = c(json!(["test"])).try_into();
assert!(bad_elem_2.is_err());
Ok(())
}
#[derive(FfxConfigBacked, Default)]
struct TestConfigBackedStruct {
#[ffx_config_default(key = "test.test.thing", default = "thing")]
value: Option<String>,
#[ffx_config_default(default = "what", key = "oops")]
reverse_value: Option<String>,
#[ffx_config_default(key = "other.test.thing")]
other_value: Option<f64>,
}
#[derive(FfxConfigBacked, Default)] // This should just compile despite having no config.
struct TestEmptyBackedStruct {}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_config_backed_attribute() {
ffx_config::test_init().expect("create test config");
let mut empty_config_struct = TestConfigBackedStruct::default();
assert!(empty_config_struct.value.is_none());
assert_eq!(empty_config_struct.value().await.unwrap(), "thing");
assert!(empty_config_struct.reverse_value.is_none());
assert_eq!(empty_config_struct.reverse_value().await.unwrap(), "what");
ffx_config::set(
("test.test.thing", ConfigLevel::User),
Value::String("config_value_thingy".to_owned()),
)
.await
.unwrap();
ffx_config::set(
("other.test.thing", ConfigLevel::User),
Value::Number(serde_json::Number::from_f64(2f64).unwrap()),
)
.await
.unwrap();
// If this is set, this should pop up before the config values.
empty_config_struct.value = Some("wat".to_owned());
assert_eq!(empty_config_struct.value().await.unwrap(), "wat");
empty_config_struct.value = None;
assert_eq!(empty_config_struct.value().await.unwrap(), "config_value_thingy");
assert_eq!(empty_config_struct.other_value().await.unwrap().unwrap(), 2f64);
ffx_config::set(
("other.test.thing", ConfigLevel::User),
Value::String("oaiwhfoiwh".to_owned()),
)
.await
.unwrap();
// This should just compile and drop without panicking is all.
let _ignore = TestEmptyBackedStruct {};
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_find_sdk_root_finds_root() {
let temp = tempdir().unwrap();
let start_path = temp.path().to_path_buf().join("test1").join("test2");
std::fs::create_dir_all(start_path.clone()).unwrap();
let meta_path = temp.path().to_path_buf().join("meta");
std::fs::create_dir(meta_path.clone()).unwrap();
std::fs::write(meta_path.join("manifest.json"), "").unwrap();
assert_eq!(find_sdk_root(start_path).unwrap().unwrap(), temp.path().to_path_buf());
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_find_sdk_root_no_manifest() {
let temp = tempdir().unwrap();
let start_path = temp.path().to_path_buf().join("test1").join("test2");
std::fs::create_dir_all(start_path.clone()).unwrap();
let meta_path = temp.path().to_path_buf().join("meta");
std::fs::create_dir(meta_path.clone()).unwrap();
assert!(find_sdk_root(start_path).unwrap().is_none());
}
}