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 anyhow::{anyhow, bail, Context, Result};
20 use clap::{builder::ArgAction, builder::EnumValueParser, Arg, ArgMatches, Command};
21 use core::any::Any;
22 use std::fs;
23 use std::io;
24 use std::io::Write;
25 use std::path::{Path, PathBuf};
26 
27 mod codegen;
28 mod commands;
29 mod dump;
30 mod storage;
31 
32 use aconfig_storage_file::StorageFileType;
33 use codegen::CodegenMode;
34 use dump::DumpFormat;
35 
36 #[cfg(test)]
37 mod test;
38 
39 use commands::{Input, OutputFile};
40 
41 const HELP_DUMP_FILTER: &str = r#"
42 Limit which flags to output. If multiple --filter arguments are provided, the output will be
43 limited to flags that match any of the filters.
44 "#;
45 
cli() -> Command46 fn cli() -> Command {
47     Command::new("aconfig")
48         .subcommand_required(true)
49         .subcommand(
50             Command::new("create-cache")
51                 .arg(Arg::new("package").long("package").required(true))
52                 // TODO(b/312769710): Make this argument required.
53                 .arg(Arg::new("container").long("container"))
54                 .arg(Arg::new("declarations").long("declarations").action(ArgAction::Append))
55                 .arg(Arg::new("values").long("values").action(ArgAction::Append))
56                 .arg(
57                     Arg::new("default-permission")
58                         .long("default-permission")
59                         .value_parser(aconfig_protos::flag_permission::parse_from_str)
60                         .default_value(aconfig_protos::flag_permission::to_string(
61                             &commands::DEFAULT_FLAG_PERMISSION,
62                         )),
63                 )
64                 .arg(Arg::new("cache").long("cache").required(true)),
65         )
66         .subcommand(
67             Command::new("create-java-lib")
68                 .arg(Arg::new("cache").long("cache").required(true))
69                 .arg(Arg::new("out").long("out").required(true))
70                 .arg(
71                     Arg::new("mode")
72                         .long("mode")
73                         .value_parser(EnumValueParser::<CodegenMode>::new())
74                         .default_value("production"),
75                 ),
76         )
77         .subcommand(
78             Command::new("create-cpp-lib")
79                 .arg(Arg::new("cache").long("cache").required(true))
80                 .arg(Arg::new("out").long("out").required(true))
81                 .arg(
82                     Arg::new("mode")
83                         .long("mode")
84                         .value_parser(EnumValueParser::<CodegenMode>::new())
85                         .default_value("production"),
86                 )
87                 .arg(
88                     Arg::new("allow-instrumentation")
89                         .long("allow-instrumentation")
90                         .value_parser(clap::value_parser!(bool))
91                         .default_value("false"),
92                 ),
93         )
94         .subcommand(
95             Command::new("create-rust-lib")
96                 .arg(Arg::new("cache").long("cache").required(true))
97                 .arg(Arg::new("out").long("out").required(true))
98                 .arg(
99                     Arg::new("allow-instrumentation")
100                         .long("allow-instrumentation")
101                         .value_parser(clap::value_parser!(bool))
102                         .default_value("false"),
103                 )
104                 .arg(
105                     Arg::new("mode")
106                         .long("mode")
107                         .value_parser(EnumValueParser::<CodegenMode>::new())
108                         .default_value("production"),
109                 ),
110         )
111         .subcommand(
112             Command::new("create-device-config-defaults")
113                 .arg(Arg::new("cache").long("cache").action(ArgAction::Append).required(true))
114                 .arg(Arg::new("out").long("out").default_value("-")),
115         )
116         .subcommand(
117             Command::new("create-device-config-sysprops")
118                 .arg(Arg::new("cache").long("cache").action(ArgAction::Append).required(true))
119                 .arg(Arg::new("out").long("out").default_value("-")),
120         )
121         .subcommand(
122             Command::new("dump-cache")
123                 .alias("dump")
124                 .arg(Arg::new("cache").long("cache").action(ArgAction::Append))
125                 .arg(
126                     Arg::new("format")
127                         .long("format")
128                         .value_parser(|s: &str| DumpFormat::try_from(s))
129                         .default_value(
130                             "{fully_qualified_name} [{container}]: {permission} + {state}",
131                         ),
132                 )
133                 .arg(
134                     Arg::new("filter")
135                         .long("filter")
136                         .action(ArgAction::Append)
137                         .help(HELP_DUMP_FILTER.trim()),
138                 )
139                 .arg(Arg::new("dedup").long("dedup").num_args(0).action(ArgAction::SetTrue))
140                 .arg(Arg::new("out").long("out").default_value("-")),
141         )
142         .subcommand(
143             Command::new("create-storage")
144                 .arg(
145                     Arg::new("container")
146                         .long("container")
147                         .required(true)
148                         .help("The target container for the generated storage file."),
149                 )
150                 .arg(
151                     Arg::new("file")
152                         .long("file")
153                         .value_parser(|s: &str| StorageFileType::try_from(s)),
154                 )
155                 .arg(Arg::new("cache").long("cache").action(ArgAction::Append).required(true))
156                 .arg(Arg::new("out").long("out").required(true)),
157         )
158 }
159 
get_required_arg<'a, T>(matches: &'a ArgMatches, arg_name: &str) -> Result<&'a T> where T: Any + Clone + Send + Sync + 'static,160 fn get_required_arg<'a, T>(matches: &'a ArgMatches, arg_name: &str) -> Result<&'a T>
161 where
162     T: Any + Clone + Send + Sync + 'static,
163 {
164     matches
165         .get_one::<T>(arg_name)
166         .ok_or(anyhow!("internal error: required argument '{}' not found", arg_name))
167 }
168 
get_optional_arg<'a, T>(matches: &'a ArgMatches, arg_name: &str) -> Option<&'a T> where T: Any + Clone + Send + Sync + 'static,169 fn get_optional_arg<'a, T>(matches: &'a ArgMatches, arg_name: &str) -> Option<&'a T>
170 where
171     T: Any + Clone + Send + Sync + 'static,
172 {
173     matches.get_one::<T>(arg_name)
174 }
175 
open_zero_or_more_files(matches: &ArgMatches, arg_name: &str) -> Result<Vec<Input>>176 fn open_zero_or_more_files(matches: &ArgMatches, arg_name: &str) -> Result<Vec<Input>> {
177     let mut opened_files = vec![];
178     for path in matches.get_many::<String>(arg_name).unwrap_or_default() {
179         let file = Box::new(fs::File::open(path)?);
180         opened_files.push(Input { source: path.to_string(), reader: file });
181     }
182     Ok(opened_files)
183 }
184 
open_single_file(matches: &ArgMatches, arg_name: &str) -> Result<Input>185 fn open_single_file(matches: &ArgMatches, arg_name: &str) -> Result<Input> {
186     let Some(path) = matches.get_one::<String>(arg_name) else {
187         bail!("missing argument {}", arg_name);
188     };
189     let file = Box::new(fs::File::open(path)?);
190     Ok(Input { source: path.to_string(), reader: file })
191 }
192 
write_output_file_realtive_to_dir(root: &Path, output_file: &OutputFile) -> Result<()>193 fn write_output_file_realtive_to_dir(root: &Path, output_file: &OutputFile) -> Result<()> {
194     let path = root.join(&output_file.path);
195     let parent = path
196         .parent()
197         .ok_or(anyhow!("unable to locate parent of output file {}", path.display()))?;
198     fs::create_dir_all(parent)
199         .with_context(|| format!("failed to create directory {}", parent.display()))?;
200     let mut file =
201         fs::File::create(&path).with_context(|| format!("failed to open {}", path.display()))?;
202     file.write_all(&output_file.contents)
203         .with_context(|| format!("failed to write to {}", path.display()))?;
204     Ok(())
205 }
206 
write_output_to_file_or_stdout(path: &str, data: &[u8]) -> Result<()>207 fn write_output_to_file_or_stdout(path: &str, data: &[u8]) -> Result<()> {
208     if path == "-" {
209         io::stdout().write_all(data).context("failed to write to stdout")?;
210     } else {
211         fs::File::create(path)
212             .with_context(|| format!("failed to open {}", path))?
213             .write_all(data)
214             .with_context(|| format!("failed to write to {}", path))?;
215     }
216     Ok(())
217 }
218 
main() -> Result<()>219 fn main() -> Result<()> {
220     let matches = cli().get_matches();
221     match matches.subcommand() {
222         Some(("create-cache", sub_matches)) => {
223             let package = get_required_arg::<String>(sub_matches, "package")?;
224             let container =
225                 get_optional_arg::<String>(sub_matches, "container").map(|c| c.as_str());
226             let declarations = open_zero_or_more_files(sub_matches, "declarations")?;
227             let values = open_zero_or_more_files(sub_matches, "values")?;
228             let default_permission = get_required_arg::<aconfig_protos::ProtoFlagPermission>(
229                 sub_matches,
230                 "default-permission",
231             )?;
232             let output = commands::parse_flags(
233                 package,
234                 container,
235                 declarations,
236                 values,
237                 *default_permission,
238             )
239             .context("failed to create cache")?;
240             let path = get_required_arg::<String>(sub_matches, "cache")?;
241             write_output_to_file_or_stdout(path, &output)?;
242         }
243         Some(("create-java-lib", sub_matches)) => {
244             let cache = open_single_file(sub_matches, "cache")?;
245             let mode = get_required_arg::<CodegenMode>(sub_matches, "mode")?;
246             let generated_files =
247                 commands::create_java_lib(cache, *mode).context("failed to create java lib")?;
248             let dir = PathBuf::from(get_required_arg::<String>(sub_matches, "out")?);
249             generated_files
250                 .iter()
251                 .try_for_each(|file| write_output_file_realtive_to_dir(&dir, file))?;
252         }
253         Some(("create-cpp-lib", sub_matches)) => {
254             let cache = open_single_file(sub_matches, "cache")?;
255             let mode = get_required_arg::<CodegenMode>(sub_matches, "mode")?;
256             let allow_instrumentation =
257                 get_required_arg::<bool>(sub_matches, "allow-instrumentation")?;
258             let generated_files = commands::create_cpp_lib(cache, *mode, *allow_instrumentation)
259                 .context("failed to create cpp lib")?;
260             let dir = PathBuf::from(get_required_arg::<String>(sub_matches, "out")?);
261             generated_files
262                 .iter()
263                 .try_for_each(|file| write_output_file_realtive_to_dir(&dir, file))?;
264         }
265         Some(("create-rust-lib", sub_matches)) => {
266             let cache = open_single_file(sub_matches, "cache")?;
267             let mode = get_required_arg::<CodegenMode>(sub_matches, "mode")?;
268             let allow_instrumentation =
269                 get_required_arg::<bool>(sub_matches, "allow-instrumentation")?;
270             let generated_file = commands::create_rust_lib(cache, *mode, *allow_instrumentation)
271                 .context("failed to create rust lib")?;
272             let dir = PathBuf::from(get_required_arg::<String>(sub_matches, "out")?);
273             write_output_file_realtive_to_dir(&dir, &generated_file)?;
274         }
275         Some(("create-device-config-defaults", sub_matches)) => {
276             let cache = open_single_file(sub_matches, "cache")?;
277             let output = commands::create_device_config_defaults(cache)
278                 .context("failed to create device config defaults")?;
279             let path = get_required_arg::<String>(sub_matches, "out")?;
280             write_output_to_file_or_stdout(path, &output)?;
281         }
282         Some(("create-device-config-sysprops", sub_matches)) => {
283             let cache = open_single_file(sub_matches, "cache")?;
284             let output = commands::create_device_config_sysprops(cache)
285                 .context("failed to create device config sysprops")?;
286             let path = get_required_arg::<String>(sub_matches, "out")?;
287             write_output_to_file_or_stdout(path, &output)?;
288         }
289         Some(("dump-cache", sub_matches)) => {
290             let input = open_zero_or_more_files(sub_matches, "cache")?;
291             let format = get_required_arg::<DumpFormat>(sub_matches, "format")
292                 .context("failed to dump previously parsed flags")?;
293             let filters = sub_matches
294                 .get_many::<String>("filter")
295                 .unwrap_or_default()
296                 .map(String::as_ref)
297                 .collect::<Vec<_>>();
298             let dedup = get_required_arg::<bool>(sub_matches, "dedup")?;
299             let output = commands::dump_parsed_flags(input, format.clone(), &filters, *dedup)?;
300             let path = get_required_arg::<String>(sub_matches, "out")?;
301             write_output_to_file_or_stdout(path, &output)?;
302         }
303         Some(("create-storage", sub_matches)) => {
304             let file = get_required_arg::<StorageFileType>(sub_matches, "file")
305                 .context("Invalid storage file selection")?;
306             let cache = open_zero_or_more_files(sub_matches, "cache")?;
307             let container = get_required_arg::<String>(sub_matches, "container")?;
308             let path = get_required_arg::<String>(sub_matches, "out")?;
309             let output = commands::create_storage(cache, container, file)
310                 .context("failed to create storage files")?;
311             write_output_to_file_or_stdout(path, &output)?;
312         }
313         _ => unreachable!(),
314     }
315     Ok(())
316 }
317