blob: f85247a61dc17fdb8b3711736ec8c0e6691262e8 [file] [log] [blame]
// Copyright 2018 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use std::collections::BTreeMap;
use std::env;
use std::fmt::Write as FmtWrite;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
/// Command that uses clang to parse a C++ file into its AST and outputs it as JSON. This is used
/// with the below jq script.
const CLANG_ARGS: &str = "-Xclang -ast-dump=json -fsyntax-only";
/// Script that recursively finds all objects with a `kind` field equal to `VarDecl` and extracts
/// the inner string literal. The output will be pairs of lines with the name followed by the value.
/// This is intended to be used with clangs' JSON formatted AST dump of a header file with string
/// constants.
const JQ_SCRIPT: &str = r#"'.. | select(.kind?=="VarDecl") | .name, .inner[0].value'"#;
/// Runs a shell command with `sh` and returns the stdout.
fn shell_helper(cmd: &str) -> String {
let child = Command::new("sh")
.expect("Failed to start shell process");
let output = child
.expect("Failed to wait on shell process");
if output.status.success() {
String::from_utf8(output.stdout).expect("Failed to convert shell process output to String")
} else {
panic!("Failed shell command: {}", cmd);
/// Converts a camel case a string such as `aVariableName` to upper snake case such as
fn upper_snake_case(input: &str) -> String {
let mut output = String::new();
for c in input.chars() {
// Assumes that uppercase symbols indicate word breaks. Sort of gets confused by capitalized
// acronyms.
if c.is_ascii_uppercase() && !output.is_empty() {
/// Used to declare constant path information about system_api style APIs to parse and generate Rust
/// bindings for.
struct SystemApiDbus {
path: &'static str,
proto: Option<&'static str>,
impl SystemApiDbus {
/// Declare a full D-Bus api with .proto and dbus-constants.h file.
const fn new(path: &'static str, proto: &'static str) -> SystemApiDbus {
SystemApiDbus {
proto: Some(proto),
/// Declare a full D-Bus api with only dbus-constants.h file under the given path. A path may
/// also contain the relative file name if dbus-constants.h isn't the appropriate constants
/// file.
const fn constants(path: &'static str) -> SystemApiDbus {
SystemApiDbus { path, proto: None }
/// Gets the include path in order to parse the .proto file for this API.
fn get_include_path(&self, proto_root: &Path) -> impl AsRef<Path> {
/// Gets the path to the .proto file for this API, if there is one.
fn get_input_path(&self, proto_root: &Path) -> Option<PathBuf> {
let proto = self.proto?;
/// Parses the string constants in dbus-constants.h or the appropriate alternative header if
/// appropriate. The given `constants` will be appended to with the name and value of each
/// constant, converted to correct Rust style. If there are duplicate constant names, they will
/// be overwritten.
fn parse_constants(&self, proto_root: &Path, constants: &mut BTreeMap<String, String>) {
let mut constants_path = proto_root.join(self.path);
if !constants_path.is_file() {
if !constants_path.is_file() {
panic!("Failed to find header at {}", self.path);
let clang = match env::var("CBUILD").ok() {
Some(cbuild) => format!("{}-clang++", cbuild),
None => "clang++".to_owned(),
let shell_parse_cmd = format!(
r#"set -o pipefail && {} {} "{}" | jq -e -r {}"#,
let parse_output = shell_helper(&shell_parse_cmd);
let mut lines = parse_output.lines();
loop {
if let (Some(var_name), Some(var_literal)) = (, {
if var_name.starts_with('k') {
let name = upper_snake_case(var_name.trim_matches('k'));
let value = var_literal.trim_matches('"').to_owned();
constants.insert(name, value);
} else {
/// A list of system_api style APIs to generate Rust bindings for.
const APIS: &[SystemApiDbus] = &[
// service_constants.h is the only header file needed that isn't called dbus_constants.h.
SystemApiDbus::new("dbus/vm_concierge", "concierge_service.proto"),
SystemApiDbus::new("dbus/vm_cicerone", "cicerone_service.proto"),
SystemApiDbus::new("dbus/dlcservice", "dlcservice.proto"),
SystemApiDbus::new("dbus/seneschal", "seneschal_service.proto"),
SystemApiDbus::new("dbus/vm_plugin_dispatcher", "vm_plugin_dispatcher.proto"),
SystemApiDbus::new("dbus/vm_launch", "launch.proto"),
fn main() {
let system_api_root = match env::var("SYSROOT") {
Ok(path) => PathBuf::from(path).join("usr/include/chromeos"),
// Make this work when typing "cargo build" in platform2/vm_tools/crostini_client
Err(_) => PathBuf::from("../../system_api"),
let mut input_paths = Vec::new();
let mut generator = protoc_rust::Codegen::new();
let mut constants = Default::default();
for api in APIS {
// Some APIs have no .proto file that needs parsing.
if let Some(input_path) = api.get_input_path(&system_api_root) {
api.parse_constants(&system_api_root, &mut constants);
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
// Build up a string of code which gets included as the module.
let mut proto_include_code = String::new();
// Collects all the Rust source files outputted by protoc as a series of modules.
for input_file in input_paths.iter() {
let stem = input_file.file_stem().unwrap().to_str().unwrap();
let mod_path = out_path.join(format!("{}.rs", stem));
&mut proto_include_code,
"#[path = \"{}\"]",
writeln!(&mut proto_include_code, "pub mod {};", stem).unwrap();
// Also put all the collected string constants from `parse_constants` into the included module.
for (name, value) in constants {
&mut proto_include_code,
"pub const {}: &str = r###\"{}\"###;", // Uses Rust raw strings for safe inclusion.
name, value
// The file gets included directly by
let mut mod_out = fs::File::create(out_path.join("")).unwrap();
writeln!(mod_out, "pub mod system_api {{\n{}}}", proto_include_code).unwrap();