diff --git a/tools/aconfig/.gitignore b/tools/aconfig/.gitignore new file mode 100644 index 0000000000..1b72444aef --- /dev/null +++ b/tools/aconfig/.gitignore @@ -0,0 +1,2 @@ +/Cargo.lock +/target diff --git a/tools/aconfig/Android.bp b/tools/aconfig/Android.bp index b3813bfe5e..e762f331f6 100644 --- a/tools/aconfig/Android.bp +++ b/tools/aconfig/Android.bp @@ -18,7 +18,11 @@ rust_defaults { srcs: ["src/main.rs"], rustlibs: [ "libaconfig_protos", + "libanyhow", + "libclap", "libprotobuf", + "libserde", + "libserde_json", ], } diff --git a/tools/aconfig/Cargo.toml b/tools/aconfig/Cargo.toml new file mode 100644 index 0000000000..b439858c9a --- /dev/null +++ b/tools/aconfig/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "aconfig" +version = "0.1.0" +edition = "2021" +build = "build.rs" + +[features] +default = ["cargo"] +cargo = [] + +[dependencies] +anyhow = "1.0.69" +clap = { version = "4.1.8", features = ["derive"] } +protobuf = "3.2.0" +serde = { version = "1.0.152", features = ["derive"] } +serde_json = "1.0.93" + +[build-dependencies] +protobuf-codegen = "3.2.0" diff --git a/tools/aconfig/build.rs b/tools/aconfig/build.rs new file mode 100644 index 0000000000..5ef5b6090f --- /dev/null +++ b/tools/aconfig/build.rs @@ -0,0 +1,17 @@ +use protobuf_codegen::Codegen; + +fn main() { + let proto_files = vec!["protos/aconfig.proto"]; + + // tell cargo to only re-run the build script if any of the proto files has changed + for path in &proto_files { + println!("cargo:rerun-if-changed={}", path); + } + + Codegen::new() + .pure() + .include("protos") + .inputs(proto_files) + .cargo_out_dir("aconfig_proto") + .run_from_script(); +} diff --git a/tools/aconfig/protos/aconfig.proto b/tools/aconfig/protos/aconfig.proto index 989c39866c..65817ca682 100644 --- a/tools/aconfig/protos/aconfig.proto +++ b/tools/aconfig/protos/aconfig.proto @@ -12,12 +12,34 @@ // See the License for the specific language governing permissions and // limitations under the License -// Placeholder proto file. Will be replaced by actual contents. +// This is the schema definition for of Aconfig files. Modifications need to be +// either backwards compatible, or include updates to all Aconfig files in the +// Android tree. -syntax = "proto3"; +syntax = "proto2"; package android.aconfig; -message Placeholder { - string name = 1; +message value { + required bool value = 1; + optional uint32 since = 2; } + +message flag { + required string id = 1; + required string description = 2; + repeated value value = 3; +}; + +message android_config { + repeated flag flag = 1; +}; + +message override { + required string id = 1; + required bool value = 2; +}; + +message override_config { + repeated override override = 1; +}; diff --git a/tools/aconfig/src/aconfig.rs b/tools/aconfig/src/aconfig.rs new file mode 100644 index 0000000000..22fcb884b1 --- /dev/null +++ b/tools/aconfig/src/aconfig.rs @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use anyhow::{anyhow, Context, Error, Result}; + +use crate::protos::{ + ProtoAndroidConfig, ProtoFlag, ProtoOverride, ProtoOverrideConfig, ProtoValue, +}; + +#[derive(Debug, PartialEq, Eq)] +pub struct Value { + value: bool, + since: Option, +} + +#[allow(dead_code)] // only used in unit tests +impl Value { + pub fn new(value: bool, since: u32) -> Value { + Value { value, since: Some(since) } + } + + pub fn default(value: bool) -> Value { + Value { value, since: None } + } +} + +impl TryFrom for Value { + type Error = Error; + + fn try_from(proto: ProtoValue) -> Result { + let Some(value) = proto.value else { + return Err(anyhow!("missing 'value' field")); + }; + Ok(Value { value, since: proto.since }) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Flag { + pub id: String, + pub description: String, + + // ordered by Value.since; guaranteed to contain at least one item (the default value, with + // since == None) + pub values: Vec, +} + +impl Flag { + #[allow(dead_code)] // only used in unit tests + pub fn try_from_text_proto(text_proto: &str) -> Result { + let proto: ProtoFlag = crate::protos::try_from_text_proto(text_proto) + .with_context(|| text_proto.to_owned())?; + proto.try_into() + } + + pub fn try_from_text_proto_list(text_proto: &str) -> Result> { + let proto: ProtoAndroidConfig = crate::protos::try_from_text_proto(text_proto) + .with_context(|| text_proto.to_owned())?; + proto.flag.into_iter().map(|proto_flag| proto_flag.try_into()).collect() + } + + pub fn resolve_value(&self, build_id: u32) -> bool { + let mut value = self.values[0].value; + for candidate in self.values.iter().skip(1) { + let since = candidate.since.expect("invariant: non-defaults values have Some(since)"); + if since <= build_id { + value = candidate.value; + } + } + value + } +} + +impl TryFrom for Flag { + type Error = Error; + + fn try_from(proto: ProtoFlag) -> Result { + let Some(id) = proto.id else { + return Err(anyhow!("missing 'id' field")); + }; + let Some(description) = proto.description else { + return Err(anyhow!("missing 'description' field")); + }; + if proto.value.is_empty() { + return Err(anyhow!("missing 'value' field")); + } + + let mut values: Vec = vec![]; + for proto_value in proto.value.into_iter() { + let v: Value = proto_value.try_into()?; + if values.iter().any(|w| v.since == w.since) { + let msg = match v.since { + None => format!("flag {}: multiple default values", id), + Some(x) => format!("flag {}: multiple values for since={}", id, x), + }; + return Err(anyhow!(msg)); + } + values.push(v); + } + values.sort_by_key(|v| v.since); + + Ok(Flag { id, description, values }) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Override { + pub id: String, + pub value: bool, +} + +impl Override { + #[allow(dead_code)] // only used in unit tests + pub fn try_from_text_proto(text_proto: &str) -> Result { + let proto: ProtoOverride = crate::protos::try_from_text_proto(text_proto)?; + proto.try_into() + } + + pub fn try_from_text_proto_list(text_proto: &str) -> Result> { + let proto: ProtoOverrideConfig = crate::protos::try_from_text_proto(text_proto)?; + proto.override_.into_iter().map(|proto_flag| proto_flag.try_into()).collect() + } +} + +impl TryFrom for Override { + type Error = Error; + + fn try_from(proto: ProtoOverride) -> Result { + let Some(id) = proto.id else { + return Err(anyhow!("missing 'id' field")); + }; + let Some(value) = proto.value else { + return Err(anyhow!("missing 'value' field")); + }; + Ok(Override { id, value }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_flag_try_from_text_proto() { + let expected = Flag { + id: "1234".to_owned(), + description: "Description of the flag".to_owned(), + values: vec![Value::default(false), Value::new(true, 8)], + }; + + let s = r#" + id: "1234" + description: "Description of the flag" + value { + value: false + } + value { + value: true + since: 8 + } + "#; + let actual = Flag::try_from_text_proto(s).unwrap(); + + assert_eq!(expected, actual); + } + + #[test] + fn test_flag_try_from_text_proto_bad_input() { + let s = r#" + id: "a" + description: "Description of the flag" + "#; + let error = Flag::try_from_text_proto(s).unwrap_err(); + assert_eq!(format!("{:?}", error), "missing 'value' field"); + + let s = r#" + description: "Description of the flag" + value { + value: true + } + "#; + let error = Flag::try_from_text_proto(s).unwrap_err(); + assert!(format!("{:?}", error).contains("Message not initialized")); + + let s = r#" + id: "a" + description: "Description of the flag" + value { + value: true + } + value { + value: true + } + "#; + let error = Flag::try_from_text_proto(s).unwrap_err(); + assert_eq!(format!("{:?}", error), "flag a: multiple default values"); + } + + #[test] + fn test_flag_try_from_text_proto_list() { + let expected = vec![ + Flag { + id: "a".to_owned(), + description: "A".to_owned(), + values: vec![Value::default(true)], + }, + Flag { + id: "b".to_owned(), + description: "B".to_owned(), + values: vec![Value::default(false)], + }, + ]; + + let s = r#" + flag { + id: "a" + description: "A" + value { + value: true + } + } + flag { + id: "b" + description: "B" + value { + value: false + } + } + "#; + let actual = Flag::try_from_text_proto_list(s).unwrap(); + + assert_eq!(expected, actual); + } + + #[test] + fn test_override_try_from_text_proto_list() { + let expected = Override { id: "1234".to_owned(), value: true }; + + let s = r#" + id: "1234" + value: true + "#; + let actual = Override::try_from_text_proto(s).unwrap(); + + assert_eq!(expected, actual); + } + + #[test] + fn test_resolve_value() { + let flag = Flag { + id: "a".to_owned(), + description: "A".to_owned(), + values: vec![ + Value::default(true), + Value::new(false, 10), + Value::new(true, 20), + Value::new(false, 30), + ], + }; + assert!(flag.resolve_value(0)); + assert!(flag.resolve_value(9)); + assert!(!flag.resolve_value(10)); + assert!(!flag.resolve_value(11)); + assert!(!flag.resolve_value(19)); + assert!(flag.resolve_value(20)); + assert!(flag.resolve_value(21)); + assert!(flag.resolve_value(29)); + assert!(!flag.resolve_value(30)); + assert!(!flag.resolve_value(10_000)); + } +} diff --git a/tools/aconfig/src/cache.rs b/tools/aconfig/src/cache.rs new file mode 100644 index 0000000000..d27459dcf5 --- /dev/null +++ b/tools/aconfig/src/cache.rs @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use anyhow::{anyhow, Result}; +use serde::{Deserialize, Serialize}; +use std::io::{Read, Write}; + +use crate::aconfig::{Flag, Override}; +use crate::commands::Source; + +#[derive(Serialize, Deserialize)] +pub struct Item { + pub id: String, + pub description: String, + pub value: bool, + pub debug: Vec, +} + +#[derive(Serialize, Deserialize)] +pub struct Cache { + build_id: u32, + items: Vec, +} + +impl Cache { + pub fn new(build_id: u32) -> Cache { + Cache { build_id, items: vec![] } + } + + pub fn read_from_reader(reader: impl Read) -> Result { + serde_json::from_reader(reader).map_err(|e| e.into()) + } + + pub fn write_to_writer(&self, writer: impl Write) -> Result<()> { + serde_json::to_writer(writer, self).map_err(|e| e.into()) + } + + pub fn add_flag(&mut self, source: Source, flag: Flag) -> Result<()> { + if self.items.iter().any(|item| item.id == flag.id) { + return Err(anyhow!( + "failed to add flag {} from {}: flag already defined", + flag.id, + source, + )); + } + let value = flag.resolve_value(self.build_id); + self.items.push(Item { + id: flag.id.clone(), + description: flag.description, + value, + debug: vec![format!("{}:{}", source, value)], + }); + Ok(()) + } + + pub fn add_override(&mut self, source: Source, override_: Override) -> Result<()> { + let Some(existing_item) = self.items.iter_mut().find(|item| item.id == override_.id) else { + return Err(anyhow!("failed to override flag {}: unknown flag", override_.id)); + }; + existing_item.value = override_.value; + existing_item.debug.push(format!("{}:{}", source, override_.value)); + Ok(()) + } + + pub fn iter(&self) -> impl Iterator { + self.items.iter() + } +} + +impl Item {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::aconfig::Value; + + #[test] + fn test_add_flag() { + let mut cache = Cache::new(1); + cache + .add_flag( + Source::File("first.txt".to_string()), + Flag { + id: "foo".to_string(), + description: "desc".to_string(), + values: vec![Value::default(true)], + }, + ) + .unwrap(); + let error = cache + .add_flag( + Source::File("second.txt".to_string()), + Flag { + id: "foo".to_string(), + description: "desc".to_string(), + values: vec![Value::default(false)], + }, + ) + .unwrap_err(); + assert_eq!( + &format!("{:?}", error), + "failed to add flag foo from second.txt: flag already defined" + ); + } + + #[test] + fn test_add_override() { + fn check_value(cache: &Cache, id: &str, expected: bool) -> bool { + cache.iter().find(|&item| item.id == id).unwrap().value == expected + } + + let mut cache = Cache::new(1); + let error = cache + .add_override(Source::Memory, Override { id: "foo".to_string(), value: true }) + .unwrap_err(); + assert_eq!(&format!("{:?}", error), "failed to override flag foo: unknown flag"); + + cache + .add_flag( + Source::File("first.txt".to_string()), + Flag { + id: "foo".to_string(), + description: "desc".to_string(), + values: vec![Value::default(true)], + }, + ) + .unwrap(); + assert!(check_value(&cache, "foo", true)); + + cache + .add_override(Source::Memory, Override { id: "foo".to_string(), value: false }) + .unwrap(); + assert!(check_value(&cache, "foo", false)); + + cache + .add_override(Source::Memory, Override { id: "foo".to_string(), value: true }) + .unwrap(); + assert!(check_value(&cache, "foo", true)); + } +} diff --git a/tools/aconfig/src/commands.rs b/tools/aconfig/src/commands.rs new file mode 100644 index 0000000000..76b853b405 --- /dev/null +++ b/tools/aconfig/src/commands.rs @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use anyhow::{Context, Result}; +use clap::ValueEnum; +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::io::Read; + +use crate::aconfig::{Flag, Override}; +use crate::cache::Cache; + +#[derive(Clone, Serialize, Deserialize)] +pub enum Source { + #[allow(dead_code)] // only used in unit tests + Memory, + File(String), +} + +impl fmt::Display for Source { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Memory => write!(f, ""), + Self::File(path) => write!(f, "{}", path), + } + } +} + +pub struct Input { + pub source: Source, + pub reader: Box, +} + +pub fn create_cache(build_id: u32, aconfigs: Vec, overrides: Vec) -> Result { + let mut cache = Cache::new(build_id); + + for mut input in aconfigs { + let mut contents = String::new(); + input.reader.read_to_string(&mut contents)?; + let flags = Flag::try_from_text_proto_list(&contents) + .with_context(|| format!("Failed to parse {}", input.source))?; + for flag in flags { + cache.add_flag(input.source.clone(), flag)?; + } + } + + for mut input in overrides { + let mut contents = String::new(); + input.reader.read_to_string(&mut contents)?; + let overrides = Override::try_from_text_proto_list(&contents) + .with_context(|| format!("Failed to parse {}", input.source))?; + for override_ in overrides { + cache.add_override(input.source.clone(), override_)?; + } + } + + Ok(cache) +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)] +pub enum Format { + Text, + Debug, +} + +pub fn dump_cache(cache: Cache, format: Format) -> Result<()> { + match format { + Format::Text => { + for item in cache.iter() { + println!("{}: {}", item.id, item.value); + } + } + Format::Debug => { + for item in cache.iter() { + println!("{}: {} ({:?})", item.id, item.value, item.debug); + } + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_create_cache() { + let s = r#" + flag { + id: "a" + description: "Description of a" + value { + value: true + } + } + "#; + let aconfigs = vec![Input { source: Source::Memory, reader: Box::new(s.as_bytes()) }]; + let o = r#" + override { + id: "a" + value: false + } + "#; + let overrides = vec![Input { source: Source::Memory, reader: Box::new(o.as_bytes()) }]; + let cache = create_cache(1, aconfigs, overrides).unwrap(); + let value = cache.iter().find(|&item| item.id == "a").unwrap().value; + assert!(!value); + } +} diff --git a/tools/aconfig/src/main.rs b/tools/aconfig/src/main.rs index 2f7255edab..3ce974781e 100644 --- a/tools/aconfig/src/main.rs +++ b/tools/aconfig/src/main.rs @@ -16,38 +16,75 @@ //! `aconfig` is a build time tool to manage build time configurations, such as feature flags. -use aconfig_protos::aconfig::Placeholder; -use protobuf::text_format::{parse_from_str, ParseError}; +use anyhow::Result; +use clap::{builder::ArgAction, builder::EnumValueParser, Arg, Command}; +use std::fs; -fn foo() -> Result { - let placeholder = parse_from_str::(r#"name: "aconfig""#)?; - Ok(placeholder.name) +mod aconfig; +mod cache; +mod commands; +mod protos; + +use crate::cache::Cache; +use commands::{Input, Source}; + +fn cli() -> Command { + Command::new("aconfig") + .subcommand_required(true) + .subcommand( + Command::new("create-cache") + .arg( + Arg::new("build-id") + .long("build-id") + .value_parser(clap::value_parser!(u32)) + .required(true), + ) + .arg(Arg::new("aconfig").long("aconfig").action(ArgAction::Append)) + .arg(Arg::new("override").long("override").action(ArgAction::Append)) + .arg(Arg::new("cache").long("cache").required(true)), + ) + .subcommand( + Command::new("dump").arg(Arg::new("cache").long("cache").required(true)).arg( + Arg::new("format") + .long("format") + .value_parser(EnumValueParser::::new()) + .default_value("text"), + ), + ) } -fn main() { - println!("{:?}", foo()); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_foo() { - assert_eq!("aconfig", foo().unwrap()); - } - - #[test] - fn test_binary_protobuf() { - use protobuf::Message; - let mut buffer = Vec::new(); - - let mut original = Placeholder::new(); - original.name = "test".to_owned(); - original.write_to_writer(&mut buffer).unwrap(); - - let copy = Placeholder::parse_from_reader(&mut buffer.as_slice()).unwrap(); - - assert_eq!(original, copy); +fn main() -> Result<()> { + let matches = cli().get_matches(); + match matches.subcommand() { + Some(("create-cache", sub_matches)) => { + let mut aconfigs = vec![]; + let build_id = *sub_matches.get_one::("build-id").unwrap(); + for path in + sub_matches.get_many::("aconfig").unwrap_or_default().collect::>() + { + let file = Box::new(fs::File::open(path)?); + aconfigs.push(Input { source: Source::File(path.to_string()), reader: file }); + } + let mut overrides = vec![]; + for path in + sub_matches.get_many::("override").unwrap_or_default().collect::>() + { + let file = Box::new(fs::File::open(path)?); + overrides.push(Input { source: Source::File(path.to_string()), reader: file }); + } + let cache = commands::create_cache(build_id, aconfigs, overrides)?; + let path = sub_matches.get_one::("cache").unwrap(); + let file = fs::File::create(path)?; + cache.write_to_writer(file)?; + } + Some(("dump", sub_matches)) => { + let path = sub_matches.get_one::("cache").unwrap(); + let file = fs::File::open(path)?; + let cache = Cache::read_from_reader(file)?; + let format = sub_matches.get_one("format").unwrap(); + commands::dump_cache(cache, *format)?; + } + _ => unreachable!(), } + Ok(()) } diff --git a/tools/aconfig/src/protos.rs b/tools/aconfig/src/protos.rs new file mode 100644 index 0000000000..3c156b3d90 --- /dev/null +++ b/tools/aconfig/src/protos.rs @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// When building with the Android tool-chain +// +// - an external crate `aconfig_protos` will be generated +// - the feature "cargo" will be disabled +// +// When building with cargo +// +// - a local sub-module will be generated in OUT_DIR and included in this file +// - the feature "cargo" will be enabled +// +// This module hides these differences from the rest of aconfig. + +// ---- When building with the Android tool-chain ---- +#[cfg(not(feature = "cargo"))] +pub use aconfig_protos::aconfig::Android_config as ProtoAndroidConfig; + +#[cfg(not(feature = "cargo"))] +pub use aconfig_protos::aconfig::Value as ProtoValue; + +#[cfg(not(feature = "cargo"))] +pub use aconfig_protos::aconfig::Flag as ProtoFlag; + +#[cfg(not(feature = "cargo"))] +pub use aconfig_protos::aconfig::Override_config as ProtoOverrideConfig; + +#[cfg(not(feature = "cargo"))] +pub use aconfig_protos::aconfig::Override as ProtoOverride; + +// ---- When building with cargo ---- +#[cfg(feature = "cargo")] +include!(concat!(env!("OUT_DIR"), "/aconfig_proto/mod.rs")); + +#[cfg(feature = "cargo")] +pub use aconfig::Android_config as ProtoAndroidConfig; + +#[cfg(feature = "cargo")] +pub use aconfig::Value as ProtoValue; + +#[cfg(feature = "cargo")] +pub use aconfig::Flag as ProtoFlag; + +#[cfg(feature = "cargo")] +pub use aconfig::Override_config as ProtoOverrideConfig; + +#[cfg(feature = "cargo")] +pub use aconfig::Override as ProtoOverride; + +// ---- Common for both the Android tool-chain and cargo ---- +use anyhow::Result; + +pub fn try_from_text_proto(s: &str) -> Result +where + T: protobuf::MessageFull, +{ + // warning: parse_from_str does not check if required fields are set + protobuf::text_format::parse_from_str(s).map_err(|e| e.into()) +}