diff options
Diffstat (limited to 'src/bindgen/cargo')
-rw-r--r-- | src/bindgen/cargo/cargo.rs | 252 | ||||
-rw-r--r-- | src/bindgen/cargo/cargo_expand.rs | 145 | ||||
-rw-r--r-- | src/bindgen/cargo/cargo_lock.rs | 51 | ||||
-rw-r--r-- | src/bindgen/cargo/cargo_metadata.rs | 253 | ||||
-rw-r--r-- | src/bindgen/cargo/cargo_toml.rs | 67 | ||||
-rw-r--r-- | src/bindgen/cargo/mod.rs | 12 |
6 files changed, 780 insertions, 0 deletions
diff --git a/src/bindgen/cargo/cargo.rs b/src/bindgen/cargo/cargo.rs new file mode 100644 index 0000000..69cf938 --- /dev/null +++ b/src/bindgen/cargo/cargo.rs @@ -0,0 +1,252 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::path::{Path, PathBuf}; + +use crate::bindgen::cargo::cargo_expand; +use crate::bindgen::cargo::cargo_lock::{self, Lock}; +pub(crate) use crate::bindgen::cargo::cargo_metadata::PackageRef; +use crate::bindgen::cargo::cargo_metadata::{self, Metadata}; +use crate::bindgen::cargo::cargo_toml; +use crate::bindgen::config::Profile; +use crate::bindgen::error::Error; +use crate::bindgen::ir::Cfg; + +/// Parse a dependency string used in Cargo.lock +fn parse_dep_string(dep_string: &str) -> (&str, Option<&str>) { + let split: Vec<&str> = dep_string.split_whitespace().collect(); + + (split[0], split.get(1).cloned()) +} + +/// A collection of metadata for a library from cargo. +#[derive(Clone, Debug)] +pub(crate) struct Cargo { + manifest_path: PathBuf, + binding_crate_name: String, + lock: Option<Lock>, + metadata: Metadata, + clean: bool, +} + +impl Cargo { + /// Gather metadata from cargo for a specific library and binding crate + /// name. If dependency finding isn't needed then Cargo.lock files don't + /// need to be parsed. + pub(crate) fn load( + crate_dir: &Path, + lock_file: Option<&str>, + binding_crate_name: Option<&str>, + use_cargo_lock: bool, + clean: bool, + only_target_dependencies: bool, + existing_metadata_file: Option<&Path>, + ) -> Result<Cargo, Error> { + let toml_path = crate_dir.join("Cargo.toml"); + let metadata = + cargo_metadata::metadata(&toml_path, existing_metadata_file, only_target_dependencies) + .map_err(|x| Error::CargoMetadata(toml_path.to_str().unwrap().to_owned(), x))?; + let lock_path = lock_file + .map(PathBuf::from) + .unwrap_or_else(|| Path::new(&metadata.workspace_root).join("Cargo.lock")); + + let lock = if use_cargo_lock { + match cargo_lock::lock(&lock_path) { + Ok(lock) => Some(lock), + Err(x) => { + warn!("Couldn't load lock file {:?}: {:?}", lock_path, x); + None + } + } + } else { + None + }; + + // Use the specified binding crate name or infer it from the manifest + let binding_crate_name = match binding_crate_name { + Some(s) => s.to_owned(), + None => { + let manifest = cargo_toml::manifest(&toml_path) + .map_err(|x| Error::CargoToml(toml_path.to_str().unwrap().to_owned(), x))?; + manifest.package.name + } + }; + + Ok(Cargo { + manifest_path: toml_path, + binding_crate_name, + lock, + metadata, + clean, + }) + } + + pub(crate) fn binding_crate_name(&self) -> &str { + &self.binding_crate_name + } + + pub(crate) fn binding_crate_ref(&self) -> PackageRef { + match self.find_pkg_ref(&self.binding_crate_name) { + Some(pkg_ref) => pkg_ref, + None => panic!( + "Unable to find {} for {:?}", + self.binding_crate_name, self.manifest_path + ), + } + } + + pub(crate) fn dependencies(&self, package: &PackageRef) -> Vec<(PackageRef, Option<Cfg>)> { + let lock = match self.lock { + Some(ref lock) => lock, + None => return vec![], + }; + + let mut dependencies = None; + + // Find the dependencies listing in the lockfile + if let Some(ref root) = lock.root { + // If the version is not on the lockfile then it shouldn't be + // ambiguous. + if root.name == package.name + && package + .version + .as_ref() + .map_or(true, |v| *v == root.version) + { + dependencies = root.dependencies.as_ref(); + } + } + if dependencies.is_none() { + if let Some(ref lock_packages) = lock.package { + for lock_package in lock_packages { + if lock_package.name == package.name + && package + .version + .as_ref() + .map_or(true, |v| *v == lock_package.version) + { + dependencies = lock_package.dependencies.as_ref(); + break; + } + } + } + } + if dependencies.is_none() { + return vec![]; + } + + dependencies + .unwrap() + .iter() + .map(|dep| { + let (dep_name, dep_version) = parse_dep_string(dep); + + // If a version was not specified find the only package with the name of the dependency + let dep_version = dep_version.or_else(|| { + let mut versions = self.metadata.packages.iter().filter_map(|package| { + if package.name_and_version.name != dep_name { + return None; + } + package.name_and_version.version.as_deref() + }); + + // If the iterator contains more items, meaning multiple versions of the same + // package are present, warn! amd abort. + let version = versions.next(); + if versions.next().is_none() { + version + } else { + warn!("when looking for a version for package {}, multiple versions where found", dep_name); + None + } + }); + + // Try to find the cfgs in the Cargo.toml + let cfg = self + .metadata + .packages + .get(package) + .and_then(|meta_package| meta_package.dependencies.get(dep_name)) + .and_then(Cfg::load_metadata); + + let package_ref = PackageRef { + name: dep_name.to_owned(), + version: dep_version.map(|v| v.to_owned()), + }; + + (package_ref, cfg) + }) + .collect() + } + + /// Finds the package reference in `cargo metadata` that has `package_name` + /// ignoring the version. + fn find_pkg_ref(&self, package_name: &str) -> Option<PackageRef> { + for package in &self.metadata.packages { + if package.name_and_version.name == package_name { + return Some(package.name_and_version.clone()); + } + } + None + } + + /// Finds the directory for a specified package reference. + #[allow(unused)] + pub(crate) fn find_crate_dir(&self, package: &PackageRef) -> Option<PathBuf> { + self.metadata + .packages + .get(package) + .and_then(|meta_package| { + Path::new(&meta_package.manifest_path) + .parent() + .map(|x| x.to_owned()) + }) + } + + /// Finds `src/lib.rs` for a specified package reference. + pub(crate) fn find_crate_src(&self, package: &PackageRef) -> Option<PathBuf> { + let kind_lib = String::from("lib"); + let kind_staticlib = String::from("staticlib"); + let kind_rlib = String::from("rlib"); + let kind_cdylib = String::from("cdylib"); + let kind_dylib = String::from("dylib"); + + self.metadata + .packages + .get(package) + .and_then(|meta_package| { + for target in &meta_package.targets { + if target.kind.contains(&kind_lib) + || target.kind.contains(&kind_staticlib) + || target.kind.contains(&kind_rlib) + || target.kind.contains(&kind_cdylib) + || target.kind.contains(&kind_dylib) + { + return Some(PathBuf::from(&target.src_path)); + } + } + None + }) + } + + pub(crate) fn expand_crate( + &self, + package: &PackageRef, + expand_all_features: bool, + expand_default_features: bool, + expand_features: &Option<Vec<String>>, + profile: Profile, + ) -> Result<String, cargo_expand::Error> { + cargo_expand::expand( + &self.manifest_path, + &package.name, + package.version.as_deref(), + self.clean, + expand_all_features, + expand_default_features, + expand_features, + profile, + ) + } +} diff --git a/src/bindgen/cargo/cargo_expand.rs b/src/bindgen/cargo/cargo_expand.rs new file mode 100644 index 0000000..565a0d1 --- /dev/null +++ b/src/bindgen/cargo/cargo_expand.rs @@ -0,0 +1,145 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::bindgen::config::Profile; +use std::env; +use std::error; +use std::fmt; +use std::io; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::str::{from_utf8, Utf8Error}; + +extern crate tempfile; +use self::tempfile::Builder; + +#[derive(Debug)] +/// Possible errors that can occur during `rustc -Zunpretty=expanded`. +pub enum Error { + /// Error during creation of temporary directory + Io(io::Error), + /// Output of `cargo metadata` was not valid utf8 + Utf8(Utf8Error), + /// Error during execution of `cargo rustc -Zunpretty=expanded` + Compile(String), +} + +impl From<io::Error> for Error { + fn from(err: io::Error) -> Self { + Error::Io(err) + } +} +impl From<Utf8Error> for Error { + fn from(err: Utf8Error) -> Self { + Error::Utf8(err) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::Io(ref err) => err.fmt(f), + Error::Utf8(ref err) => err.fmt(f), + Error::Compile(ref err) => write!(f, "{}", err), + } + } +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match self { + Error::Io(ref err) => Some(err), + Error::Utf8(ref err) => Some(err), + Error::Compile(..) => None, + } + } +} + +/// Use rustc to expand and pretty print the crate into a single file, +/// removing any macros in the process. +#[allow(clippy::too_many_arguments)] +pub fn expand( + manifest_path: &Path, + crate_name: &str, + version: Option<&str>, + use_tempdir: bool, + expand_all_features: bool, + expand_default_features: bool, + expand_features: &Option<Vec<String>>, + profile: Profile, +) -> Result<String, Error> { + let cargo = env::var("CARGO").unwrap_or_else(|_| String::from("cargo")); + let mut cmd = Command::new(cargo); + + let mut _temp_dir = None; // drop guard + if use_tempdir { + _temp_dir = Some(Builder::new().prefix("cbindgen-expand").tempdir()?); + cmd.env("CARGO_TARGET_DIR", _temp_dir.unwrap().path()); + } else if let Ok(ref path) = env::var("CARGO_EXPAND_TARGET_DIR") { + cmd.env("CARGO_TARGET_DIR", path); + } else if let Ok(ref path) = env::var("OUT_DIR") { + // When cbindgen was started programatically from a build.rs file, Cargo is running and + // locking the default target directory. In this case we need to use another directory, + // else we would end up in a deadlock. If Cargo is running `OUT_DIR` will be set, so we + // can use a directory relative to that. + cmd.env("CARGO_TARGET_DIR", PathBuf::from(path).join("expanded")); + } + + // Set this variable so that we don't call it recursively if we expand a crate that is using + // cbindgen + cmd.env("_CBINDGEN_IS_RUNNING", "1"); + + cmd.arg("rustc"); + cmd.arg("--lib"); + // When build with the release profile we can't choose the `check` profile. + if profile != Profile::Release { + cmd.arg("--profile=check"); + } + cmd.arg("--manifest-path"); + cmd.arg(manifest_path); + if let Some(features) = expand_features { + cmd.arg("--features"); + let mut features_str = String::new(); + for (index, feature) in features.iter().enumerate() { + if index != 0 { + features_str.push(' '); + } + features_str.push_str(feature); + } + cmd.arg(features_str); + } + if expand_all_features { + cmd.arg("--all-features"); + } + if !expand_default_features { + cmd.arg("--no-default-features"); + } + match profile { + Profile::Debug => {} + Profile::Release => { + cmd.arg("--release"); + } + } + cmd.arg("-p"); + let mut package = crate_name.to_owned(); + if let Some(version) = version { + package.push(':'); + package.push_str(version); + } + cmd.arg(&package); + cmd.arg("--verbose"); + cmd.arg("--"); + cmd.arg("-Zunpretty=expanded"); + info!("Command: {:?}", cmd); + let output = cmd.output()?; + + let src = from_utf8(&output.stdout)?.to_owned(); + let error = from_utf8(&output.stderr)?.to_owned(); + + if src.is_empty() { + Err(Error::Compile(error)) + } else { + Ok(src) + } +} diff --git a/src/bindgen/cargo/cargo_lock.rs b/src/bindgen/cargo/cargo_lock.rs new file mode 100644 index 0000000..9302082 --- /dev/null +++ b/src/bindgen/cargo/cargo_lock.rs @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::fs::File; +use std::io; +use std::io::Read; +use std::path::Path; + +#[derive(Debug)] +/// Possible errors that can occur during Cargo.toml parsing. +pub enum Error { + /// Error during reading of Cargo.toml + Io(io::Error), + /// Deserialization error + Toml(toml::de::Error), +} + +impl From<io::Error> for Error { + fn from(err: io::Error) -> Self { + Error::Io(err) + } +} +impl From<toml::de::Error> for Error { + fn from(err: toml::de::Error) -> Self { + Error::Toml(err) + } +} + +#[derive(Clone, Deserialize, Debug)] +pub struct Lock { + pub root: Option<Package>, + pub package: Option<Vec<Package>>, +} + +#[derive(Clone, Deserialize, Debug)] +pub struct Package { + pub name: String, + pub version: String, + /// A list of dependencies formatted like "NAME VERSION-OPT REGISTRY-OPT" + pub dependencies: Option<Vec<String>>, +} + +/// Parse the Cargo.toml for a given path +pub fn lock(manifest_path: &Path) -> Result<Lock, Error> { + let mut s = String::new(); + let mut f = File::open(manifest_path)?; + f.read_to_string(&mut s)?; + + toml::from_str::<Lock>(&s).map_err(|x| x.into()) +} diff --git a/src/bindgen/cargo/cargo_metadata.rs b/src/bindgen/cargo/cargo_metadata.rs new file mode 100644 index 0000000..01ab80f --- /dev/null +++ b/src/bindgen/cargo/cargo_metadata.rs @@ -0,0 +1,253 @@ +#![deny(missing_docs)] +#![allow(dead_code)] +//! Structured access to the output of `cargo metadata` +//! Usually used from within a `cargo-*` executable + +// Forked from `https://github.com/oli-obk/cargo_metadata` +// Modifications: +// 1. Remove `resolve` from Metadata because it was causing parse failures +// 2. Fix the `manifest-path` argument +// 3. Add `--all-features` argument +// 4. Remove the `--no-deps` argument + +use std::borrow::{Borrow, Cow}; +use std::collections::{HashMap, HashSet}; +use std::env; +use std::error; +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::io; +use std::path::Path; +use std::process::{Command, Output}; +use std::str::Utf8Error; + +#[derive(Clone, Deserialize, Debug)] +/// Starting point for metadata returned by `cargo metadata` +pub struct Metadata { + /// A list of all crates referenced by this crate (and the crate itself) + pub packages: HashSet<Package>, + version: usize, + /// path to the workspace containing the `Cargo.lock` + pub workspace_root: String, +} + +/// A reference to a package including it's name and the specific version. +#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct PackageRef { + pub name: String, + pub version: Option<String>, +} + +#[derive(Clone, Deserialize, Debug)] +/// A crate +pub struct Package { + #[serde(flatten)] + pub name_and_version: PackageRef, + id: String, + source: Option<String>, + /// List of dependencies of this particular package + pub dependencies: HashSet<Dependency>, + /// Targets provided by the crate (lib, bin, example, test, ...) + pub targets: Vec<Target>, + features: HashMap<String, Vec<String>>, + /// path containing the `Cargo.toml` + pub manifest_path: String, +} + +#[derive(Clone, Deserialize, Debug)] +/// A dependency of the main crate +pub struct Dependency { + /// Name as given in the `Cargo.toml` + pub name: String, + source: Option<String>, + /// Whether this is required or optional + pub req: String, + kind: Option<String>, + optional: bool, + uses_default_features: bool, + features: Vec<String>, + pub target: Option<String>, +} + +#[derive(Clone, Deserialize, Debug)] +/// A single target (lib, bin, example, ...) provided by a crate +pub struct Target { + /// Name as given in the `Cargo.toml` or generated from the file name + pub name: String, + /// Kind of target ("bin", "example", "test", "bench", "lib") + pub kind: Vec<String>, + /// Almost the same as `kind`, except when an example is a library instad of an executable. + /// In that case `crate_types` contains things like `rlib` and `dylib` while `kind` is `example` + #[serde(default)] + pub crate_types: Vec<String>, + /// Path to the main source file of the target + pub src_path: String, +} + +#[derive(Debug)] +/// Possible errors that can occur during metadata parsing. +pub enum Error { + /// Error during execution of `cargo metadata` + Io(io::Error), + /// Metadata extraction failure + Metadata(Output), + /// Output of `cargo metadata` was not valid utf8 + Utf8(Utf8Error), + /// Deserialization error (structure of json did not match expected structure) + Json(serde_json::Error), +} + +impl From<io::Error> for Error { + fn from(err: io::Error) -> Self { + Error::Io(err) + } +} +impl From<Utf8Error> for Error { + fn from(err: Utf8Error) -> Self { + Error::Utf8(err) + } +} +impl From<serde_json::Error> for Error { + fn from(err: serde_json::Error) -> Self { + Error::Json(err) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::Io(ref err) => err.fmt(f), + Error::Metadata(_) => write!(f, "Metadata error"), + Error::Utf8(ref err) => err.fmt(f), + Error::Json(ref err) => err.fmt(f), + } + } +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match self { + Error::Io(ref err) => Some(err), + Error::Metadata(_) => None, + Error::Utf8(ref err) => Some(err), + Error::Json(ref err) => Some(err), + } + } +} + +// Implementations that let us lookup Packages and Dependencies by name (string) + +impl Borrow<PackageRef> for Package { + fn borrow(&self) -> &PackageRef { + &self.name_and_version + } +} + +impl Hash for Package { + fn hash<H: Hasher>(&self, state: &mut H) { + self.name_and_version.hash(state); + } +} + +impl PartialEq for Package { + fn eq(&self, other: &Self) -> bool { + self.name_and_version == other.name_and_version + } +} + +impl Eq for Package {} + +impl Borrow<str> for Dependency { + fn borrow(&self) -> &str { + &self.name + } +} + +impl Hash for Dependency { + fn hash<H: Hasher>(&self, state: &mut H) { + self.name.hash(state); + } +} + +impl PartialEq for Dependency { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} + +impl Eq for Dependency {} + +fn discover_target(manifest_path: &Path) -> Option<String> { + if let Ok(target) = std::env::var("TARGET") { + return Some(target); + } + + // We must be running as a standalone script, not under cargo. + // Let's use the host platform instead. + // We figure out the host platform through rustc and use that. + // We unfortunatelly cannot go through cargo, since cargo rustc _also_ builds. + // If `rustc` fails to run, we just fall back to not passing --filter-platforms. + // + // NOTE: We set the current directory in case of rustup shenanigans. + let rustc = env::var("RUSTC").unwrap_or_else(|_| String::from("rustc")); + debug!("Discovering host platform by {:?}", rustc); + + let rustc_output = Command::new(rustc) + .current_dir(manifest_path.parent().unwrap()) + .arg("-vV") + .output(); + let rustc_output = match rustc_output { + Ok(ref out) => String::from_utf8_lossy(&out.stdout), + Err(..) => return None, + }; + + let field = "host: "; + rustc_output + .lines() + .find_map(|l| l.strip_prefix(field).map(|stripped| stripped.to_string())) +} + +/// The main entry point to obtaining metadata +pub fn metadata( + manifest_path: &Path, + existing_metadata_file: Option<&Path>, + only_target: bool, +) -> Result<Metadata, Error> { + let output; + let metadata = match existing_metadata_file { + Some(path) => Cow::Owned(std::fs::read_to_string(path)?), + None => { + let target = if only_target { + let target = discover_target(manifest_path); + if target.is_none() { + warn!( + "Failed to discover host platform for cargo metadata; \ + will fetch dependencies for all platforms." + ); + } + target + } else { + None + }; + + let cargo = env::var("CARGO").unwrap_or_else(|_| String::from("cargo")); + let mut cmd = Command::new(cargo); + cmd.arg("metadata"); + cmd.arg("--all-features"); + cmd.arg("--format-version").arg("1"); + if let Some(target) = target { + cmd.arg("--filter-platform").arg(target); + } + cmd.arg("--manifest-path"); + cmd.arg(manifest_path); + output = cmd.output()?; + if !output.status.success() { + return Err(Error::Metadata(output)); + } + Cow::Borrowed(std::str::from_utf8(&output.stdout)?) + } + }; + + let meta: Metadata = serde_json::from_str(&metadata)?; + Ok(meta) +} diff --git a/src/bindgen/cargo/cargo_toml.rs b/src/bindgen/cargo/cargo_toml.rs new file mode 100644 index 0000000..998176e --- /dev/null +++ b/src/bindgen/cargo/cargo_toml.rs @@ -0,0 +1,67 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::error; +use std::fmt; +use std::fs::File; +use std::io; +use std::io::Read; +use std::path::Path; + +#[derive(Debug)] +/// Possible errors that can occur during Cargo.toml parsing. +pub enum Error { + /// Error during reading of Cargo.toml + Io(io::Error), + /// Deserialization error + Toml(toml::de::Error), +} + +impl From<io::Error> for Error { + fn from(err: io::Error) -> Self { + Error::Io(err) + } +} +impl From<toml::de::Error> for Error { + fn from(err: toml::de::Error) -> Self { + Error::Toml(err) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::Io(ref err) => err.fmt(f), + Error::Toml(ref err) => err.fmt(f), + } + } +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match self { + Error::Io(ref err) => Some(err), + Error::Toml(ref err) => Some(err), + } + } +} + +#[derive(Clone, Deserialize, Debug)] +pub struct Manifest { + pub package: Package, +} + +#[derive(Clone, Deserialize, Debug)] +pub struct Package { + pub name: String, +} + +/// Parse the Cargo.toml for a given path +pub fn manifest(manifest_path: &Path) -> Result<Manifest, Error> { + let mut s = String::new(); + let mut f = File::open(manifest_path)?; + f.read_to_string(&mut s)?; + + toml::from_str::<Manifest>(&s).map_err(|x| x.into()) +} diff --git a/src/bindgen/cargo/mod.rs b/src/bindgen/cargo/mod.rs new file mode 100644 index 0000000..19fef54 --- /dev/null +++ b/src/bindgen/cargo/mod.rs @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#[allow(clippy::module_inception)] +mod cargo; +pub(crate) mod cargo_expand; +pub(crate) mod cargo_lock; +pub(crate) mod cargo_metadata; +pub(crate) mod cargo_toml; + +pub(crate) use self::cargo::*; |