Merge "aconfig: introduce new aflags CLI" into main
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
PRODUCT_PACKAGES += \
|
||||
abx \
|
||||
adbd_system_api \
|
||||
aflags \
|
||||
am \
|
||||
android.hidl.base-V1.0-java \
|
||||
android.hidl.manager-V1.0-java \
|
||||
|
@@ -4,6 +4,7 @@ members = [
|
||||
"aconfig",
|
||||
"aconfig_protos",
|
||||
"aconfig_storage_file",
|
||||
"aflags",
|
||||
"printflags"
|
||||
]
|
||||
|
||||
|
@@ -74,6 +74,11 @@
|
||||
{
|
||||
// aconfig_storage read api cpp integration tests
|
||||
"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