blob: 78639889366a347fbd27a1db8551905d0256d3fa [file] [log] [blame]
#![feature(try_trait)]
// Copyright 2019 Google LLC
//
// 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.
// See LICENSE for licensing information.
//
// This build.rs script tries to generate low-level rust bindings for the current ICU library.
// Please refer to README.md for instructions on how to build the library for
// your use.
use {
anyhow::{Context, Result},
std::env,
std::fs::File,
std::io::Write,
std::path::Path,
std::process,
};
/// A `Command` that also knows its name.
struct Command {
name: String,
rep: process::Command,
}
impl Command {
/// Creates a new command to run, with the executable `name`.
pub fn new(name: &'static str) -> Self {
let rep = process::Command::new(&name);
let name = String::from(name);
Command { name, rep }
}
/// Runs this command with `args` as arguments.
pub fn run(&mut self, args: &[&str]) -> Result<String> {
self.rep.args(args);
let stdout = self.stdout()?;
Ok(String::from(&stdout).trim().to_string())
}
// Captures the stdout of the command.
fn stdout(&mut self) -> Result<String> {
let output = self
.rep
.output()
.with_context(|| format!("could not execute command: {}", self.name))?;
let result = String::from_utf8(output.stdout)
.with_context(|| format!("could not convert output to UTF8"))?;
Ok(result.trim().to_string())
}
}
/// A command representing an `icu-config` run. Use `ICUConfig::new()` to create.
struct ICUConfig {
rep: Command,
}
impl ICUConfig {
/// Creates a new ICUConfig.
fn new() -> Self {
ICUConfig {
rep: Command::new("icu-config"),
}
}
/// Runs `icu-config --prefix`.
fn prefix(&mut self) -> Result<String> {
self.rep
.run(&["--prefix"])
.with_context(|| format!("could not get config prefix"))
}
/// Runs `icu-config --libdir`.
fn libdir(&mut self) -> Result<String> {
self.rep
.run(&["--libdir"])
.with_context(|| format!("could not get library directory"))
}
/// Runs `icu-config --ldflags`.
fn ldflags(&mut self) -> Result<String> {
// Replacements needed because of https://github.com/rust-lang/cargo/issues/7217
let result = self
.rep
.run(&["--ldflags"])
.with_context(|| format!("could not get the ld flags"))?;
Ok(result.replace("-L", "-L ").replace("-l", "-l "))
}
/// Runs `icu-config --cppflags`.
fn cppflags(&mut self) -> Result<String> {
self.rep
.run(&["--cppflags"])
.with_context(|| format!("while getting the cpp flags"))
}
/// Runs `icu-config --version`. Returns a string like `64.2`.
fn version(&mut self) -> Result<String> {
self.rep
.run(&["--version"])
.with_context(|| format!("while getting ICU version; is icu-config in $PATH?"))
}
fn install_dir(&mut self) -> Result<String> {
self.prefix()
}
/// Returns the config major number. For example, will return "64" for
/// version "64.2"
fn version_major() -> Result<String> {
let version = ICUConfig::new().version()?;
let components = version.split(".");
let last = components
.take(1)
.last()
.with_context(|| format!("could not parse version number: {}", version))?;
Ok(last.to_string())
}
}
/// Returns true if the ICU library was compiled with renaming enabled.
fn has_renaming() -> Result<bool> {
let cpp_flags = ICUConfig::new().cppflags()?;
let found = cpp_flags.find("-DU_DISABLE_RENAMING=1");
println!("flags: {}", cpp_flags);
Ok(found.is_none())
}
fn rustfmt_cmd() -> Command {
Command::new("rustfmt")
}
fn rustfmt_version() -> Result<String> {
rustfmt_cmd()
.run(&["--version"])
.with_context(|| format!("while getting rustfmt version; is rustfmt in $PATH?"))
}
fn bindgen_cmd() -> Command {
Command::new("bindgen")
}
fn bindgen_version() -> Result<String> {
bindgen_cmd()
.run(&["--version"])
.with_context(|| format!("while getting bindgen version; is bindgen in $PATH?"))
}
/// Generates an additional include file which contains the linker directives.
/// This is done because cargo does not allow the rustc link directives to be
/// anything other than `-L` and `-l`.
fn generate_linker_file(out_dir_path: &Path, lib_dir: &str, lib_names: &Vec<&str>) {
let file_path = out_dir_path.join("link.rs");
let mut linker_file = File::create(&file_path).unwrap();
let mut content: Vec<String> = vec![];
content.push(String::from(r#"extern "C" {}"#));
linker_file
.write_all(&content.join("\n").into_bytes())
.expect("successful write into linker file");
}
/// Generates a wrapper header that includes all headers of interest for binding.
///
/// This is the recommended way to bind complex libraries at the moment. Returns
/// the full path of the generated wrapper header file.
fn generate_wrapper_header(
out_dir_path: &Path,
bindgen_source_modules: &Vec<&str>,
include_path: &Path,
) -> String {
let wrapper_path = out_dir_path.join("wrapper.h");
let mut wrapper_file = File::create(&wrapper_path).unwrap();
wrapper_file
.write_all(b"/* Generated file, do not edit. */ \n")
.unwrap();
let includes = bindgen_source_modules
.iter()
.map(|f| {
let file_path = include_path.join(format!("{}.h", f));
let file_path_str = format!("#include \"{}\"\n", file_path.to_str().unwrap());
println!("include-file: '{}'", file_path.to_str().unwrap());
file_path_str
})
.collect::<String>();
wrapper_file.write_all(&includes.into_bytes()).unwrap();
String::from(wrapper_path.to_str().unwrap())
}
fn commaify(s: &Vec<&str>) -> String {
format!("{}", s.join("|"))
}
fn run_bindgen(header_file: &str, out_dir_path: &Path) -> Result<()> {
let whitelist_types_regexes = commaify(&vec![
"UAcceptResult",
"UBool",
"UCalendar.*",
"UChar.*",
"UData.*",
"UDate",
"UDateFormat.*",
"UEnumeration.*",
"UErrorCode",
"UMessageFormat",
"UParseError",
"UText.*",
]);
let whitelist_functions_regexes = commaify(&vec![
"u_.*", "ucal_.*", "udata_*", "udat_.*", "uenum_.*", "uloc_.*", "utext_.*", "umsg_.*",
]);
let opaque_types_regexes = commaify(&vec![]);
// Common arguments for all bindgen invocations.
let bindgen_generate_args = vec![
"--default-enum-style=rust",
"--no-doc-comments",
"--with-derive-default",
"--with-derive-hash",
"--with-derive-partialord",
"--with-derive-partialeq",
"--whitelist-type",
&whitelist_types_regexes,
"--whitelist-function",
&whitelist_functions_regexes,
"--opaque-type",
&opaque_types_regexes,
];
let output_file_path = out_dir_path.join("lib.rs");
let output_file = output_file_path.to_str().unwrap();
let ld_flags = ICUConfig::new().ldflags()?;
let cpp_flags = ICUConfig::new().cppflags()?;
let mut file_args = vec![
"-o",
&output_file,
&header_file,
"--",
&ld_flags,
&cpp_flags,
];
if !has_renaming()? {
file_args.push("-DU_DISABLE_RENAMING=1");
}
let all_args = [&bindgen_generate_args[..], &file_args[..]].concat();
println!("bindgen-cmdline: {:?}", all_args);
process::Command::new("bindgen")
.args(&all_args)
.spawn()
.with_context(|| format!("while running bindgen"))?;
Ok(())
}
// Generates the library renaming macro: this allows us to use renamed function
// names in the resulting low-level bindings library.
fn run_renamegen(out_dir_path: &Path) -> Result<()> {
let output_file_path = out_dir_path.join("macros.rs");
let mut macro_file = File::create(&output_file_path)
.with_context(|| format!("while opening {:?}", output_file_path))?;
if has_renaming()? {
println!("renaming: true");
// The library names have been renamed, need to generate a macro that
// converts the call of `foo()` into `foo_64()`.
let icu_major_version = ICUConfig::version_major()?;
let to_write = format!(
r#"
// Macros for changing function names.
// Automatically generated by build.rs.
extern crate paste;
// This library was build with version renaming, so rewrite every function name
// with its name with version number appended.
// The macro below will rename a symbol `foo::bar` to `foo::bar_{0}` (where "{0}")
// may be some other number depending on the ICU library in use.
#[cfg(feature="renaming")]
#[macro_export]
macro_rules! versioned_function {{
($i:ident) => {{
paste::expr! {{
[< $i _{0} >]
}}
}}
}}
// This allows the user to override the renaming configuration detected from
// icu-config.
#[cfg(not(feature="renaming"))]
#[macro_export]
macro_rules! versioned_function {{
($func_name:path) => {{
$func_name
}}
}}
"#,
icu_major_version
);
macro_file
.write_all(&to_write.into_bytes())
.with_context(|| format!("while writing macros.rs with renaming"))
} else {
// The library names have not been renamed, generating an empty macro
println!("renaming: false");
macro_file
.write_all(
&r#"
// Macros for changing function names.
// Automatically generated by build.rs.
// There was no renaming in this one, so just short-circuit this macro.
#[macro_export]
macro_rules! versioned_function {{
($func_name:path) => {{
$func_name
}}
}}
"#
.to_string()
.into_bytes(),
)
.with_context(|| format!("while writing macros.rs without renaming"))
}
}
/// Copies the featuers set in `Cargo.toml` into the build script. Not sure
/// why, but the features seem *ignored* when `build.rs` is used.
fn copy_features() -> Result<()> {
if let Some(_) = env::var_os("CARGO_FEATURE_RENAMING") {
println!("cargo:rustc-cfg=features=\"renaming\"");
}
if let Some(_) = env::var_os("CARGO_FEATURE_BINDGEN") {
println!("cargo:rustc-cfg=features=\"bindgen\"");
}
if let Some(_) = env::var_os("CARGO_FEATURE_ICU_CONFIG") {
println!("cargo:rustc-cfg=features=\"icu_config\"");
}
if let Some(_) = env::var_os("CARGO_FEATURE_ICU_VERSION_IN_ENV") {
println!("cargo:rustc-cfg=features=\"icu_version_in_env\"");
}
Ok(())
}
fn icu_config_autodetect() -> Result<()> {
println!("rustfmt: {}", rustfmt_version()?);
println!("icu-config: {}", ICUConfig::new().version()?);
println!("icu-config-cpp-flags: {}", ICUConfig::new().cppflags()?);
println!("icu-config-has-renaming: {}", has_renaming()?);
println!("bindgen: {}", bindgen_version()?);
// The path to the directory where cargo will add the output artifacts.
let out_dir = env::var("OUT_DIR").unwrap();
let out_dir_path = Path::new(&out_dir);
// The path where all unicode headers can be found.
let include_dir_path = Path::new(&ICUConfig::new().prefix()?)
.join("include")
.join("unicode");
let lib_dir = ICUConfig::new().libdir()?;
generate_linker_file(out_dir_path, &lib_dir, &vec!["icui18n", "icuuc", "icudata"]);
// The modules for which bindings will be generated. Add more if you need
// them. The list should be topologicaly sorted based on the inclusion
// relationship between the respective headers.
// Any of these will fail if the required binaries are not present in $PATH.
let bindgen_source_modules: Vec<&str> = vec![
"ucal", "udat", "udata", "uenum", "ustring", "utext", "uclean", "umsg",
];
let header_file =
generate_wrapper_header(&out_dir_path, &bindgen_source_modules, &include_dir_path);
run_bindgen(&header_file, out_dir_path).with_context(|| format!("while running bindgen"))?;
run_renamegen(out_dir_path).with_context(|| format!("while running renamegen"))?;
println!("cargo:install-dir={}", ICUConfig::new().install_dir()?);
println!("cargo:rustc-link-search=native={}", lib_dir);
println!("cargo:rustc-flags={}", ICUConfig::new().ldflags()?);
Ok(())
}
fn main() -> Result<()> {
std::env::set_var("RUST_BACKTRACE", "full");
copy_features()?;
if let None = env::var_os("CARGO_FEATURE_ICU_CONFIG") {
return Ok(());
}
icu_config_autodetect()?;
println!("done:true");
Ok(())
}