Merge "aconfig: introduce new aflags CLI" into main am: 30512e43be
Original change: https://android-review.googlesource.com/c/platform/build/+/2956393 Change-Id: I983eb31ada9a5f957f4c61c9e28d8ecfa7ed18d0 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -18,6 +18,7 @@
|
|||||||
PRODUCT_PACKAGES += \
|
PRODUCT_PACKAGES += \
|
||||||
abx \
|
abx \
|
||||||
adbd_system_api \
|
adbd_system_api \
|
||||||
|
aflags \
|
||||||
am \
|
am \
|
||||||
android.hidl.base-V1.0-java \
|
android.hidl.base-V1.0-java \
|
||||||
android.hidl.manager-V1.0-java \
|
android.hidl.manager-V1.0-java \
|
||||||
|
@@ -4,6 +4,7 @@ members = [
|
|||||||
"aconfig",
|
"aconfig",
|
||||||
"aconfig_protos",
|
"aconfig_protos",
|
||||||
"aconfig_storage_file",
|
"aconfig_storage_file",
|
||||||
|
"aflags",
|
||||||
"printflags"
|
"printflags"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@@ -74,6 +74,11 @@
|
|||||||
{
|
{
|
||||||
// aconfig_storage read api cpp integration tests
|
// aconfig_storage read api cpp integration tests
|
||||||
"name": "aconfig_storage.test.cpp"
|
"name": "aconfig_storage.test.cpp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// aflags CLI unit tests
|
||||||
|
// TODO(b/326062088): add to presubmit once proven in postsubmit.
|
||||||
|
"name": "aflags.test"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
29
tools/aconfig/aflags/Android.bp
Normal file
29
tools/aconfig/aflags/Android.bp
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package {
|
||||||
|
default_applicable_licenses: ["Android-Apache-2.0"],
|
||||||
|
}
|
||||||
|
|
||||||
|
rust_defaults {
|
||||||
|
name: "aflags.defaults",
|
||||||
|
edition: "2021",
|
||||||
|
clippy_lints: "android",
|
||||||
|
lints: "android",
|
||||||
|
srcs: ["src/main.rs"],
|
||||||
|
rustlibs: [
|
||||||
|
"libaconfig_protos",
|
||||||
|
"libanyhow",
|
||||||
|
"libclap",
|
||||||
|
"libprotobuf",
|
||||||
|
"libregex",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
rust_binary {
|
||||||
|
name: "aflags",
|
||||||
|
defaults: ["aflags.defaults"],
|
||||||
|
}
|
||||||
|
|
||||||
|
rust_test_host {
|
||||||
|
name: "aflags.test",
|
||||||
|
defaults: ["aflags.defaults"],
|
||||||
|
test_suites: ["general-tests"],
|
||||||
|
}
|
12
tools/aconfig/aflags/Cargo.toml
Normal file
12
tools/aconfig/aflags/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "aflags"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.69"
|
||||||
|
paste = "1.0.11"
|
||||||
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
protobuf = "3.2.0"
|
||||||
|
regex = "1.10.3"
|
||||||
|
aconfig_protos = { path = "../aconfig_protos" }
|
171
tools/aconfig/aflags/src/device_config_source.rs
Normal file
171
tools/aconfig/aflags/src/device_config_source.rs
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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 crate::{Flag, FlagPermission, FlagSource, ValuePickedFrom};
|
||||||
|
use aconfig_protos::ProtoFlagPermission as ProtoPermission;
|
||||||
|
use aconfig_protos::ProtoFlagState as ProtoState;
|
||||||
|
use aconfig_protos::ProtoParsedFlag;
|
||||||
|
use aconfig_protos::ProtoParsedFlags;
|
||||||
|
use anyhow::{anyhow, bail, Result};
|
||||||
|
use regex::Regex;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::process::Command;
|
||||||
|
use std::{fs, str};
|
||||||
|
|
||||||
|
pub struct DeviceConfigSource {}
|
||||||
|
|
||||||
|
fn convert_parsed_flag(flag: &ProtoParsedFlag) -> Flag {
|
||||||
|
let namespace = flag.namespace().to_string();
|
||||||
|
let package = flag.package().to_string();
|
||||||
|
let name = flag.name().to_string();
|
||||||
|
|
||||||
|
let container = if flag.container().is_empty() {
|
||||||
|
"system".to_string()
|
||||||
|
} else {
|
||||||
|
flag.container().to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let value = match flag.state() {
|
||||||
|
ProtoState::ENABLED => "true",
|
||||||
|
ProtoState::DISABLED => "false",
|
||||||
|
}
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let permission = match flag.permission() {
|
||||||
|
ProtoPermission::READ_ONLY => FlagPermission::ReadOnly,
|
||||||
|
ProtoPermission::READ_WRITE => FlagPermission::ReadWrite,
|
||||||
|
};
|
||||||
|
|
||||||
|
Flag {
|
||||||
|
namespace,
|
||||||
|
package,
|
||||||
|
name,
|
||||||
|
container,
|
||||||
|
value,
|
||||||
|
permission,
|
||||||
|
value_picked_from: ValuePickedFrom::Default,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_pb_files() -> Result<Vec<Flag>> {
|
||||||
|
let mut flags: BTreeMap<String, Flag> = BTreeMap::new();
|
||||||
|
for partition in ["system", "system_ext", "product", "vendor"] {
|
||||||
|
let path = format!("/{}/etc/aconfig_flags.pb", partition);
|
||||||
|
let Ok(bytes) = fs::read(&path) else {
|
||||||
|
eprintln!("warning: failed to read {}", path);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let parsed_flags: ProtoParsedFlags = protobuf::Message::parse_from_bytes(&bytes)?;
|
||||||
|
for flag in parsed_flags.parsed_flag {
|
||||||
|
let key = format!("{}.{}", flag.package(), flag.name());
|
||||||
|
let container = if flag.container().is_empty() {
|
||||||
|
"system".to_string()
|
||||||
|
} else {
|
||||||
|
flag.container().to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
if container.eq(partition) {
|
||||||
|
flags.insert(key, convert_parsed_flag(&flag));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(flags.values().cloned().collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_device_config(raw: &str) -> Result<HashMap<String, String>> {
|
||||||
|
let mut flags = HashMap::new();
|
||||||
|
let regex = Regex::new(r"(?m)^([[[:alnum:]]_]+/[[[:alnum:]]_\.]+)=(true|false)$")?;
|
||||||
|
for capture in regex.captures_iter(raw) {
|
||||||
|
let key =
|
||||||
|
capture.get(1).ok_or(anyhow!("invalid device_config output"))?.as_str().to_string();
|
||||||
|
let value = capture.get(2).ok_or(anyhow!("invalid device_config output"))?.as_str();
|
||||||
|
flags.insert(key, value.to_string());
|
||||||
|
}
|
||||||
|
Ok(flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_device_config_output(command: &str) -> Result<String> {
|
||||||
|
let output = Command::new("/system/bin/device_config").arg(command).output()?;
|
||||||
|
if !output.status.success() {
|
||||||
|
let reason = match output.status.code() {
|
||||||
|
Some(code) => format!("exit code {}", code),
|
||||||
|
None => "terminated by signal".to_string(),
|
||||||
|
};
|
||||||
|
bail!("failed to execute device_config: {}", reason);
|
||||||
|
}
|
||||||
|
Ok(str::from_utf8(&output.stdout)?.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_device_config_flags() -> Result<HashMap<String, String>> {
|
||||||
|
let list_output = read_device_config_output("list")?;
|
||||||
|
parse_device_config(&list_output)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reconcile(pb_flags: &[Flag], dc_flags: HashMap<String, String>) -> Vec<Flag> {
|
||||||
|
pb_flags
|
||||||
|
.iter()
|
||||||
|
.map(|f| {
|
||||||
|
dc_flags
|
||||||
|
.get(&format!("{}/{}.{}", f.namespace, f.package, f.name))
|
||||||
|
.map(|value| {
|
||||||
|
if value.eq(&f.value) {
|
||||||
|
Flag { value_picked_from: ValuePickedFrom::Default, ..f.clone() }
|
||||||
|
} else {
|
||||||
|
Flag {
|
||||||
|
value_picked_from: ValuePickedFrom::Server,
|
||||||
|
value: value.to_string(),
|
||||||
|
..f.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(f.clone())
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FlagSource for DeviceConfigSource {
|
||||||
|
fn list_flags() -> Result<Vec<Flag>> {
|
||||||
|
let pb_flags = read_pb_files()?;
|
||||||
|
let dc_flags = read_device_config_flags()?;
|
||||||
|
|
||||||
|
let flags = reconcile(&pb_flags, dc_flags);
|
||||||
|
Ok(flags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_device_config() {
|
||||||
|
let input = r#"
|
||||||
|
namespace_one/com.foo.bar.flag_one=true
|
||||||
|
namespace_one/com.foo.bar.flag_two=false
|
||||||
|
random_noise;
|
||||||
|
namespace_two/android.flag_one=true
|
||||||
|
namespace_two/android.flag_two=nonsense
|
||||||
|
"#;
|
||||||
|
let expected = HashMap::from([
|
||||||
|
("namespace_one/com.foo.bar.flag_one".to_string(), "true".to_string()),
|
||||||
|
("namespace_one/com.foo.bar.flag_two".to_string(), "false".to_string()),
|
||||||
|
("namespace_two/android.flag_one".to_string(), "true".to_string()),
|
||||||
|
]);
|
||||||
|
let actual = parse_device_config(input).unwrap();
|
||||||
|
assert_eq!(expected, actual);
|
||||||
|
}
|
||||||
|
}
|
163
tools/aconfig/aflags/src/main.rs
Normal file
163
tools/aconfig/aflags/src/main.rs
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
//! `aflags` is a device binary to read and write aconfig flags.
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
mod device_config_source;
|
||||||
|
use device_config_source::DeviceConfigSource;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum FlagPermission {
|
||||||
|
ReadOnly,
|
||||||
|
ReadWrite,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for FlagPermission {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
match &self {
|
||||||
|
Self::ReadOnly => "read-only".into(),
|
||||||
|
Self::ReadWrite => "read-write".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum ValuePickedFrom {
|
||||||
|
Default,
|
||||||
|
Server,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for ValuePickedFrom {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
match &self {
|
||||||
|
Self::Default => "default".into(),
|
||||||
|
Self::Server => "server".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Flag {
|
||||||
|
namespace: String,
|
||||||
|
name: String,
|
||||||
|
package: String,
|
||||||
|
container: String,
|
||||||
|
value: String,
|
||||||
|
permission: FlagPermission,
|
||||||
|
value_picked_from: ValuePickedFrom,
|
||||||
|
}
|
||||||
|
|
||||||
|
trait FlagSource {
|
||||||
|
fn list_flags() -> Result<Vec<Flag>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ABOUT_TEXT: &str = "Tool for reading and writing flags.
|
||||||
|
|
||||||
|
Rows in the table from the `list` command follow this format:
|
||||||
|
|
||||||
|
package flag_name value provenance permission container
|
||||||
|
|
||||||
|
* `package`: package set for this flag in its .aconfig definition.
|
||||||
|
* `flag_name`: flag name, also set in definition.
|
||||||
|
* `value`: the value read from the flag.
|
||||||
|
* `provenance`: one of:
|
||||||
|
+ `default`: the flag value comes from its build-time default.
|
||||||
|
+ `server`: the flag value comes from a server override.
|
||||||
|
* `permission`: read-write or read-only.
|
||||||
|
* `container`: the container for the flag, configured in its definition.
|
||||||
|
";
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[clap(long_about=ABOUT_TEXT)]
|
||||||
|
struct Cli {
|
||||||
|
#[clap(subcommand)]
|
||||||
|
command: Command,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
enum Command {
|
||||||
|
/// List all aconfig flags on this device.
|
||||||
|
List,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PaddingInfo {
|
||||||
|
longest_package_col: usize,
|
||||||
|
longest_name_col: usize,
|
||||||
|
longest_val_col: usize,
|
||||||
|
longest_value_picked_from_col: usize,
|
||||||
|
longest_permission_col: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_flag_row(flag: &Flag, info: &PaddingInfo) -> String {
|
||||||
|
let pkg = &flag.package;
|
||||||
|
let p0 = info.longest_package_col + 1;
|
||||||
|
|
||||||
|
let name = &flag.name;
|
||||||
|
let p1 = info.longest_name_col + 1;
|
||||||
|
|
||||||
|
let val = flag.value.to_string();
|
||||||
|
let p2 = info.longest_val_col + 1;
|
||||||
|
|
||||||
|
let value_picked_from = flag.value_picked_from.to_string();
|
||||||
|
let p3 = info.longest_value_picked_from_col + 1;
|
||||||
|
|
||||||
|
let perm = flag.permission.to_string();
|
||||||
|
let p4 = info.longest_permission_col + 1;
|
||||||
|
|
||||||
|
let container = &flag.container;
|
||||||
|
|
||||||
|
format!("{pkg:p0$}{name:p1$}{val:p2$}{value_picked_from:p3$}{perm:p4$}{container}\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list() -> Result<String> {
|
||||||
|
let flags = DeviceConfigSource::list_flags()?;
|
||||||
|
let padding_info = PaddingInfo {
|
||||||
|
longest_package_col: flags.iter().map(|f| f.package.len()).max().unwrap_or(0),
|
||||||
|
longest_name_col: flags.iter().map(|f| f.name.len()).max().unwrap_or(0),
|
||||||
|
longest_val_col: flags.iter().map(|f| f.value.to_string().len()).max().unwrap_or(0),
|
||||||
|
longest_value_picked_from_col: flags
|
||||||
|
.iter()
|
||||||
|
.map(|f| f.value_picked_from.to_string().len())
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0),
|
||||||
|
longest_permission_col: flags
|
||||||
|
.iter()
|
||||||
|
.map(|f| f.permission.to_string().len())
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut result = String::from("");
|
||||||
|
for flag in flags {
|
||||||
|
let row = format_flag_row(&flag, &padding_info);
|
||||||
|
result.push_str(&row);
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
let output = match cli.command {
|
||||||
|
Command::List => list(),
|
||||||
|
};
|
||||||
|
match output {
|
||||||
|
Ok(text) => println!("{text}"),
|
||||||
|
Err(msg) => println!("Error: {}", msg),
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user