1 /*
2 * Copyright (C) 2023 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 //! `aconfig` is a build time tool to manage build time configurations, such as feature flags.
18
19 use aconfig_storage_file::DEFAULT_FILE_VERSION;
20 use aconfig_storage_file::MAX_SUPPORTED_FILE_VERSION;
21 use anyhow::{anyhow, bail, Context, Result};
22 use clap::{builder::ArgAction, builder::EnumValueParser, Arg, ArgMatches, Command};
23 use core::any::Any;
24 use std::fs;
25 use std::io;
26 use std::io::Write;
27 use std::path::{Path, PathBuf};
28
29 mod codegen;
30 mod commands;
31 mod dump;
32 mod storage;
33
34 use aconfig_storage_file::StorageFileType;
35 use codegen::CodegenMode;
36 use convert_finalized_flags::FinalizedFlagMap;
37 use dump::DumpFormat;
38
39 #[cfg(test)]
40 mod test;
41
42 use commands::{Input, OutputFile};
43
44 const HELP_DUMP_CACHE: &str = r#"
45 An aconfig cache file, created via `aconfig create-cache`.
46 "#;
47
48 const HELP_DUMP_FORMAT: &str = r#"
49 Change the output format for each flag.
50
51 The argument to --format is a format string. Each flag will be a copy of this string, with certain
52 placeholders replaced by attributes of the flag. The placeholders are
53
54 {package}
55 {name}
56 {namespace}
57 {description}
58 {bug}
59 {state}
60 {state:bool}
61 {permission}
62 {trace}
63 {trace:paths}
64 {is_fixed_read_only}
65 {is_exported}
66 {container}
67 {metadata}
68 {fully_qualified_name}
69
70 Note: the format strings "textproto" and "protobuf" are handled in a special way: they output all
71 flag attributes in text or binary protobuf format.
72
73 Examples:
74
75 # See which files were read to determine the value of a flag; the files were read in the order
76 # listed.
77 --format='{fully_qualified_name} {trace}'
78
79 # Trace the files read for a specific flag. Useful during debugging.
80 --filter=fully_qualified_name:com.foo.flag_name --format='{trace}'
81
82 # Print a somewhat human readable description of each flag.
83 --format='The flag {name} in package {package} is {state} and has permission {permission}.'
84 "#;
85
86 const HELP_DUMP_FILTER: &str = r#"
87 Limit which flags to output. If --filter is omitted, all flags will be printed. If multiple
88 --filter options are provided, the output will be limited to flags that match any of the filters.
89
90 The argument to --filter is a search query. Multiple queries can be AND-ed together by
91 concatenating them with a plus sign.
92
93 Valid queries are:
94
95 package:<string>
96 name:<string>
97 namespace:<string>
98 bug:<string>
99 state:ENABLED|DISABLED
100 permission:READ_ONLY|READ_WRITE
101 is_fixed_read_only:true|false
102 is_exported:true|false
103 container:<string>
104 fully_qualified_name:<string>
105
106 Note: there is currently no support for filtering based on these flag attributes: description,
107 trace, metadata.
108
109 Examples:
110
111 # Print a single flag:
112 --filter=fully_qualified_name:com.foo.flag_name
113
114 # Print all known information about a single flag:
115 --filter=fully_qualified_name:com.foo.flag_name --format=textproto
116
117 # Print all flags in the com.foo package, and all enabled flags in the com.bar package:
118 --filter=package:com.foo --filter=package.com.bar+state:ENABLED
119 "#;
120
121 const HELP_DUMP_DEDUP: &str = r#"
122 Allow the same flag to be present in multiple cache files; if duplicates are found, collapse into
123 a single instance.
124 "#;
125
cli() -> Command126 fn cli() -> Command {
127 Command::new("aconfig")
128 .subcommand_required(true)
129 .subcommand(
130 Command::new("create-cache")
131 .arg(Arg::new("package").long("package").required(true))
132 .arg(Arg::new("container").long("container").required(true))
133 .arg(Arg::new("declarations").long("declarations").action(ArgAction::Append))
134 .arg(Arg::new("values").long("values").action(ArgAction::Append))
135 .arg(
136 Arg::new("default-permission")
137 .long("default-permission")
138 .value_parser(aconfig_protos::flag_permission::parse_from_str)
139 .default_value(aconfig_protos::flag_permission::to_string(
140 &commands::DEFAULT_FLAG_PERMISSION,
141 )),
142 )
143 .arg(
144 Arg::new("allow-read-write")
145 .long("allow-read-write")
146 .value_parser(clap::value_parser!(bool))
147 .default_value("true"),
148 )
149 .arg(Arg::new("cache").long("cache").required(true)),
150 )
151 .subcommand(
152 Command::new("create-java-lib")
153 .arg(Arg::new("cache").long("cache").required(true))
154 .arg(Arg::new("out").long("out").required(true))
155 .arg(
156 Arg::new("mode")
157 .long("mode")
158 .value_parser(EnumValueParser::<CodegenMode>::new())
159 .default_value("production"),
160 )
161 .arg(
162 Arg::new("single-exported-file")
163 .long("single-exported-file")
164 .value_parser(clap::value_parser!(bool))
165 .default_value("false"),
166 )
167 // TODO: b/395899938 - clean up flags for switching to new storage
168 .arg(
169 Arg::new("allow-instrumentation")
170 .long("allow-instrumentation")
171 .value_parser(clap::value_parser!(bool))
172 .default_value("false"),
173 )
174 // TODO: b/395899938 - clean up flags for switching to new storage
175 .arg(
176 Arg::new("new-exported")
177 .long("new-exported")
178 .value_parser(clap::value_parser!(bool))
179 .default_value("false"),
180 )
181 // Allows build flag toggling of checking API level in exported
182 // flag lib for finalized API flags.
183 // TODO: b/378936061 - Remove once build flag for API level
184 // check is fully enabled.
185 .arg(
186 Arg::new("check-api-level")
187 .long("check-api-level")
188 .value_parser(clap::value_parser!(bool))
189 .default_value("false"),
190 ),
191 )
192 .subcommand(
193 Command::new("create-cpp-lib")
194 .arg(Arg::new("cache").long("cache").required(true))
195 .arg(Arg::new("out").long("out").required(true))
196 .arg(
197 Arg::new("mode")
198 .long("mode")
199 .value_parser(EnumValueParser::<CodegenMode>::new())
200 .default_value("production"),
201 )
202 .arg(
203 Arg::new("allow-instrumentation")
204 .long("allow-instrumentation")
205 .value_parser(clap::value_parser!(bool))
206 .default_value("false"),
207 ),
208 )
209 .subcommand(
210 Command::new("create-rust-lib")
211 .arg(Arg::new("cache").long("cache").required(true))
212 .arg(Arg::new("out").long("out").required(true))
213 .arg(
214 Arg::new("allow-instrumentation")
215 .long("allow-instrumentation")
216 .value_parser(clap::value_parser!(bool))
217 .default_value("false"),
218 )
219 .arg(
220 Arg::new("mode")
221 .long("mode")
222 .value_parser(EnumValueParser::<CodegenMode>::new())
223 .default_value("production"),
224 ),
225 )
226 .subcommand(
227 Command::new("create-device-config-defaults")
228 .arg(Arg::new("cache").long("cache").action(ArgAction::Append).required(true))
229 .arg(Arg::new("out").long("out").default_value("-")),
230 )
231 .subcommand(
232 Command::new("create-device-config-sysprops")
233 .arg(Arg::new("cache").long("cache").action(ArgAction::Append).required(true))
234 .arg(Arg::new("out").long("out").default_value("-")),
235 )
236 .subcommand(
237 Command::new("dump-cache")
238 .alias("dump")
239 .arg(
240 Arg::new("cache")
241 .long("cache")
242 .action(ArgAction::Append)
243 .long_help(HELP_DUMP_CACHE.trim()),
244 )
245 .arg(
246 Arg::new("format")
247 .long("format")
248 .value_parser(|s: &str| DumpFormat::try_from(s))
249 .default_value(
250 "{fully_qualified_name} [{container}]: {permission} + {state}",
251 )
252 .long_help(HELP_DUMP_FORMAT.trim()),
253 )
254 .arg(
255 Arg::new("filter")
256 .long("filter")
257 .action(ArgAction::Append)
258 .long_help(HELP_DUMP_FILTER.trim()),
259 )
260 .arg(
261 Arg::new("dedup")
262 .long("dedup")
263 .num_args(0)
264 .action(ArgAction::SetTrue)
265 .long_help(HELP_DUMP_DEDUP.trim()),
266 )
267 .arg(Arg::new("out").long("out").default_value("-")),
268 )
269 .subcommand(
270 Command::new("create-storage")
271 .arg(
272 Arg::new("container")
273 .long("container")
274 .required(true)
275 .help("The target container for the generated storage file."),
276 )
277 .arg(
278 Arg::new("file")
279 .long("file")
280 .value_parser(|s: &str| StorageFileType::try_from(s)),
281 )
282 .arg(Arg::new("cache").long("cache").action(ArgAction::Append).required(true))
283 .arg(Arg::new("out").long("out").required(true))
284 .arg(
285 Arg::new("version")
286 .long("version")
287 .required(false)
288 .value_parser(|s: &str| s.parse::<u32>()),
289 ),
290 )
291 }
292
get_required_arg<'a, T>(matches: &'a ArgMatches, arg_name: &str) -> Result<&'a T> where T: Any + Clone + Send + Sync + 'static,293 fn get_required_arg<'a, T>(matches: &'a ArgMatches, arg_name: &str) -> Result<&'a T>
294 where
295 T: Any + Clone + Send + Sync + 'static,
296 {
297 matches
298 .get_one::<T>(arg_name)
299 .ok_or(anyhow!("internal error: required argument '{}' not found", arg_name))
300 }
301
get_optional_arg<'a, T>(matches: &'a ArgMatches, arg_name: &str) -> Option<&'a T> where T: Any + Clone + Send + Sync + 'static,302 fn get_optional_arg<'a, T>(matches: &'a ArgMatches, arg_name: &str) -> Option<&'a T>
303 where
304 T: Any + Clone + Send + Sync + 'static,
305 {
306 matches.get_one::<T>(arg_name)
307 }
308
open_zero_or_more_files(matches: &ArgMatches, arg_name: &str) -> Result<Vec<Input>>309 fn open_zero_or_more_files(matches: &ArgMatches, arg_name: &str) -> Result<Vec<Input>> {
310 let mut opened_files = vec![];
311 for path in matches.get_many::<String>(arg_name).unwrap_or_default() {
312 let file = Box::new(fs::File::open(path)?);
313 opened_files.push(Input { source: path.to_string(), reader: file });
314 }
315 Ok(opened_files)
316 }
317
open_single_file(matches: &ArgMatches, arg_name: &str) -> Result<Input>318 fn open_single_file(matches: &ArgMatches, arg_name: &str) -> Result<Input> {
319 let Some(path) = matches.get_one::<String>(arg_name) else {
320 bail!("missing argument {}", arg_name);
321 };
322 let file = Box::new(fs::File::open(path)?);
323 Ok(Input { source: path.to_string(), reader: file })
324 }
325
write_output_file_realtive_to_dir(root: &Path, output_file: &OutputFile) -> Result<()>326 fn write_output_file_realtive_to_dir(root: &Path, output_file: &OutputFile) -> Result<()> {
327 let path = root.join(&output_file.path);
328 let parent = path
329 .parent()
330 .ok_or(anyhow!("unable to locate parent of output file {}", path.display()))?;
331 fs::create_dir_all(parent)
332 .with_context(|| format!("failed to create directory {}", parent.display()))?;
333 let mut file =
334 fs::File::create(&path).with_context(|| format!("failed to open {}", path.display()))?;
335 file.write_all(&output_file.contents)
336 .with_context(|| format!("failed to write to {}", path.display()))?;
337 Ok(())
338 }
339
write_output_to_file_or_stdout(path: &str, data: &[u8]) -> Result<()>340 fn write_output_to_file_or_stdout(path: &str, data: &[u8]) -> Result<()> {
341 if path == "-" {
342 io::stdout().write_all(data).context("failed to write to stdout")?;
343 } else {
344 fs::File::create(path)
345 .with_context(|| format!("failed to open {}", path))?
346 .write_all(data)
347 .with_context(|| format!("failed to write to {}", path))?;
348 }
349 Ok(())
350 }
351
load_finalized_flags() -> Result<FinalizedFlagMap>352 fn load_finalized_flags() -> Result<FinalizedFlagMap> {
353 let json_str = include_str!(concat!(env!("OUT_DIR"), "/finalized_flags_record.json"));
354 let map = serde_json::from_str(json_str)?;
355 Ok(map)
356 }
357
main() -> Result<()>358 fn main() -> Result<()> {
359 let matches = cli().get_matches();
360 match matches.subcommand() {
361 Some(("create-cache", sub_matches)) => {
362 let package = get_required_arg::<String>(sub_matches, "package")?;
363 let container =
364 get_optional_arg::<String>(sub_matches, "container").map(|c| c.as_str());
365 let declarations = open_zero_or_more_files(sub_matches, "declarations")?;
366 let values = open_zero_or_more_files(sub_matches, "values")?;
367 let default_permission = get_required_arg::<aconfig_protos::ProtoFlagPermission>(
368 sub_matches,
369 "default-permission",
370 )?;
371 let allow_read_write = get_optional_arg::<bool>(sub_matches, "allow-read-write")
372 .expect("failed to parse allow-read-write");
373 let output = commands::parse_flags(
374 package,
375 container,
376 declarations,
377 values,
378 *default_permission,
379 *allow_read_write,
380 )
381 .context("failed to create cache")?;
382 let path = get_required_arg::<String>(sub_matches, "cache")?;
383 write_output_to_file_or_stdout(path, &output)?;
384 }
385 Some(("create-java-lib", sub_matches)) => {
386 let cache = open_single_file(sub_matches, "cache")?;
387 let mode = get_required_arg::<CodegenMode>(sub_matches, "mode")?;
388 let allow_instrumentation =
389 get_required_arg::<bool>(sub_matches, "allow-instrumentation")?;
390 let new_exported = get_required_arg::<bool>(sub_matches, "new-exported")?;
391 let single_exported_file =
392 get_required_arg::<bool>(sub_matches, "single-exported-file")?;
393
394 let check_api_level = get_required_arg::<bool>(sub_matches, "check-api-level")?;
395 let finalized_flags: FinalizedFlagMap =
396 if *check_api_level { load_finalized_flags()? } else { FinalizedFlagMap::new() };
397
398 let generated_files = commands::create_java_lib(
399 cache,
400 *mode,
401 *allow_instrumentation,
402 *new_exported,
403 *single_exported_file,
404 finalized_flags,
405 )
406 .context("failed to create java lib")?;
407 let dir = PathBuf::from(get_required_arg::<String>(sub_matches, "out")?);
408 generated_files
409 .iter()
410 .try_for_each(|file| write_output_file_realtive_to_dir(&dir, file))?;
411 }
412 Some(("create-cpp-lib", sub_matches)) => {
413 let cache = open_single_file(sub_matches, "cache")?;
414 let mode = get_required_arg::<CodegenMode>(sub_matches, "mode")?;
415 let generated_files =
416 commands::create_cpp_lib(cache, *mode).context("failed to create cpp lib")?;
417 let dir = PathBuf::from(get_required_arg::<String>(sub_matches, "out")?);
418 generated_files
419 .iter()
420 .try_for_each(|file| write_output_file_realtive_to_dir(&dir, file))?;
421 }
422 Some(("create-rust-lib", sub_matches)) => {
423 let cache = open_single_file(sub_matches, "cache")?;
424 let mode = get_required_arg::<CodegenMode>(sub_matches, "mode")?;
425 let generated_file =
426 commands::create_rust_lib(cache, *mode).context("failed to create rust lib")?;
427 let dir = PathBuf::from(get_required_arg::<String>(sub_matches, "out")?);
428 write_output_file_realtive_to_dir(&dir, &generated_file)?;
429 }
430 Some(("create-device-config-defaults", sub_matches)) => {
431 let cache = open_single_file(sub_matches, "cache")?;
432 let output = commands::create_device_config_defaults(cache)
433 .context("failed to create device config defaults")?;
434 let path = get_required_arg::<String>(sub_matches, "out")?;
435 write_output_to_file_or_stdout(path, &output)?;
436 }
437 Some(("create-device-config-sysprops", sub_matches)) => {
438 let cache = open_single_file(sub_matches, "cache")?;
439 let output = commands::create_device_config_sysprops(cache)
440 .context("failed to create device config sysprops")?;
441 let path = get_required_arg::<String>(sub_matches, "out")?;
442 write_output_to_file_or_stdout(path, &output)?;
443 }
444 Some(("dump-cache", sub_matches)) => {
445 let input = open_zero_or_more_files(sub_matches, "cache")?;
446 let format = get_required_arg::<DumpFormat>(sub_matches, "format")
447 .context("failed to dump previously parsed flags")?;
448 let filters = sub_matches
449 .get_many::<String>("filter")
450 .unwrap_or_default()
451 .map(String::as_ref)
452 .collect::<Vec<_>>();
453 let dedup = get_required_arg::<bool>(sub_matches, "dedup")?;
454 let output = commands::dump_parsed_flags(input, format.clone(), &filters, *dedup)?;
455 let path = get_required_arg::<String>(sub_matches, "out")?;
456 write_output_to_file_or_stdout(path, &output)?;
457 }
458 Some(("create-storage", sub_matches)) => {
459 let version =
460 get_optional_arg::<u32>(sub_matches, "version").unwrap_or(&DEFAULT_FILE_VERSION);
461 if *version > MAX_SUPPORTED_FILE_VERSION {
462 bail!("Invalid version selected ({})", version);
463 }
464 let file = get_required_arg::<StorageFileType>(sub_matches, "file")
465 .context("Invalid storage file selection")?;
466 let cache = open_zero_or_more_files(sub_matches, "cache")?;
467 let container = get_required_arg::<String>(sub_matches, "container")?;
468 let path = get_required_arg::<String>(sub_matches, "out")?;
469
470 let output = commands::create_storage(cache, container, file, *version)
471 .context("failed to create storage files")?;
472 write_output_to_file_or_stdout(path, &output)?;
473 }
474 _ => unreachable!(),
475 }
476 Ok(())
477 }
478