blob: cff3989805f0a1469660b243593697d1c92389a8 [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::file_backed_config::FileBacked as Config,
crate::mapping::{
config::config, data::data, env_var::env_var, file_check::file_check, home::home,
identity::identity,
},
anyhow::{anyhow, bail, Context, Result},
std::{
convert::{From, TryFrom, TryInto},
env::var,
fs::{create_dir_all, File},
io::Write,
path::PathBuf,
},
};
pub use config_macros::FfxConfigBacked;
pub use serde_json::Value;
#[cfg(test)]
use tempfile::NamedTempFile;
pub mod api;
pub mod constants;
pub mod environment;
pub mod logging;
pub mod sdk;
mod cache;
mod file_backed_config;
mod mapping;
mod persistent_config;
mod priority_config;
mod runtime;
pub use cache::{env_file, init_config};
#[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, &validate_type::<T>).await.map_err(|e| e.into())?.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)?;
let env_var_mapper = env_var(&validate_type::<T>);
let home_mapper = home(&env_var_mapper);
let config_mapper = config(&home_mapper);
let data_mapper = data(&config_mapper);
let array_env_var_mapper = T::handle_arrays(&data_mapper);
get_config(converted_query, &array_env_var_mapper).await.map_err(|e| e.into())?.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)?;
let file_check_mapper = file_check(&identity);
let env_var_mapper = env_var(&file_check_mapper);
let home_mapper = home(&env_var_mapper);
let config_mapper = config(&home_mapper);
let data_mapper = data(&config_mapper);
let array_env_var_mapper = T::handle_arrays(&data_mapper);
get_config(converted_query, &array_env_var_mapper).await.map_err(|e| e.into())?.try_into()
}
pub(crate) fn get_config_base_path() -> Result<PathBuf> {
let mut path = match var("XDG_CONFIG_HOME").map(PathBuf::from) {
Ok(p) => p,
Err(_) => {
let mut home = home::home_dir().ok_or(anyhow!("cannot find home directory"))?;
home.push(".local");
home.push("share");
home
}
};
path.push("Fuchsia");
path.push("ffx");
path.push("config");
create_dir_all(&path)?;
Ok(path)
}
pub(crate) fn get_data_base_path() -> Result<PathBuf> {
let mut path = match var("XDG_DATA_HOME").map(PathBuf::from) {
Ok(p) => p,
Err(_) => {
let mut home = home::home_dir().ok_or(anyhow!("cannot find home directory"))?;
home.push(".local");
home.push("share");
home
}
};
path.push("Fuchsia");
path.push("ffx");
create_dir_all(&path)?;
Ok(path)
}
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;
(*write_guard).set(&config_query, value)?;
save_config(&mut *write_guard, &config_query.build_dir.map(String::from))
}
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;
if let Some(mut current) = (*write_guard).get(&config_query, &identity) {
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)?;
}
save_config(&mut *write_guard, &config_query.build_dir.map(String::from))
}
#[cfg(test)]
fn get_default_user_file_path() -> PathBuf {
lazy_static::lazy_static! {
static ref FILE: NamedTempFile = NamedTempFile::new().expect("tmp access failed");
}
FILE.path().to_path_buf()
}
#[cfg(not(test))]
fn get_default_user_file_path() -> PathBuf {
let mut default_path = get_config_base_path().expect("cannot get configuration base path");
default_path.push(constants::DEFAULT_USER_CONFIG);
default_path
}
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)?;
build_dir.as_ref().map_or(config.save(&env.global, &None, &env.user), |b| {
config.save(&env.global, &env.build.as_ref().and_then(|c| c.get(b)), &env.user)
})
}
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_sdk() -> Result<sdk::Sdk> {
if let Ok(manifest) = get("sdk.root").await {
if get("sdk.type").await.unwrap_or("".to_string()) == "in-tree" {
sdk::Sdk::from_build_dir(manifest)
} else {
sdk::Sdk::from_sdk_dir(manifest)
}
} else {
ffx_core::ffx_bail!(
"SDK directory could not be found. Please set with \
`ffx config set sdk.root <PATH_TO_SDK_DIR>`\n\
If you are developing in the fuchsia tree, use the Fuchsia build directory, and \
also run `ffx config set sdk.type in-tree`"
);
}
}
////////////////////////////////////////////////////////////////////////////////
// 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;
#[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() {
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();
// Error from incorrect type set in the config.
assert!(empty_config_struct.other_value().await.is_err());
// This should just compile and drop without panicking is all.
let _ignore = TestEmptyBackedStruct {};
}
}