cargo-shim 0.1.0

A helper library for interacting with Cargo
Documentation
#![deny(
    missing_debug_implementations,
    trivial_numeric_casts,
    unstable_features,
    unused_import_braces,
    unused_qualifications
)]

extern crate cargo_metadata;
extern crate regex;

use std::process::Command;
use std::path::{Path, PathBuf};

use regex::Regex;

#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum BuildType {
    Debug,
    Release
}

#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum Profile {
    Main,
    Test,
    Bench
}

#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum TargetKind {
    Lib,
    Bin,
    Example,
    Test,
    Bench
}

#[derive(Clone, Debug)]
pub struct CargoProject {
    pub packages: Vec< CargoPackage >
}

#[derive(Clone, Debug)]
pub struct CargoPackage {
    pub name: String,
    pub manifest_path: PathBuf,
    pub crate_root: PathBuf,
    pub targets: Vec< CargoTarget >
}

#[derive(Clone, Debug)]
pub struct CargoTarget {
    pub name: String,
    pub kind: TargetKind,
    pub source_directory: PathBuf
}

impl CargoProject {
    pub fn new( manifest_path: Option< &str > ) -> CargoProject {
        let metadata = cargo_metadata::metadata( manifest_path ).unwrap();
        CargoProject {
            packages: metadata.packages.into_iter().map( |package| {
                let manifest_path: PathBuf = package.manifest_path.into();
                CargoPackage {
                    name: package.name,
                    crate_root: manifest_path.parent().unwrap().into(),
                    manifest_path: manifest_path,
                    targets: package.targets.into_iter().map( |target| {
                        CargoTarget {
                            name: target.name,
                            kind: match target.kind[ 0 ].as_str() {
                                "lib" => TargetKind::Lib,
                                "bin" => TargetKind::Bin,
                                "example" => TargetKind::Example,
                                "test" => TargetKind::Test,
                                "bench" => TargetKind::Bench,
                                _ => panic!( "Unknown target kind: '{}'", target.kind[ 0 ] )
                            },
                            source_directory: Into::< PathBuf >::into( target.src_path ).parent().unwrap().into()
                        }
                    }).collect()
                }
            }).collect()
        }
    }

    pub fn default_package( &self ) -> &CargoPackage {
        &self.packages[ 0 ]
    }
}

#[derive(Clone, Debug)]
pub enum BuildTarget {
    Lib( String, Profile ),
    Bin( String, Profile ),
    ExampleBin( String ),
    IntegrationTest( String ),
    IntegrationBench( String )
}

#[derive(Clone, Debug)]
pub struct BuildConfig {
    pub build_target: BuildTarget,
    pub build_type: BuildType,
    pub triplet: Option< String >,
    pub package: Option< String >
}

fn profile_to_arg( profile: Profile ) -> &'static str {
    match profile {
        Profile::Main => "dev",
        Profile::Test => "test",
        Profile::Bench => "bench"
    }
}

pub fn target_to_build_target( target: &CargoTarget, profile: Profile ) -> BuildTarget {
    match target.kind {
        TargetKind::Lib => BuildTarget::Lib( target.name.clone(), profile ),
        TargetKind::Bin => BuildTarget::Bin( target.name.clone(), profile ),
        TargetKind::Example => BuildTarget::ExampleBin( target.name.clone() ),
        TargetKind::Test => BuildTarget::IntegrationTest( target.name.clone() ),
        TargetKind::Bench => BuildTarget::IntegrationBench( target.name.clone() )
    }
}

impl BuildConfig {
    pub fn as_command( &self ) -> Command {
        let mut command = Command::new( "cargo" );
        command.arg( "rustc" );

        if let Some( ref triplet ) = self.triplet {
            command.arg( "--target" ).arg( triplet.as_str() );
        }

        if let Some( ref package ) = self.package {
            command.arg( "--package" ).arg( package.as_str() );
        }

        match self.build_type {
            BuildType::Debug => {},
            BuildType::Release => {
                command.arg( "--release" );
            }
        }

        match self.build_target {
            BuildTarget::Lib( _, profile ) => {
                command
                    .arg( "--profile" ).arg( profile_to_arg( profile ) )
                    .arg( "--lib" );
            },
            BuildTarget::Bin( ref name, profile ) => {
                command
                    .arg( "--profile" ).arg( profile_to_arg( profile ) )
                    .arg( "--bin" ).arg( name.as_str() );
            },
            BuildTarget::ExampleBin( ref name ) => {
                command.arg( "--example" ).arg( name.as_str() );
            },
            BuildTarget::IntegrationTest( ref name ) => {
                command.arg( "--test" ).arg( name.as_str() );
            },
            BuildTarget::IntegrationBench( ref name ) => {
                command.arg( "--bench" ).arg( name.as_str() );
            }
        }

        command
    }

    pub fn output_directory< P: AsRef< Path > >( &self, crate_root: P ) -> PathBuf {
        let crate_root = crate_root.as_ref();
        let mut directory = crate_root.join( "target" );
        if let Some( ref triplet ) = self.triplet {
            directory = directory.join( &triplet );
        }

        directory = match self.build_type {
            BuildType::Debug => directory.join( "debug" ),
            BuildType::Release => directory.join( "release" )
        };

        match self.build_target {
            BuildTarget::ExampleBin( _ ) => directory.join( "examples" ),
            _ => directory
        }
    }

    // Ugh... this is really dumb, and probably buggy, but Cargo doesn't support a seemingly fundamental
    // feature like being able to tell us what exactly it generated, so we have to guess. Hopefully
    // it will get fixed eventually.
    pub fn potential_artifacts< P: AsRef< Path > >( &self, crate_root: P ) -> Vec< PathBuf > {
        let mut matchers = Vec::new();

        macro_rules! matcher {
            ($regex:expr) => {
                matchers.push( Regex::new( $regex.as_ref() ).unwrap() )
            }
        };

        macro_rules! prefix_matcher {
            ($prefix:expr) => {
                matcher!( format!( "^{}-", $prefix ) )
            }
        };

        if self.triplet.as_ref().map( |value| value.as_str() ) == Some( "asmjs-unknown-emscripten" ) {
            matcher!( "\\.js$" );
        } if cfg!( target_os = "windows" ) {
            match self.build_target {
                BuildTarget::Lib( _, Profile::Main ) => matcher!( "\\.(lib|dll)$" ),
                _ => matcher!( "\\.exe$" )
            }
        } if cfg!( target_os = "linux" ) {
            match self.build_target {
                BuildTarget::Lib( _, Profile::Main ) => matcher!( "\\.(a|so)$" ),
                _ => {}
            }
        }

        match self.build_target {
            BuildTarget::Lib( _, Profile::Main ) => {},
            BuildTarget::Lib( ref name, Profile::Test ) => prefix_matcher!( name ),
            BuildTarget::Lib( ref name, Profile::Bench ) => prefix_matcher!( name ),
            BuildTarget::Bin( ref name, profile ) => {
                match profile {
                    Profile::Main => matcher!( format!( "^{}(\\.|$)", name ) ),
                    Profile::Test | Profile::Bench => prefix_matcher!( name )
                }
            },
            BuildTarget::ExampleBin( ref name ) => matcher!( format!( "^{}(\\.|$)", name ) ),
            BuildTarget::IntegrationTest( ref name ) => prefix_matcher!( name ),
            BuildTarget::IntegrationBench( ref name ) => prefix_matcher!( name )
        }

        let crate_root = crate_root.as_ref();
        let output_directory = self.output_directory( crate_root );

        let mut output = Vec::new();
        for entry in output_directory.read_dir().unwrap() {
            let entry = entry.unwrap();
            let filename = entry.file_name();
            let filename = filename.to_string_lossy().into_owned();

            if !matchers.iter().all( |regex| regex.is_match( filename.as_str() ) ) {
                continue;
            }

            output.push( output_directory.join( filename ) );
        }

        output
    }
}