#![deny(missing_docs)]
use std::collections::BTreeSet;
use std::ffi::OsStr;
use std::io::{BufRead, Cursor, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use serde_json::{Map, Value};
const CONAN_ENV: &str = "CONAN";
const DEFAULT_CONAN: &str = "conan";
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ConanVerbosity {
Quiet,
Error,
#[default]
Warning,
Notice,
Status,
Verbose,
Debug,
Trace,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub enum ConanScope<'a> {
#[default]
Global,
Local,
Package(&'a str),
}
#[derive(Default)]
pub struct ConanInstall {
output_folder: Option<PathBuf>,
recipe_path: Option<PathBuf>,
profile: Option<String>,
build_profile: Option<String>,
new_profile: bool,
build: Option<String>,
build_type: Option<String>,
remote: Option<String>,
confs: Vec<(String, String)>,
options: Vec<(String, String, String)>,
verbosity: ConanVerbosity,
extra_args: Vec<String>,
}
pub struct ConanOutput(Output);
pub struct CargoInstructions {
out: Vec<u8>,
includes: BTreeSet<PathBuf>,
lib_dirs: BTreeSet<PathBuf>,
}
struct ConanDependencyGraph(Value);
impl std::fmt::Display for ConanVerbosity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConanVerbosity::Quiet => f.write_str("quiet"),
ConanVerbosity::Error => f.write_str("error"),
ConanVerbosity::Warning => f.write_str("warning"),
ConanVerbosity::Notice => f.write_str("notice"),
ConanVerbosity::Status => f.write_str("status"),
ConanVerbosity::Verbose => f.write_str("verbose"),
ConanVerbosity::Debug => f.write_str("debug"),
ConanVerbosity::Trace => f.write_str("trace"),
}
}
}
impl std::fmt::Display for ConanScope<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConanScope::Global => f.write_str("*"),
ConanScope::Local => f.write_str("&"),
ConanScope::Package(name) => {
if name.contains('/') {
f.write_str(name)
} else {
write!(f, "{name}/*")
}
}
}
}
}
impl ConanInstall {
#[must_use]
pub fn new() -> ConanInstall {
ConanInstall::default()
}
#[must_use]
pub fn with_recipe(recipe_path: &Path) -> ConanInstall {
ConanInstall {
recipe_path: Some(recipe_path.to_owned()),
..Default::default()
}
}
pub fn output_folder(&mut self, output_folder: &Path) -> &mut ConanInstall {
self.output_folder = Some(output_folder.to_owned());
self
}
pub fn profile(&mut self, profile: &str) -> &mut ConanInstall {
self.profile = Some(profile.to_owned());
self
}
pub fn host_profile(&mut self, profile: &str) -> &mut ConanInstall {
self.profile(profile)
}
pub fn build_profile(&mut self, profile: &str) -> &mut ConanInstall {
self.build_profile = Some(profile.to_owned());
self
}
pub fn detect_profile(&mut self) -> &mut ConanInstall {
self.new_profile = true;
self
}
pub fn build_type(&mut self, build_type: &str) -> &mut ConanInstall {
self.build_type = Some(build_type.to_owned());
self
}
pub fn remote(&mut self, remote: &str) -> &mut ConanInstall {
self.remote = Some(remote.to_owned());
self
}
pub fn config(&mut self, key: &str, value: &str) -> &mut ConanInstall {
self.confs.push((key.to_owned(), value.to_owned()));
self
}
pub fn option(&mut self, scope: ConanScope, key: &str, value: &str) -> &mut ConanInstall {
self.options
.push((scope.to_string(), key.to_owned(), value.to_owned()));
self
}
pub fn build(&mut self, build: &str) -> &mut ConanInstall {
self.build = Some(build.to_owned());
self
}
pub fn verbosity(&mut self, verbosity: ConanVerbosity) -> &mut ConanInstall {
self.verbosity = verbosity;
self
}
pub fn arg(&mut self, arg: &str) -> &mut ConanInstall {
self.extra_args.push(arg.to_owned());
self
}
#[must_use]
pub fn run(&self) -> ConanOutput {
let conan = std::env::var_os(CONAN_ENV).unwrap_or_else(|| DEFAULT_CONAN.into());
let recipe = self.recipe_path.as_deref().unwrap_or(Path::new("."));
let output_folder = match &self.output_folder {
Some(s) => s.clone(),
None => std::env::var_os("OUT_DIR")
.expect("OUT_DIR environment variable must be set")
.into(),
};
if self.new_profile {
Self::run_profile_detect(&conan, self.profile.as_deref());
if self.build_profile != self.profile {
Self::run_profile_detect(&conan, self.build_profile.as_deref());
};
}
let mut command = Command::new(conan);
command
.arg("install")
.arg(recipe)
.arg(format!("-v{}", self.verbosity))
.arg("--format")
.arg("json")
.arg("--output-folder")
.arg(output_folder);
if let Some(remote) = self.remote.as_deref() {
command.arg("--remote");
command.arg(remote);
}
if let Some(profile) = self.profile.as_deref() {
command.arg("--profile:host").arg(profile);
}
if let Some(build_profile) = self.build_profile.as_deref() {
command.arg("--profile:build").arg(build_profile);
}
if let Some(build) = self.build.as_deref() {
command.arg("--build");
command.arg(build);
}
if let Some(build_type) = self.build_type.as_deref() {
command.arg("--settings");
command.arg(format!("build_type={build_type}"));
} else {
Self::add_settings_from_env(&mut command);
}
for (scope, key, value) in &self.options {
command.arg("--options");
command.arg(format!("{scope}:{key}={value}"));
}
for (key, value) in &self.confs {
command.arg("--conf");
command.arg(format!("{key}={value}"));
}
self.extra_args.iter().for_each(|x| {
command.arg(x);
});
let output = command
.output()
.expect("failed to run the Conan executable");
ConanOutput(output)
}
fn run_profile_detect(conan: &OsStr, profile: Option<&str>) {
let mut command = Command::new(conan);
command.arg("profile").arg("detect").arg("--exist-ok");
if let Some(profile) = profile {
println!("running 'conan profile detect' for profile '{profile}'");
command.arg("--name").arg(profile);
} else {
println!("running 'conan profile detect' for the default profile");
}
let status = command
.status()
.expect("failed to run the Conan executable");
#[allow(clippy::manual_assert)]
if !status.success() {
panic!("'conan profile detect' command failed: {status}");
}
}
fn add_settings_from_env(command: &mut Command) {
match std::env::var("PROFILE").as_deref() {
Ok("debug") => {
command.arg("-s");
command.arg("build_type=Debug");
}
Ok("release") => {
command.arg("-s");
command.arg("build_type=Release");
}
_ => (),
}
}
}
impl ConanOutput {
#[must_use]
pub fn parse(self) -> CargoInstructions {
self.ensure_success();
let mut cargo = CargoInstructions::new();
cargo.rerun_if_env_changed(CONAN_ENV);
for line in Cursor::new(self.stderr()).lines() {
if let Some(msg) = line.unwrap().strip_prefix("WARN: ") {
cargo.warning(msg);
}
}
let metadata: Value =
serde_json::from_slice(self.stdout()).expect("failed to parse JSON output");
ConanDependencyGraph(metadata).traverse(&mut cargo);
cargo
}
pub fn ensure_success(&self) {
if self.is_success() {
return;
}
let code = self.status_code();
let msg = String::from_utf8_lossy(self.stderr());
panic!("Conan failed with status {code}: {msg}");
}
#[must_use]
pub fn is_success(&self) -> bool {
self.0.status.success()
}
#[must_use]
pub fn status_code(&self) -> i32 {
self.0.status.code().unwrap_or_default()
}
#[must_use]
pub fn stdout(&self) -> &[u8] {
&self.0.stdout
}
#[must_use]
pub fn stderr(&self) -> &[u8] {
&self.0.stderr
}
}
impl CargoInstructions {
pub fn emit(&self) {
std::io::stdout().write_all(self.as_bytes()).unwrap();
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.out
}
#[must_use]
pub fn include_paths(&self) -> Vec<PathBuf> {
self.includes.iter().cloned().collect()
}
#[must_use]
pub fn library_paths(&self) -> Vec<PathBuf> {
self.lib_dirs.iter().cloned().collect()
}
fn new() -> CargoInstructions {
CargoInstructions {
out: Vec::with_capacity(1024),
includes: BTreeSet::new(),
lib_dirs: BTreeSet::new(),
}
}
fn warning(&mut self, message: &str) {
writeln!(self.out, "cargo:warning={message}").unwrap();
}
fn rerun_if_env_changed(&mut self, val: &str) {
writeln!(self.out, "cargo:rerun-if-env-changed={val}").unwrap();
}
fn rustc_cdylib_link_arg(&mut self, val: &str) {
writeln!(self.out, "cargo:rustc-cdylib-link-arg={val}").unwrap();
}
fn rustc_link_arg_bins(&mut self, val: &str) {
writeln!(self.out, "cargo:rustc-link-arg-bins={val}").unwrap();
}
fn rustc_link_lib(&mut self, lib: &str) {
if let Some(lib) = lib.strip_prefix("lib") {
if let Some(lib) = lib.strip_suffix(".a") {
self.rustc_link_lib_kind(lib, Some("static"));
return;
} else if let Some(lib) = lib.strip_suffix(".so") {
self.rustc_link_lib_kind(lib, Some("dylib"));
return;
}
}
self.rustc_link_lib_kind(lib, None);
}
fn rustc_link_lib_kind(&mut self, lib: &str, kind: Option<&str>) {
match kind {
Some(kind) => {
writeln!(self.out, "cargo:rustc-link-lib={kind}={lib}").unwrap();
}
None => {
writeln!(self.out, "cargo:rustc-link-lib={lib}").unwrap();
}
}
}
fn rustc_link_search(&mut self, path: &str) {
let lib_dir = path.into();
if !self.lib_dirs.contains(&lib_dir) {
writeln!(self.out, "cargo:rustc-link-search={path}").unwrap();
self.lib_dirs.insert(lib_dir);
}
}
fn include(&mut self, path: &str) {
let include_dir = path.into();
if !self.includes.contains(&include_dir) {
writeln!(self.out, "cargo:include={path}").unwrap();
self.includes.insert(include_dir);
}
}
}
impl ConanDependencyGraph {
fn traverse(self, cargo: &mut CargoInstructions) {
let root_node_id = "0";
self.visit_dependency(cargo, root_node_id);
}
fn visit_dependency(&self, cargo: &mut CargoInstructions, node_id: &str) {
let Some(node) = self.find_node(node_id) else {
return;
};
if let Some(Value::Object(cpp_info)) = node.get("cpp_info") {
for cpp_comp_name in cpp_info.keys() {
Self::visit_cpp_component(cargo, cpp_info, cpp_comp_name);
}
};
if let Some(Value::Object(dependencies)) = node.get("dependencies") {
for dependency_id in dependencies.keys() {
self.visit_dependency(cargo, dependency_id);
}
};
}
fn visit_cpp_component(
cargo: &mut CargoInstructions,
cpp_info: &Map<String, Value>,
comp_name: &str,
) {
let Some(component) = Self::find_cpp_component(cpp_info, comp_name) else {
return;
};
if let Some(Value::Array(libs)) = component.get("libs") {
if !libs.is_empty() {
if let Some(Value::Array(libdirs)) = component.get("libdirs") {
for libdir in libdirs {
if let Value::String(libdir) = libdir {
cargo.rustc_link_search(libdir);
}
}
}
}
for lib in libs {
if let Value::String(lib) = lib {
cargo.rustc_link_lib(lib);
}
}
}
if let Some(Value::Array(system_libs)) = component.get("system_libs") {
for system_lib in system_libs {
if let Value::String(system_lib) = system_lib {
cargo.rustc_link_lib(system_lib);
}
}
};
if let Some(Value::Array(includedirs)) = component.get("includedirs") {
for include in includedirs {
if let Value::String(include) = include {
cargo.include(include);
}
}
};
if let Some(Value::Array(flags)) = component.get("sharedlinkflags") {
for flag in flags {
if let Value::String(flag) = flag {
cargo.rustc_cdylib_link_arg(flag);
}
}
}
if let Some(Value::Array(flags)) = component.get("exelinkflags") {
for flag in flags {
if let Value::String(flag) = flag {
cargo.rustc_link_arg_bins(flag);
}
}
}
if let Some(Value::Array(requires)) = component.get("requires") {
for requirement in requires {
if let Value::String(req_comp_name) = requirement {
Self::visit_cpp_component(cargo, cpp_info, req_comp_name);
}
}
};
}
fn find_node(&self, id: &str) -> Option<&Map<String, Value>> {
let Value::Object(root) = &self.0 else {
panic!("root JSON object expected");
};
let Some(Value::Object(graph)) = root.get("graph") else {
panic!("root 'graph' object expected");
};
let Some(Value::Object(nodes)) = graph.get("nodes") else {
panic!("root 'nodes' object expected");
};
if let Some(Value::Object(node)) = nodes.get(id) {
Some(node)
} else {
None
}
}
fn find_cpp_component<'a>(
cpp_info: &'a Map<String, Value>,
name: &str,
) -> Option<&'a Map<String, Value>> {
if let Some(Value::Object(component)) = cpp_info.get(name) {
Some(component)
} else {
None
}
}
}