conan 0.4.2

A Rust wrapper of the conan C/C++ package manager (conan.io) to simplify usage in build scripts
Documentation
#[cfg(test)]
mod tests;

use super::util::find_program;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use std::process::ExitStatus;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum ConanPackageError {
    #[error("The recipe path is missing")]
    MissingRecipePath,

    #[error("Invalid Unicode in path")]
    InvalidUnicodeInPath,

    #[error("Conan binary not found")]
    ConanNotFound,

    #[error("Command execution failed")]
    CommandExecutionFailed,

    #[error("I/O error: {0}")]
    Io(#[from] std::io::Error),

    #[error("Invalid file name: {0}")]
    InvalidFileName(String),

    #[error("Other error: {0}")]
    Other(String),
}

/// Thin Wrapper around binary packages that contain libraries and headers
pub struct ConanPackage {
    path: PathBuf,
}

/// "conan package" command runner
#[derive(Default)]
pub struct PackageCommand {
    build_path: Option<PathBuf>,
    install_path: Option<PathBuf>,
    package_path: Option<PathBuf>,
    source_path: Option<PathBuf>,
    recipe_path: Option<PathBuf>,
}

impl Default for PackageCommandBuilder {
    fn default() -> Self {
        PackageCommandBuilder {
            build_path: None,
            install_path: None,
            package_path: None,
            source_path: None,
            recipe_path: Some(PathBuf::from(".")),
        }
    }
}

/// Command arguments builder for "conan package"
pub struct PackageCommandBuilder {
    build_path: Option<PathBuf>,
    install_path: Option<PathBuf>,
    package_path: Option<PathBuf>,
    source_path: Option<PathBuf>,
    recipe_path: Option<PathBuf>,
}

impl PackageCommandBuilder {
    pub fn new() -> Self {
        PackageCommandBuilder::default()
    }

    pub fn with_build_path(mut self, build_path: PathBuf) -> Self {
        self.build_path = Some(build_path);
        self
    }

    pub fn with_install_path(mut self, install_path: PathBuf) -> Self {
        self.install_path = Some(install_path);
        self
    }

    pub fn with_package_path(mut self, package_path: PathBuf) -> Self {
        self.package_path = Some(package_path);
        self
    }

    pub fn with_source_path(mut self, source_path: PathBuf) -> Self {
        self.source_path = Some(source_path);
        self
    }

    pub fn with_recipe_path(mut self, recipe_path: PathBuf) -> Self {
        self.recipe_path = Some(recipe_path);
        self
    }

    pub fn build(self) -> PackageCommand {
        PackageCommand {
            build_path: self.build_path,
            install_path: self.install_path,
            package_path: self.package_path,
            source_path: self.source_path,
            recipe_path: self.recipe_path,
        }
    }
}

impl PackageCommand {
    pub fn args(&self) -> Result<Vec<String>, ConanPackageError> {
        let mut args: Vec<&str> = Vec::new();

        args.extend(&["package", self.recipe_path.as_ref().unwrap().to_str().unwrap()]);

        if let Some(build_path) = &self.build_path {
            args.extend(&[
                "--build-folder",
                build_path.to_str().ok_or(ConanPackageError::InvalidUnicodeInPath)?,
            ]);
        }

        if let Some(install_path) = &self.install_path {
            args.extend(&[
                "--install-folder",
                install_path.to_str().ok_or(ConanPackageError::InvalidUnicodeInPath)?,
            ]);
        }

        if let Some(package_path) = &self.package_path {
            args.extend(&[
                "--package-folder",
                package_path.to_str().ok_or(ConanPackageError::InvalidUnicodeInPath)?,
            ]);
        }

        if let Some(source_path) = &self.source_path {
            args.extend(&[
                "--source-folder",
                source_path.to_str().ok_or(ConanPackageError::InvalidUnicodeInPath)?,
            ]);
        }

        Ok(args.iter().map(|s| s.to_string()).collect())
    }

    pub fn run(&self) -> Option<ExitStatus> {
        let args = self.args().ok()?;
        let conan_bin = find_program()?;
        let mut command = Command::new(conan_bin);
        Some(command.args(args).status().ok()?)
    }
}

impl Default for ConanPackage {
    fn default() -> Self {
        ConanPackage {
            path: PathBuf::from("./package/"),
        }
    }
}

impl ConanPackage {
    pub fn new(path: PathBuf) -> Self {
        ConanPackage { path }
    }

    pub fn emit_cargo_libs_linkage(&self, libs_dir: PathBuf) -> Result<(), ConanPackageError> {
        let libs_dir_path = self.path.join(libs_dir);

        let entries = fs::read_dir(libs_dir_path.clone())?;

        for entry in entries {
            let lib_path = entry?.path();
            if lib_path.is_file() {
                let lib_name = lib_path
                    .file_stem()
                    .and_then(|n| n.to_str())
                    .ok_or_else(|| ConanPackageError::InvalidFileName(lib_path.display().to_string()))?;

                let lib_name = if lib_name.starts_with("lib") {
                    &lib_name[3..]
                } else {
                    lib_name
                };

                if let Some(lib_suffix) = lib_path.extension().and_then(|s| s.to_str()) {
                    let lib_type = match lib_suffix {
                        "so" | "dll" | "dylib" => "dylib",
                        "a" | "lib" => "static",
                        _ => continue,
                    };
                    println!("cargo:rustc-link-lib={}={}", lib_type, lib_name);
                }
            }
        }

        println!("cargo:rustc-link-search=native={}", libs_dir_path.display());

        Ok(())
    }
}