blob: 7319f4dcd797a0d8c29d2161a6515509c9c37279 [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::value::{ConfigValue, ValueStrategy};
use crate::api::{validate_type, ConfigError};
use analytics::{is_opted_in, set_opt_in_status};
use anyhow::{anyhow, Context, Result};
use std::io::Write;
use std::path::PathBuf;
use std::sync::Mutex;
pub mod api;
pub mod environment;
pub mod keys;
pub mod logging;
pub mod runtime;
mod aliases;
mod cache;
mod mapping;
mod nested;
mod paths;
mod storage;
pub use aliases::{
is_analytics_disabled, is_mdns_autoconnect_disabled, is_mdns_discovery_disabled,
is_usb_discovery_disabled,
};
pub use api::query::{BuildOverride, ConfigQuery, SelectMode};
pub use config_macros::FfxConfigBacked;
pub use environment::{test_init, Environment, EnvironmentContext, TestEnv};
pub use sdk::{self, Sdk, SdkRoot};
pub use storage::ConfigMap;
lazy_static::lazy_static! {
static ref ENV: Mutex<Option<EnvironmentContext>> = Mutex::default();
}
#[doc(hidden)]
pub mod macro_deps {
pub use {anyhow, serde_json};
}
/// The levels of configuration possible
// If you edit this enum, make sure to also change the enum counter below to match.
#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)]
pub enum ConfigLevel {
/// Default configurations are provided through GN build rules across all subcommands and are hard-coded and immutable.
Default,
/// Global configuration is intended to be a system-wide configuration level.
Global,
/// User configuration is configuration set in the user's home directory and applies to all invocations of ffx by that user.
User,
/// Build configuration is associated with a build directory.
Build,
/// Runtime configuration is set by the user when invoking ffx, and can't be 'set' by any other means.
Runtime,
}
impl ConfigLevel {
/// The number of elements in the above enum, used for tests.
const _COUNT: usize = 5;
/// Iterates over the config levels in priority order, starting from the most narrow scope if given None.
/// Note this is not conformant to Iterator::next(), it's just meant to be a simple source of truth about ordering.
pub(crate) fn next(current: Option<Self>) -> Option<Self> {
use ConfigLevel::*;
match current {
Some(Default) => None,
Some(Global) => Some(Default),
Some(User) => Some(Global),
Some(Build) => Some(User),
Some(Runtime) => Some(Build),
None => Some(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 fn global_env_context() -> Option<EnvironmentContext> {
ENV.lock().unwrap().clone()
}
pub fn global_env() -> Result<Environment> {
let context =
global_env_context().context("Tried to load global environment before configuration")?;
match context.load() {
Err(err) => {
tracing::error!("failed to load environment, reverting to default: {}", err);
Ok(Environment::new_empty(context))
}
Ok(ctx) => Ok(ctx),
}
}
/// Initialize the configuration. Only the first call in a process runtime takes effect, so users must
/// call this early with the required values, such as in main() in the ffx binary.
pub async fn init(context: &EnvironmentContext) -> Result<()> {
let mut env_lock = ENV.lock().unwrap();
if env_lock.is_some() {
anyhow::bail!("Attempted to set the global environment more than once in a process invocation, outside of a test");
}
env_lock.replace(context.clone());
Ok(())
}
/// Creates a [`ConfigQuery`] against the global config cache and environment.
///
/// Example:
///
/// ```no_run
/// use ffx_config::ConfigLevel;
/// use ffx_config::BuildSelect;
/// use ffx_config::SelectMode;
///
/// let query = ffx_config::build()
/// .name("testing")
/// .level(Some(ConfigLevel::Build))
/// .build(Some(BuildSelect::Path("/tmp/build.json")))
/// .select(SelectMode::All);
/// let value = query.get().await?;
/// ```
pub fn build<'a>() -> ConfigQuery<'a> {
ConfigQuery::default()
}
/// Creates a [`ConfigQuery`] against the global config cache and environment,
/// using the provided value converted in to a base query.
///
/// Example:
///
/// ```no_run
/// ffx_config::query("a_key").get();
/// ffx_config::query(ffx_config::ConfigLevel::User).get();
/// ```
pub fn query<'a>(with: impl Into<ConfigQuery<'a>>) -> ConfigQuery<'a> {
with.into()
}
/// A shorthand for the very common case of querying a value from the global config
/// cache and environment, using the provided value converted into a query.
pub async fn get<'a, T, U>(with: 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>>,
{
query(with).get()
}
pub const SDK_OVERRIDE_KEY_PREFIX: &str = "sdk.overrides";
/// Returns the path to the tool with the given name by first
/// checking for configured override with the key of `sdk.override.{name}`,
/// and no override is found, sdk.get_host_tool() is called.
pub async fn get_host_tool(sdk: &Sdk, name: &str) -> Result<PathBuf> {
// Check for configured override for the host tool.
let override_key = format!("{SDK_OVERRIDE_KEY_PREFIX}.{name}");
let override_result: Result<PathBuf, ConfigError> = query(&override_key).get();
if let Ok(tool_path) = override_result {
if tool_path.exists() {
tracing::info!("Using configured override for {name}: {tool_path:?}");
return Ok(tool_path);
} else {
return Err(anyhow!(
"Override path for {name} set to {tool_path:?}, but does not exist"
));
}
}
sdk.get_host_tool(name)
}
pub async fn print_config<W: Write>(ctx: &EnvironmentContext, mut writer: W) -> Result<()> {
let config = ctx.load()?.config_from_cache(None)?;
let read_guard = config.read().map_err(|_| anyhow!("config read guard"))?;
writeln!(writer, "{}", *read_guard).context("displaying config")
}
pub async fn get_log_dirs() -> Result<Vec<String>> {
match query("log.dir").get() {
Ok(log_dirs) => Ok(log_dirs),
Err(e) => errors::ffx_bail!("Failed to load host log directories from ffx config: {:?}", e),
}
}
/// Print out useful hints about where important log information might be found after an error.
pub async fn print_log_hint<W: std::io::Write>(writer: &mut W) {
let msg = match get_log_dirs().await {
Ok(log_dirs) if log_dirs.len() == 1 => format!(
"More information may be available in ffx host logs in directory:\n {}",
log_dirs[0]
),
Ok(log_dirs) => format!(
"More information may be available in ffx host logs in directories:\n {}",
log_dirs.join("\n ")
),
Err(err) => format!(
"More information may be available in ffx host logs, but ffx failed to retrieve configured log file locations. Error:\n {}",
err,
),
};
if writeln!(writer, "{}", msg).is_err() {
println!("{}", msg);
}
}
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::{self as ffx_config};
use serde_json::{json, Value};
use std::collections::HashSet;
use std::fs;
#[test]
fn test_config_levels_make_sense_from_first() {
let mut found_set = HashSet::new();
let mut from_first = None;
for _ in 0..ConfigLevel::_COUNT + 1 {
if let Some(next) = ConfigLevel::next(from_first) {
let entry = found_set.get(&next);
assert!(entry.is_none(), "Found duplicate config level while iterating: {next:?}");
found_set.insert(next);
from_first = Some(next);
} else {
break;
}
}
assert_eq!(
ConfigLevel::_COUNT,
found_set.len(),
"A config level was missing from the forward iteration of levels: {found_set:?}"
);
}
#[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 env = ffx_config::test_init().await.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");
env.context
.query("test.test.thing")
.level(Some(ConfigLevel::User))
.set(Value::String("config_value_thingy".to_owned()))
.await
.unwrap();
env.context
.query("other.test.thing")
.level(Some(ConfigLevel::User))
.set(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);
env.context
.query("other.test.thing")
.level(Some(ConfigLevel::User))
.set(Value::String("oaiwhfoiwh".to_owned()))
.await
.unwrap();
// This should just compile and drop without panicking is all.
let _ignore = TestEmptyBackedStruct {};
}
/// Writes the file to $root, with the path $path, from the source tree prefix $prefix
/// (relative to this source file)
macro_rules! put_file {
($root:expr, $prefix:literal, $name:literal) => {{
fs::create_dir_all($root.join($name).parent().unwrap()).unwrap();
fs::File::create($root.join($name))
.unwrap()
.write_all(include_bytes!(concat!($prefix, "/", $name)))
.unwrap();
}};
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_get_host_tool() {
let env = ffx_config::test_init().await.expect("create test config");
let sdk_root = env.isolate_root.path().join("sdk");
env.context
.query("sdk.root")
.level(Some(ConfigLevel::User))
.set(sdk_root.to_string_lossy().into())
.await
.expect("creating temp sdk root");
put_file!(sdk_root, "../test_data/sdk", "meta/manifest.json");
put_file!(sdk_root, "../test_data/sdk", "tools/x64/a_host_tool-meta.json");
let sdk = env.context.get_sdk().await.expect("test sdk");
let result = get_host_tool(&sdk, "a_host_tool").await.expect("a_host_tool");
assert_eq!(result, sdk_root.join("tools/x64/a-host-tool"));
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_get_host_tool_override() {
let env = ffx_config::test_init().await.expect("create test config");
let sdk_root = env.isolate_root.path().join("sdk");
env.context
.query("sdk.root")
.level(Some(ConfigLevel::User))
.set(sdk_root.to_string_lossy().into())
.await
.expect("creating temp sdk root");
put_file!(sdk_root, "../test_data/sdk", "meta/manifest.json");
put_file!(sdk_root, "../test_data/sdk", "tools/x64/a_host_tool-meta.json");
// Override the path via config
let override_path = env.isolate_root.path().join("a_override_host_tool");
fs::write(&override_path, "a_override_tool_contents").expect("override file written");
env.context
.query(&format!("{SDK_OVERRIDE_KEY_PREFIX}.a_host_tool"))
.level(Some(ConfigLevel::User))
.set(override_path.to_string_lossy().into())
.await
.expect("setting override");
let sdk = env.context.get_sdk().await.expect("test sdk");
let result = get_host_tool(&sdk, "a_host_tool").await.expect("a_host_tool");
assert_eq!(result, override_path);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_get_host_tool_override_no_exists() {
let env = ffx_config::test_init().await.expect("create test config");
let sdk_root = env.isolate_root.path().join("sdk");
env.context
.query("sdk.root")
.level(Some(ConfigLevel::User))
.set(sdk_root.to_string_lossy().into())
.await
.expect("creating temp sdk root");
put_file!(sdk_root, "../test_data/sdk", "meta/manifest.json");
put_file!(sdk_root, "../test_data/sdk", "tools/x64/a_host_tool-meta.json");
// Override the path via config
let override_path = env.isolate_root.path().join("a_override_host_tool");
// do not create file, this should report an error.
env.context
.query(&format!("{SDK_OVERRIDE_KEY_PREFIX}.a_host_tool"))
.level(Some(ConfigLevel::User))
.set(override_path.to_string_lossy().into())
.await
.expect("setting override");
let sdk = env.context.get_sdk().await.expect("test sdk");
let result = get_host_tool(&sdk, "a_host_tool").await;
assert_eq!(
result.err().unwrap().to_string(),
format!("Override path for a_host_tool set to {override_path:?}, but does not exist")
);
}
}