Merge changes from topic "aconfig-part-1"
* changes: aconfig: add support for changing flag value based on build aconfig: introduce cache aconfig: define Aconfig proto aconfig: add support for cargo
This commit is contained in:
2
tools/aconfig/.gitignore
vendored
Normal file
2
tools/aconfig/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/Cargo.lock
|
||||||
|
/target
|
@@ -18,7 +18,11 @@ rust_defaults {
|
|||||||
srcs: ["src/main.rs"],
|
srcs: ["src/main.rs"],
|
||||||
rustlibs: [
|
rustlibs: [
|
||||||
"libaconfig_protos",
|
"libaconfig_protos",
|
||||||
|
"libanyhow",
|
||||||
|
"libclap",
|
||||||
"libprotobuf",
|
"libprotobuf",
|
||||||
|
"libserde",
|
||||||
|
"libserde_json",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
19
tools/aconfig/Cargo.toml
Normal file
19
tools/aconfig/Cargo.toml
Normal file
@@ -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"
|
17
tools/aconfig/build.rs
Normal file
17
tools/aconfig/build.rs
Normal file
@@ -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();
|
||||||
|
}
|
@@ -12,12 +12,34 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License
|
// 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;
|
package android.aconfig;
|
||||||
|
|
||||||
message Placeholder {
|
message value {
|
||||||
string name = 1;
|
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;
|
||||||
|
};
|
||||||
|
284
tools/aconfig/src/aconfig.rs
Normal file
284
tools/aconfig/src/aconfig.rs
Normal file
@@ -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<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<ProtoValue> for Value {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(proto: ProtoValue) -> Result<Self, Self::Error> {
|
||||||
|
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<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Flag {
|
||||||
|
#[allow(dead_code)] // only used in unit tests
|
||||||
|
pub fn try_from_text_proto(text_proto: &str) -> Result<Flag> {
|
||||||
|
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<Vec<Flag>> {
|
||||||
|
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<ProtoFlag> for Flag {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(proto: ProtoFlag) -> Result<Self, Self::Error> {
|
||||||
|
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<Value> = 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<Override> {
|
||||||
|
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<Vec<Override>> {
|
||||||
|
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<ProtoOverride> for Override {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(proto: ProtoOverride) -> Result<Self, Self::Error> {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
153
tools/aconfig/src/cache.rs
Normal file
153
tools/aconfig/src/cache.rs
Normal file
@@ -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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct Cache {
|
||||||
|
build_id: u32,
|
||||||
|
items: Vec<Item>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cache {
|
||||||
|
pub fn new(build_id: u32) -> Cache {
|
||||||
|
Cache { build_id, items: vec![] }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_from_reader(reader: impl Read) -> Result<Cache> {
|
||||||
|
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<Item = &Item> {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
122
tools/aconfig/src/commands.rs
Normal file
122
tools/aconfig/src/commands.rs
Normal file
@@ -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, "<memory>"),
|
||||||
|
Self::File(path) => write!(f, "{}", path),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Input {
|
||||||
|
pub source: Source,
|
||||||
|
pub reader: Box<dyn Read>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_cache(build_id: u32, aconfigs: Vec<Input>, overrides: Vec<Input>) -> Result<Cache> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@@ -16,38 +16,75 @@
|
|||||||
|
|
||||||
//! `aconfig` is a build time tool to manage build time configurations, such as feature flags.
|
//! `aconfig` is a build time tool to manage build time configurations, such as feature flags.
|
||||||
|
|
||||||
use aconfig_protos::aconfig::Placeholder;
|
use anyhow::Result;
|
||||||
use protobuf::text_format::{parse_from_str, ParseError};
|
use clap::{builder::ArgAction, builder::EnumValueParser, Arg, Command};
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
fn foo() -> Result<String, ParseError> {
|
mod aconfig;
|
||||||
let placeholder = parse_from_str::<Placeholder>(r#"name: "aconfig""#)?;
|
mod cache;
|
||||||
Ok(placeholder.name)
|
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::<commands::Format>::new())
|
||||||
|
.default_value("text"),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() -> Result<()> {
|
||||||
println!("{:?}", foo());
|
let matches = cli().get_matches();
|
||||||
}
|
match matches.subcommand() {
|
||||||
|
Some(("create-cache", sub_matches)) => {
|
||||||
#[cfg(test)]
|
let mut aconfigs = vec![];
|
||||||
mod tests {
|
let build_id = *sub_matches.get_one::<u32>("build-id").unwrap();
|
||||||
use super::*;
|
for path in
|
||||||
|
sub_matches.get_many::<String>("aconfig").unwrap_or_default().collect::<Vec<_>>()
|
||||||
#[test]
|
{
|
||||||
fn test_foo() {
|
let file = Box::new(fs::File::open(path)?);
|
||||||
assert_eq!("aconfig", foo().unwrap());
|
aconfigs.push(Input { source: Source::File(path.to_string()), reader: file });
|
||||||
}
|
}
|
||||||
|
let mut overrides = vec![];
|
||||||
#[test]
|
for path in
|
||||||
fn test_binary_protobuf() {
|
sub_matches.get_many::<String>("override").unwrap_or_default().collect::<Vec<_>>()
|
||||||
use protobuf::Message;
|
{
|
||||||
let mut buffer = Vec::new();
|
let file = Box::new(fs::File::open(path)?);
|
||||||
|
overrides.push(Input { source: Source::File(path.to_string()), reader: file });
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
let cache = commands::create_cache(build_id, aconfigs, overrides)?;
|
||||||
|
let path = sub_matches.get_one::<String>("cache").unwrap();
|
||||||
|
let file = fs::File::create(path)?;
|
||||||
|
cache.write_to_writer(file)?;
|
||||||
|
}
|
||||||
|
Some(("dump", sub_matches)) => {
|
||||||
|
let path = sub_matches.get_one::<String>("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(())
|
||||||
}
|
}
|
||||||
|
73
tools/aconfig/src/protos.rs
Normal file
73
tools/aconfig/src/protos.rs
Normal file
@@ -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<T>(s: &str) -> Result<T>
|
||||||
|
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())
|
||||||
|
}
|
Reference in New Issue
Block a user