blob: 5c46f11c47d4aff8a7bb72d169dd45b37cae9a79 [file] [log] [blame]
// Copyright 2019 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.
// Provides tools for generating a Rust library with D-Bus bindings. The generated bindings are
// included in the published crate since the source XML files are only available from the original
// path or the ebuild. See the README.md for usage.
use std::env;
use std::ffi::OsStr;
use std::fmt::{self, Debug, Display};
use std::fs::{create_dir_all, remove_dir_all, File};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
pub const CROSSROADS_SERVER_OPTS: &[&str] = &["-s", "--crossroads"];
pub const TREE_SERVER_OPTS: &[&str] = &["-s", "-m", "Fn", "-a", "RefClosure"];
const DEFAULT_BINDINGS_DIR: &str = "src/bindings";
const DEFAULT_CLIENT_OPTS: &[&str] = &["-s", "-m", "None"];
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum BindingsType<'a> {
Client(Option<&'a [&'a str]>),
Server(&'a [&'a str]),
Both {
client_opts: Option<&'a [&'a str]>,
server_opts: &'a [&'a str],
},
}
#[derive(Debug)]
pub enum Error {
CleanupFailed(io::Error),
CommandFailed(Option<i32>),
Create(io::Error),
CreateDirAll(io::Error),
Generate(String),
MissingCodegen(which::Error),
Open(io::Error),
SourceDoesntExist(PathBuf),
Spawn(io::Error),
Wait(io::Error),
Write(io::Error),
}
impl Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::Error::*;
match self {
CleanupFailed(e) => write!(f, "failed to cleanup old bindings: {}", e),
CommandFailed(o) => match o {
Some(c) => write!(f, "command failed with code {}", c),
None => write!(f, "command failed"),
},
Create(e) => write!(f, "create failed: {}", e),
CreateDirAll(e) => write!(f, "create_dir_all failed: {}", e),
Generate(s) => write!(f, "generate failed: {}", s),
MissingCodegen(e) => write!(f, "dbus-codegen-rust required, but not found: {:?}", e),
Open(e) => write!(f, "open failed: {}", e),
SourceDoesntExist(file) => match file.to_str() {
None => write!(f, "empty path"),
Some(s) => write!(f, "source file doesn't exist: {}", s),
},
Spawn(e) => write!(f, "spawn failed: {}", e),
Wait(e) => write!(f, "wait failed: {}", e),
Write(e) => write!(f, "write error: {}", e),
}
}
}
/// The result of an operation in this crate.
pub type Result<T> = std::result::Result<T, Error>;
// Include generated bindings.
fn include_module(mod_out: &mut File, module: &str) -> Result<()> {
writeln!(
mod_out,
" pub mod {module};\n pub use {module}::*;",
module = module
)
.map_err(Error::Write)
}
// sub_modules: &[(<module name>, <relative path to source xml>), ...]
pub fn generate_module(
source_dir: &Path,
sub_modules: &[(&str, &str, BindingsType)],
) -> Result<()> {
let bindings_dir = PathBuf::from(DEFAULT_BINDINGS_DIR);
let client_dir = bindings_dir.join("client");
let server_dir = bindings_dir.join("server");
let mut errors: String = String::new();
// Check that dbus-bindgen-rust is available.
let codegen = get_dbus_codgen()?;
// If the bindings exist and it is a release build exit early.
if bindings_dir.exists() {
if env::var("PROFILE").map_or(false, |v| &v == "release") {
return Ok(());
}
remove_dir_all(&bindings_dir).map_err(Error::CleanupFailed)?;
}
create_dir_all(&client_dir).map_err(Error::CreateDirAll)?;
create_dir_all(&server_dir).map_err(Error::CreateDirAll)?;
// Write header of include file.
let mut mod_out =
File::create(bindings_dir.join("include_modules.rs")).map_err(Error::Create)?;
writeln!(
mod_out,
r#"// Do not edit. This is generated by system_api/build.rs.
#[allow(unused_imports)]
#[allow(clippy::all)]
pub mod client {{"#
)
.map_err(Error::Write)?;
for (module, source, bindings_type) in sub_modules {
let opts = match bindings_type {
BindingsType::Client(client_opts) | BindingsType::Both { client_opts, .. } => {
client_opts
}
BindingsType::Server(_) => {
continue;
}
}
.unwrap_or(DEFAULT_CLIENT_OPTS);
// Generate bindings if they don't already exist.
let destination = client_dir.join(format!("{}.rs", module));
if !destination.exists() {
if let Err(err) =
generate_bindings(&codegen, &source_dir.join(source), &destination, opts)
{
errors.push_str(&format!(
"Failed to generate {:?} from {:?}: {}\n",
module, source, err
));
// Canonicalize will fail if the destination doesn't exist.
continue;
}
}
include_module(&mut mod_out, module)?;
}
writeln!(
mod_out,
r#"}}
#[allow(unused_imports)]
#[allow(clippy::all)]
pub mod server {{"#
)
.map_err(Error::Write)?;
for (module, source, bindings_type) in sub_modules {
let opts = *match bindings_type {
BindingsType::Server(server_opts) | BindingsType::Both { server_opts, .. } => {
server_opts
}
BindingsType::Client(_) => {
continue;
}
};
// Generate bindings if they don't already exist.
let destination = server_dir.join(format!("{}.rs", module));
if !destination.exists() {
if let Err(err) =
generate_bindings(&codegen, &source_dir.join(source), &destination, opts)
{
errors.push_str(&format!(
"Failed to generate {:?} from {:?}: {}\n",
module, source, err
));
// Canonicalize will fail if the destination doesn't exist.
continue;
}
}
include_module(&mut mod_out, module)?;
}
writeln!(mod_out, "}}").map_err(Error::Write)?;
if errors.is_empty() {
Ok(())
} else {
Err(Error::Generate(errors))
}
}
fn get_dbus_codgen() -> Result<PathBuf> {
let mut ret = which::which("dbus-codegen-rust").map_err(Error::MissingCodegen);
if ret.is_err() {
if let Some(dir) = std::env::var_os("HOME") {
let alternative = Path::new(&dir).join(".cargo/bin/dbus-codegen-rust");
if alternative.exists() {
ret = Ok(alternative)
}
}
}
ret
}
fn generate_bindings<A: AsRef<OsStr>, S: AsRef<OsStr>, I: IntoIterator<Item = S>>(
codegen: A,
source: &Path,
destination: &Path,
opts: I,
) -> Result<()> {
if !source.exists() {
return Err(Error::SourceDoesntExist(source.to_path_buf()));
}
let status = Command::new(codegen)
.args(opts)
.stdin(File::open(source).map_err(Error::Open)?)
.stdout(File::create(destination).map_err(Error::Create)?)
.spawn()
.map_err(Error::Spawn)?
.wait()
.map_err(Error::Wait)?;
if !status.success() {
return Err(Error::CommandFailed(status.code()));
}
Ok(())
}