use std::path::{Path, PathBuf};
use std::process::Command;
use serde::Deserialize;
use crate::cli::{CliError, Result};
use crate::target::{BuiltLibrary, Platform, RustTarget};
use super::Cargo;
#[derive(Debug, Deserialize)]
pub(crate) struct CargoMetadata {
pub(crate) packages: Vec<CargoMetadataPackage>,
pub(crate) target_directory: PathBuf,
#[serde(default)]
pub(crate) workspace_root: Option<PathBuf>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct CargoMetadataPackage {
pub(crate) id: String,
pub(crate) name: String,
pub(crate) manifest_path: PathBuf,
pub(crate) targets: Vec<CargoMetadataPackageTarget>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct CargoMetadataPackageTarget {
pub(crate) name: String,
pub(crate) crate_types: Vec<CargoCrateType>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(from = "String")]
pub(crate) enum CargoCrateType {
StaticLib,
Cdylib,
Dylib,
Rlib,
Bin,
ProcMacro,
Other(String),
}
impl CargoMetadata {
pub(crate) fn load(cargo: &Cargo) -> Result<Self> {
let mut command = Command::new("cargo");
command.current_dir(cargo.working_directory());
if let Some(toolchain_selector) = cargo.toolchain_selector() {
command.arg(toolchain_selector);
}
let output = command
.args(["metadata", "--format-version", "1", "--no-deps"])
.args(cargo.metadata_arguments())
.output()
.map_err(|source| CliError::CommandFailed {
command: format!("cargo metadata: {source}"),
status: None,
})?;
if !output.status.success() {
return Err(CliError::CommandFailed {
command: "cargo metadata --format-version 1 --no-deps".to_string(),
status: output.status.code(),
});
}
Self::parse(&output.stdout)
}
pub(crate) fn find_package(
&self,
manifest_path: &Path,
package_selector: Option<&str>,
) -> Result<&CargoMetadataPackage> {
if let Some(package_selector) = package_selector {
return self
.packages
.iter()
.find(|package| package.matches_selector(package_selector))
.ok_or_else(|| CliError::CommandFailed {
command: format!(
"could not find selected cargo package '{}' in cargo metadata",
package_selector
),
status: None,
});
}
self.packages
.iter()
.find(|package| package.manifest_path == manifest_path)
.ok_or_else(|| CliError::CommandFailed {
command: format!(
"could not find current package manifest '{}' in cargo metadata",
manifest_path.display()
),
status: None,
})
}
#[cfg(test)]
pub(crate) fn target_directory_from_bytes(metadata: &[u8]) -> Result<PathBuf> {
Ok(Self::parse(metadata)?.target_directory)
}
fn parse(metadata: &[u8]) -> Result<Self> {
serde_json::from_slice::<Self>(metadata).map_err(|source| CliError::CommandFailed {
command: format!("parse cargo metadata: {source}"),
status: None,
})
}
pub(crate) fn resolve_built_libraries_for_targets(
&self,
cargo_manifest_path: &Path,
profile_directory_name: &str,
preferred_artifact_name: &str,
package_selector: Option<&str>,
package_targets: &[RustTarget],
) -> Result<Vec<BuiltLibrary>> {
let package = self.find_package(cargo_manifest_path, package_selector)?;
let artifact_name = package
.resolve_library_artifact_name(preferred_artifact_name, cargo_manifest_path)?
.to_string();
let library_target = package.resolve_library_target(&artifact_name, cargo_manifest_path)?;
let mut libraries = vec![];
for t in package_targets {
for crate_type in &library_target.crate_types {
let prefix = if crate_type.is_staticlib() {
"lib"
} else {
match t.platform() {
Platform::Wasm => "",
Platform::Android
| Platform::Ios
| Platform::IosSimulator
| Platform::Linux
| Platform::MacOs => "lib",
}
};
let suffix = if crate_type.is_cdylib() {
match t.platform() {
Platform::Wasm => "wasm",
Platform::Android | Platform::Linux => "so",
Platform::MacOs | Platform::Ios | Platform::IosSimulator => "dylib",
}
} else if crate_type.is_staticlib() {
"a"
} else {
continue;
};
let library_filename = format!("{}{}.{}", prefix, artifact_name, suffix);
let library_filepath = self
.target_directory
.join(t.triple())
.join(profile_directory_name)
.join(library_filename);
libraries.push(BuiltLibrary {
target: *t,
path: library_filepath,
})
}
}
Ok(libraries)
}
}
impl CargoMetadataPackage {
pub(crate) fn has_target(&self, target_name: &str) -> bool {
self.targets.iter().any(|target| target.name == target_name)
}
pub(crate) fn resolve_library_artifact_name(
&self,
preferred_artifact_name: &str,
manifest_path: &Path,
) -> Result<&str> {
self.resolve_library_target(preferred_artifact_name, manifest_path)
.map(|target| target.name.as_str())
}
pub(crate) fn resolve_library_target(
&self,
preferred_artifact_name: &str,
manifest_path: &Path,
) -> Result<&CargoMetadataPackageTarget> {
let ffi_targets = self
.targets
.iter()
.filter(|target| target.builds_ffi())
.collect::<Vec<_>>();
if let Some(target) = ffi_targets
.iter()
.copied()
.find(|target| target.name == preferred_artifact_name)
{
return Ok(target);
}
if ffi_targets.len() == 1 {
return Ok(ffi_targets[0]);
}
Err(CliError::CommandFailed {
command: format!(
"could not find library target '{}' in cargo metadata for '{}'",
preferred_artifact_name,
manifest_path.display()
),
status: None,
})
}
fn matches_selector(&self, package_selector: &str) -> bool {
self.name == package_selector
|| self.id == package_selector
|| self.matches_package_spec(package_selector)
}
fn matches_package_spec(&self, package_selector: &str) -> bool {
let Some((name, version)) = package_selector.rsplit_once('@') else {
return false;
};
self.name == name && self.version().is_some_and(|value| value == version)
}
fn version(&self) -> Option<&str> {
let fragment = self.id.rsplit('#').next()?;
let (_, version) = fragment.rsplit_once('@')?;
Some(version)
}
}
impl CargoMetadataPackageTarget {
pub(crate) fn builds_ffi(&self) -> bool {
self.builds_staticlib() || self.builds_cdylib()
}
pub(crate) fn builds_staticlib(&self) -> bool {
self.crate_types.iter().any(CargoCrateType::is_staticlib)
}
pub(crate) fn builds_cdylib(&self) -> bool {
self.crate_types.iter().any(CargoCrateType::is_cdylib)
}
}
impl CargoCrateType {
pub(crate) fn is_staticlib(&self) -> bool {
matches!(self, Self::StaticLib)
}
pub(crate) fn is_cdylib(&self) -> bool {
matches!(self, Self::Cdylib)
}
}
impl From<String> for CargoCrateType {
fn from(crate_type: String) -> Self {
match crate_type.as_str() {
"staticlib" => Self::StaticLib,
"cdylib" => Self::Cdylib,
"dylib" => Self::Dylib,
"rlib" => Self::Rlib,
"bin" => Self::Bin,
"proc-macro" => Self::ProcMacro,
_ => Self::Other(crate_type),
}
}
}
impl From<&str> for CargoCrateType {
fn from(crate_type: &str) -> Self {
Self::from(crate_type.to_string())
}
}
impl From<CargoCrateType> for String {
fn from(crate_type: CargoCrateType) -> Self {
match crate_type {
CargoCrateType::StaticLib => "staticlib".to_string(),
CargoCrateType::Cdylib => "cdylib".to_string(),
CargoCrateType::Dylib => "dylib".to_string(),
CargoCrateType::Rlib => "rlib".to_string(),
CargoCrateType::Bin => "bin".to_string(),
CargoCrateType::ProcMacro => "proc-macro".to_string(),
CargoCrateType::Other(crate_type) => crate_type,
}
}
}
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
use super::CargoMetadata;
use crate::{
cargo::{
CargoCrateType,
fixture::{CargoMetadataFixture, CargoPackageFixture, CargoTargetFixture},
},
target::RustTarget,
};
fn metadata_fixture() -> CargoMetadataFixture {
CargoMetadataFixture::new("/tmp/boltffi-target")
}
#[test]
fn parses_target_directory_from_cargo_metadata() {
let target_directory =
CargoMetadata::target_directory_from_bytes(&metadata_fixture().json_bytes())
.expect("expected target directory");
assert_eq!(target_directory, PathBuf::from("/tmp/boltffi-target"));
}
#[test]
fn parses_workspace_root_from_cargo_metadata() {
let metadata = metadata_fixture()
.workspace_root("/tmp/workspace")
.metadata();
assert_eq!(
metadata.workspace_root,
Some(PathBuf::from("/tmp/workspace"))
);
}
#[test]
fn finds_current_cargo_metadata_package_by_manifest_path() {
let metadata = metadata_fixture()
.package(CargoPackageFixture::manifest_package(
"workspace-a",
"/tmp/workspace/a/Cargo.toml",
"0.1.0",
))
.package(CargoPackageFixture::manifest_package(
"workspace-b",
"/tmp/workspace/b/Cargo.toml",
"0.1.0",
))
.metadata();
let package = metadata
.find_package(Path::new("/tmp/workspace/b/Cargo.toml"), None)
.expect("package lookup");
assert_eq!(package.id, "path+file:///tmp/workspace/b#0.1.0");
}
#[test]
fn finds_selected_cargo_metadata_package_by_package_name() {
let metadata = metadata_fixture()
.package(CargoPackageFixture::workspace_package(
"workspace-a",
"/tmp/workspace/Cargo.toml",
"0.1.0",
))
.package(CargoPackageFixture::workspace_package(
"workspace-b",
"/tmp/workspace/Cargo.toml",
"0.1.0",
))
.metadata();
let package = metadata
.find_package(Path::new("/tmp/workspace/Cargo.toml"), Some("workspace-b"))
.expect("package lookup");
assert_eq!(package.id, "path+file:///tmp/workspace#workspace-b@0.1.0");
}
#[test]
fn finds_selected_cargo_metadata_package_by_package_spec() {
let metadata = metadata_fixture()
.package(CargoPackageFixture::workspace_package(
"workspace-a",
"/tmp/workspace/Cargo.toml",
"0.1.0",
))
.package(CargoPackageFixture::workspace_package(
"workspace-b",
"/tmp/workspace/Cargo.toml",
"1.2.3",
))
.metadata();
let package = metadata
.find_package(
Path::new("/tmp/workspace/Cargo.toml"),
Some("workspace-b@1.2.3"),
)
.expect("package lookup");
assert_eq!(package.id, "path+file:///tmp/workspace#workspace-b@1.2.3");
}
#[test]
fn metadata_resolves_built_libraries_for_targets() {
let metadata = metadata_fixture()
.package(
CargoPackageFixture::manifest_package(
"my-workspace",
"/tmp/my-workspace/Cargo.toml",
"0.1.0",
)
.target(CargoTargetFixture::library(
"my_workspace",
[
CargoCrateType::Cdylib,
CargoCrateType::StaticLib,
CargoCrateType::Bin,
],
)),
)
.metadata();
let targets = [
RustTarget::IOS_ARM64,
RustTarget::WASM32_UNKNOWN_UNKNOWN,
RustTarget::LINUX_X86_64,
];
let expected_library_paths_for_targets = [
[
format!(
"/tmp/boltffi-target/{}/release/libmy_workspace.dylib",
targets[0].triple()
),
format!(
"/tmp/boltffi-target/{}/release/libmy_workspace.a",
targets[0].triple()
),
],
[
format!(
"/tmp/boltffi-target/{}/release/my_workspace.wasm",
targets[1].triple()
),
format!(
"/tmp/boltffi-target/{}/release/libmy_workspace.a",
targets[1].triple()
),
],
[
format!(
"/tmp/boltffi-target/{}/release/libmy_workspace.so",
targets[2].triple()
),
format!(
"/tmp/boltffi-target/{}/release/libmy_workspace.a",
targets[2].triple()
),
],
];
let libraries = metadata
.resolve_built_libraries_for_targets(
Path::new("/tmp/my-workspace/Cargo.toml"),
"release",
"my_workspace",
None,
&targets,
)
.unwrap();
let matches_any_expected_path = |i: usize, p: &Path| {
expected_library_paths_for_targets[i]
.iter()
.any(|ex| ex == p)
};
for l in libraries {
for (i, target) in targets.into_iter().enumerate() {
if l.target == target {
assert!(matches_any_expected_path(i, &l.path));
}
}
}
}
}