//! A build dependency for Cargo libraries to find libraries in a
//! [Vcpkg](https://github.com/microsoft/vcpkg) tree
//!
//! From a Vcpkg package name
//! this build helper will emit cargo metadata to link it and it's dependencies
//! (excluding system libraries, which it does not determine).
//!
//! The simplest possible usage looks like this :-
//!
//! ```rust,no_run
//! // build.rs
//! vcpkg::find_package("libssh2").unwrap();
//! ```
//!
//! The cargo metadata that is emitted can be changed like this :-
//!
//! ```rust,no_run
//! // build.rs
//! vcpkg::Config::new()
//!     .emit_includes(true)
//!     .find_package("zlib").unwrap();
//! ```
//!
//! If the search was successful all appropriate Cargo metadata will be printed
//! to stdout.
//!
//! # Static vs. dynamic linking
//! ## Linux and Mac
//! At this time, vcpkg has a single triplet on macOS and Linux, which builds
//! static link versions of libraries. This triplet works well with Rust. It is also possible
//! to select a custom triplet using the `VCPKGRS_TRIPLET` environment variable.
//! ## Windows
//! On Windows there are three
//! configurations that are supported for 64-bit builds and another three for 32-bit.
//! The default 64-bit configuration is `x64-windows-static-md` which is a
//! [community supported](https://github.com/microsoft/vcpkg/blob/master/docs/users/triplets.md#community-triplets)
//! configuration that is a good match for Rust - dynamically linking to the C runtime,
//! and statically linking to the packages in vcpkg.
//!
//! Another option is to build a fully static
//! binary using `RUSTFLAGS=-Ctarget-feature=+crt-static`. This will link to libraries built
//! with vcpkg triplet `x64-windows-static`.
//!
//! For dynamic linking, set `VCPKGRS_DYNAMIC=1` in the
//! environment. This will link to libraries built with vcpkg triplet `x64-windows`. If `VCPKGRS_DYNAMIC` is set, `cargo install` will
//! generate dynamically linked binaries, in which case you will have to arrange for
//! dlls from your Vcpkg installation to be available in your path.
//!
//! # Environment variables
//!
//! A number of environment variables are available to globally configure which
//! libraries are selected.
//!
//! * `VCPKG_ROOT` - Set the directory to look in for a vcpkg installation. If
//! it is not set, vcpkg will use the user-wide installation if one has been
//! set up with `vcpkg integrate install`, and check the crate source and target
//! to see if a vcpkg tree has been created by [cargo-vcpkg](https://crates.io/crates/cargo-vcpkg).
//!
//! * `VCPKGRS_TRIPLET` - Use this to override vcpkg-rs' default triplet selection with your own.
//! This is how to select a custom vcpkg triplet.
//!
//! * `VCPKGRS_NO_FOO` - if set, vcpkg-rs will not attempt to find the
//! library named `foo`.
//!
//! * `VCPKGRS_DISABLE` - if set, vcpkg-rs will not attempt to find any libraries.
//!
//! * `VCPKGRS_DYNAMIC` - if set, vcpkg-rs will link to DLL builds of ports.
//! # Related tools
//! ## cargo vcpkg
//! [`cargo vcpkg`](https://crates.io/crates/cargo-vcpkg) can fetch and build a vcpkg installation of
//! required packages from scratch. It merges package requirements specified in the `Cargo.toml` of
//! crates in the dependency tree.  
//! ## vcpkg_cli
//! There is also a rudimentary companion crate, `vcpkg_cli` that allows testing of environment
//! and flag combinations.
//!
//! ```Batchfile
//! C:\src> vcpkg_cli probe -l static mysqlclient
//! Found library mysqlclient
//! Include paths:
//!         C:\src\[..]\vcpkg\installed\x64-windows-static\include
//! Library paths:
//!         C:\src\[..]\vcpkg\installed\x64-windows-static\lib
//! Cargo metadata:
//!         cargo:rustc-link-search=native=C:\src\[..]\vcpkg\installed\x64-windows-static\lib
//!         cargo:rustc-link-lib=static=mysqlclient
//! ```

// The CI will test vcpkg-rs on 1.12 because that is how far back vcpkg-rs 0.2 tries to be
// compatible (was actually 1.10 see #29).  This was originally based on how far back
// rust-openssl's openssl-sys was backward compatible when this crate originally released.
//
// This will likely get bumped by the next major release.
#![allow(deprecated)]
#![allow(warnings)]

#[cfg(test)]
#[macro_use]
extern crate lazy_static;

#[allow(unused_imports)]
use std::ascii::AsciiExt;

use std::collections::BTreeMap;
use std::collections::HashMap;
use std::env;
use std::error;
use std::ffi::OsStr;
use std::fmt;
use std::fs::{self, File};
use std::io::{BufRead, BufReader, Read};
use std::path::{Path, PathBuf};

/// Configuration options for finding packages, setting up the tree and emitting metadata to cargo
#[derive(Default)]
pub struct Config {
    /// should the cargo metadata actually be emitted
    cargo_metadata: bool,

    /// should cargo:include= metadata be emitted (defaults to false)
    emit_includes: bool,

    /// .lib/.a files that must be be found for probing to be considered successful
    required_libs: Vec<String>,

    /// .dlls that must be be found for probing to be considered successful
    required_dlls: Vec<String>,

    /// should DLLs be copied to OUT_DIR?
    copy_dlls: bool,

    /// override VCPKG_ROOT environment variable
    vcpkg_root: Option<PathBuf>,

    target: Option<TargetTriplet>,
}

/// Details of a package that was found
#[derive(Debug)]
pub struct Library {
    /// Paths for the linker to search for static or import libraries
    pub link_paths: Vec<PathBuf>,

    /// Paths to search at runtme to find DLLs
    pub dll_paths: Vec<PathBuf>,

    /// Paths to include files
    pub include_paths: Vec<PathBuf>,

    /// cargo: metadata lines
    pub cargo_metadata: Vec<String>,

    /// libraries found are static
    pub is_static: bool,

    /// DLLs found
    pub found_dlls: Vec<PathBuf>,

    /// static libs or import libs found
    pub found_libs: Vec<PathBuf>,

    /// link name of libraries found, this is useful to emit linker commands
    pub found_names: Vec<String>,

    /// ports that are providing the libraries to link to, in port link order
    pub ports: Vec<String>,

    /// the vcpkg triplet that has been selected
    pub vcpkg_triplet: String,
}

#[derive(Clone)]
struct TargetTriplet {
    triplet: String,
    is_static: bool,
    lib_suffix: String,
    strip_lib_prefix: bool,
}

impl<S: AsRef<str>> From<S> for TargetTriplet {
    fn from(triplet: S) -> TargetTriplet {
        let triplet = triplet.as_ref();
        if triplet.contains("windows") {
            TargetTriplet {
                triplet: triplet.into(),
                is_static: triplet.contains("-static"),
                lib_suffix: "lib".into(),
                strip_lib_prefix: false,
            }
        } else {
            TargetTriplet {
                triplet: triplet.into(),
                is_static: true,
                lib_suffix: "a".into(),
                strip_lib_prefix: true,
            }
        }
    }
}

#[derive(Debug)] // need Display?
pub enum Error {
    /// Aborted because of a `VCPKGRS_NO_*` environment variable.
    ///
    /// Contains the name of the responsible environment variable.
    DisabledByEnv(String),

    /// Aborted because a required environment variable was not set.
    RequiredEnvMissing(String),

    /// On Windows, only MSVC ABI is supported
    NotMSVC,

    /// Can't find a vcpkg tree
    VcpkgNotFound(String),

    /// Library not found in vcpkg tree
    LibNotFound(String),

    /// Could not understand vcpkg installation
    VcpkgInstallation(String),

    #[doc(hidden)]
    __Nonexhaustive,
}

impl error::Error for Error {
    fn description(&self) -> &str {
        match *self {
            Error::DisabledByEnv(_) => "vcpkg-rs requested to be aborted",
            Error::RequiredEnvMissing(_) => "a required env setting is missing",
            Error::NotMSVC => "vcpkg-rs only can only find libraries for MSVC ABI builds",
            Error::VcpkgNotFound(_) => "could not find Vcpkg tree",
            Error::LibNotFound(_) => "could not find library in Vcpkg tree",
            Error::VcpkgInstallation(_) => "could not look up details of packages in vcpkg tree",
            Error::__Nonexhaustive => panic!(),
        }
    }

    fn cause(&self) -> Option<&error::Error> {
        match *self {
            // Error::Command { ref cause, .. } => Some(cause),
            _ => None,
        }
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        match *self {
            Error::DisabledByEnv(ref name) => write!(f, "Aborted because {} is set", name),
            Error::RequiredEnvMissing(ref name) => write!(f, "Aborted because {} is not set", name),
            Error::NotMSVC => write!(
                f,
                "the vcpkg-rs Vcpkg build helper can only find libraries built for the MSVC ABI."
            ),
            Error::VcpkgNotFound(ref detail) => write!(f, "Could not find Vcpkg tree: {}", detail),
            Error::LibNotFound(ref detail) => {
                write!(f, "Could not find library in Vcpkg tree {}", detail)
            }
            Error::VcpkgInstallation(ref detail) => write!(
                f,
                "Could not look up details of packages in vcpkg tree {}",
                detail
            ),
            Error::__Nonexhaustive => panic!(),
        }
    }
}

/// Deprecated in favor of the find_package function
#[doc(hidden)]
pub fn probe_package(name: &str) -> Result<Library, Error> {
    Config::new().probe(name)
}

/// Find the package `package` in a Vcpkg tree.
///
/// Emits cargo metadata to link to libraries provided by the Vcpkg package/port
/// named, and any (non-system) libraries that they depend on.
///
/// This will select the architecture and linkage based on environment
/// variables and build flags as described in the module docs.
pub fn find_package(package: &str) -> Result<Library, Error> {
    Config::new().find_package(package)
}

/// Find the vcpkg root
#[doc(hidden)]
pub fn find_vcpkg_root(cfg: &Config) -> Result<PathBuf, Error> {
    // prefer the setting from the use if there is one
    if let &Some(ref path) = &cfg.vcpkg_root {
        return Ok(path.clone());
    }

    // otherwise, use the setting from the environment
    if let Some(path) = env::var_os("VCPKG_ROOT") {
        return Ok(PathBuf::from(path));
    }

    // see if there is a per-user vcpkg tree that has been integrated into msbuild
    // using `vcpkg integrate install`
    if let Ok(ref local_app_data) = env::var("LOCALAPPDATA") {
        let vcpkg_user_targets_path = Path::new(local_app_data.as_str())
            .join("vcpkg")
            .join("vcpkg.user.targets");

        if let Ok(file) = File::open(vcpkg_user_targets_path.clone()) {
            let file = BufReader::new(&file);

            for line in file.lines() {
                let line = try!(line.map_err(|_| Error::VcpkgNotFound(format!(
                    "Parsing of {} failed.",
                    vcpkg_user_targets_path.to_string_lossy().to_owned()
                ))));
                let mut split = line.split("Project=\"");
                split.next(); // eat anything before Project="
                if let Some(found) = split.next() {
                    // " is illegal in a Windows pathname
                    if let Some(found) = found.split_terminator('"').next() {
                        let mut vcpkg_root = PathBuf::from(found);
                        if !(vcpkg_root.pop()
                            && vcpkg_root.pop()
                            && vcpkg_root.pop()
                            && vcpkg_root.pop())
                        {
                            return Err(Error::VcpkgNotFound(format!(
                                "Could not find vcpkg root above {}",
                                found
                            )));
                        }
                        return Ok(vcpkg_root);
                    }
                }
            }

            // return Err(Error::VcpkgNotFound(format!(
            //     "Project location not found parsing {}.",
            //     vcpkg_user_targets_path.to_string_lossy().to_owned()
            // )));
        }
    }

    // walk up the directory structure and see if it is there
    if let Some(path) = env::var_os("OUT_DIR") {
        // path.ancestors() is supported from Rust 1.28
        let mut path = PathBuf::from(path);
        while path.pop() {
            let mut try_root = path.clone();
            try_root.push("vcpkg");
            try_root.push(".vcpkg-root");
            if try_root.exists() {
                try_root.pop();

                // this could walk up beyond the target directory and find a vcpkg installation
                // that would not have been found by previous versions of vcpkg-rs, so this
                // checks that the vcpkg tree was created by cargo-vcpkg and ignores it if not.
                let mut cv_cfg = try_root.clone();
                cv_cfg.push("downloads");
                cv_cfg.push("cargo-vcpkg.toml");
                if cv_cfg.exists() {
                    return Ok(try_root);
                }
            }
        }
    }

    Err(Error::VcpkgNotFound(
        "No vcpkg installation found. Set the VCPKG_ROOT environment \
             variable or run 'vcpkg integrate install'"
            .to_string(),
    ))
}

fn validate_vcpkg_root(path: &PathBuf) -> Result<(), Error> {
    let mut vcpkg_root_path = path.clone();
    vcpkg_root_path.push(".vcpkg-root");

    if vcpkg_root_path.exists() {
        Ok(())
    } else {
        Err(Error::VcpkgNotFound(format!(
            "Could not find Vcpkg root at {}",
            vcpkg_root_path.to_string_lossy()
        )))
    }
}

fn find_vcpkg_target(cfg: &Config, target_triplet: &TargetTriplet) -> Result<VcpkgTarget, Error> {
    let vcpkg_root = try!(find_vcpkg_root(&cfg));
    try!(validate_vcpkg_root(&vcpkg_root));

    let mut base = vcpkg_root.clone();
    base.push("installed");
    let status_path = base.join("vcpkg");

    base.push(&target_triplet.triplet);

    let lib_path = base.join("lib");
    let bin_path = base.join("bin");
    let include_path = base.join("include");
    let packages_path = vcpkg_root.join("packages");

    Ok(VcpkgTarget {
        lib_path: lib_path,
        bin_path: bin_path,
        include_path: include_path,
        status_path: status_path,
        packages_path: packages_path,
        target_triplet: target_triplet.clone(),
    })
}

/// Parsed knowledge from a .pc file.
#[derive(Debug)]
struct PcFile {
    /// The pkg-config name of this library.
    id: String,
    /// List of libraries found as '-l', translated to a given vcpkg_target. e.g. libbrotlicommon.a
    libs: Vec<String>,
    /// List of pkgconfig dependencies, e.g. PcFile::id.
    deps: Vec<String>,
}
impl PcFile {
    fn parse_pc_file(vcpkg_target: &VcpkgTarget, path: &Path) -> Result<Self, Error> {
        // Extract the pkg-config name.
        let id = try!(path
            .file_stem()
            .ok_or_else(|| Error::VcpkgInstallation(format!(
                "pkg-config file {} has bogus name",
                path.to_string_lossy()
            ))))
        .to_string_lossy();
        // Read through the file and gather what we want.
        let mut file = try!(File::open(path)
            .map_err(|_| Error::VcpkgInstallation(format!("Couldn't open {}", path.display()))));
        let mut pc_file_contents = String::new();

        try!(file
            .read_to_string(&mut pc_file_contents)
            .map_err(|_| Error::VcpkgInstallation(format!("Couldn't read {}", path.display()))));
        PcFile::from_str(&id, &pc_file_contents, &vcpkg_target.target_triplet)
    }
    fn from_str(id: &str, s: &str, target_triplet: &TargetTriplet) -> Result<Self, Error> {
        let mut libs = Vec::new();
        let mut deps = Vec::new();

        for line in s.lines() {
            // We could collect a lot of stuff here, but we only care about Requires and Libs for the moment.
            if line.starts_with("Requires:") {
                let mut requires_args = line
                    .split(":")
                    .skip(1)
                    .next()
                    .unwrap_or("")
                    .split_whitespace()
                    .flat_map(|e| e.split(","))
                    .filter(|s| *s != "");
                while let Some(dep) = requires_args.next() {
                    // Drop any versioning requirements, we only care about library order and rely upon
                    // port dependencies to resolve versioning.
                    if let Some(_) = dep.find(|c| c == '=' || c == '<' || c == '>') {
                        requires_args.next();
                        continue;
                    }
                    deps.push(dep.to_owned());
                }
            } else if line.starts_with("Libs:") {
                let lib_flags = line
                    .split(":")
                    .skip(1)
                    .next()
                    .unwrap_or("")
                    .split_whitespace();
                for lib_flag in lib_flags {
                    if lib_flag.starts_with("-l") {
                        // reconstruct the library name.
                        let lib = format!(
                            "{}{}.{}",
                            if target_triplet.strip_lib_prefix {
                                "lib"
                            } else {
                                ""
                            },
                            lib_flag.trim_left_matches("-l"),
                            target_triplet.lib_suffix
                        );
                        libs.push(lib);
                    }
                }
            }
        }

        Ok(PcFile {
            id: id.to_string(),
            libs: libs,
            deps: deps,
        })
    }
}

/// Collection of PcFile.  Can be built and queried as a set of .pc files.
#[derive(Debug)]
struct PcFiles {
    files: HashMap<String, PcFile>,
}
impl PcFiles {
    fn load_pkgconfig_dir(vcpkg_target: &VcpkgTarget, path: &PathBuf) -> Result<Self, Error> {
        let mut files = HashMap::new();
        for dir_entry in try!(path.read_dir().map_err(|e| {
            Error::VcpkgInstallation(format!(
                "Missing pkgconfig directory {}: {}",
                path.to_string_lossy(),
                e
            ))
        })) {
            let dir_entry = try!(dir_entry.map_err(|e| {
                Error::VcpkgInstallation(format!(
                    "Troubling reading pkgconfig dir {}: {}",
                    path.to_string_lossy(),
                    e
                ))
            }));
            // Only look at .pc files.
            if dir_entry.path().extension() != Some(OsStr::new("pc")) {
                continue;
            }
            let pc_file = try!(PcFile::parse_pc_file(vcpkg_target, &dir_entry.path()));
            files.insert(pc_file.id.to_owned(), pc_file);
        }
        Ok(PcFiles { files: files })
    }
    /// Use the .pc files as a hint to the library sort order.
    fn fix_ordering(&self, mut libs: Vec<String>) -> Vec<String> {
        // Overall heuristic: for each library given as input, identify which PcFile declared it.
        // Then, looking at that PcFile, check its Requires: (deps), and if the pc file for that
        // dep is in our set, check if its libraries are in our set of libs.  If so, move it to the
        // end to ensure it gets linked afterwards.

        // We may need to do this a few times to properly handle the case where A -> (depends on) B
        // -> C -> D and libraries were originally sorted D, C, B, A.  Avoid recursion so we don't
        // have to detect potential cycles.
        for _iter in 0..3 {
            let mut required_lib_order: Vec<String> = Vec::new();
            for lib in &libs {
                required_lib_order.push(lib.to_owned());
                if let Some(pc_file) = self.locate_pc_file_by_lib(lib) {
                    // Consider its requirements:
                    for dep in &pc_file.deps {
                        // Only consider pkgconfig dependencies we know about.
                        if let Some(dep_pc_file) = self.files.get(dep) {
                            // Intra-port library ordering found, pivot any already seen dep_lib to the
                            // end of the list.
                            for dep_lib in &dep_pc_file.libs {
                                if let Some(removed) = remove_item(&mut required_lib_order, dep_lib)
                                {
                                    required_lib_order.push(removed);
                                }
                            }
                        }
                    }
                }
            }
            // We should always end up with the same number of libraries, only their order should
            // change.
            assert_eq!(libs.len(), required_lib_order.len());
            // Termination:
            if required_lib_order == libs {
                // Nothing changed, we're done here.
                return libs;
            }
            libs = required_lib_order;
        }
        println!("cargo:warning=vcpkg gave up trying to resolve pkg-config ordering.");
        libs
    }
    /// Locate which PcFile contains this library, if any.
    fn locate_pc_file_by_lib(&self, lib: &str) -> Option<&PcFile> {
        for (id, pc_file) in &self.files {
            if pc_file.libs.contains(&lib.to_owned()) {
                return Some(pc_file);
            }
        }
        None
    }
}

#[derive(Clone, Debug)]
struct Port {
    // dlls if any
    dlls: Vec<String>,

    // libs (static or import)
    libs: Vec<String>,

    // ports that this port depends on
    deps: Vec<String>,
}

fn load_port_manifest(
    path: &PathBuf,
    port: &str,
    version: &str,
    vcpkg_target: &VcpkgTarget,
) -> Result<(Vec<String>, Vec<String>), Error> {
    let manifest_file = path.join("info").join(format!(
        "{}_{}_{}.list",
        port, version, vcpkg_target.target_triplet.triplet
    ));

    let mut dlls = Vec::new();
    let mut libs = Vec::new();

    let f = try!(
        File::open(&manifest_file).map_err(|_| Error::VcpkgInstallation(format!(
            "Could not open port manifest file {}",
            manifest_file.display()
        )))
    );

    let file = BufReader::new(&f);

    let dll_prefix = Path::new(&vcpkg_target.target_triplet.triplet).join("bin");
    let lib_prefix = Path::new(&vcpkg_target.target_triplet.triplet).join("lib");

    for line in file.lines() {
        let line = line.unwrap();

        let file_path = Path::new(&line);

        if let Ok(dll) = file_path.strip_prefix(&dll_prefix) {
            if dll.extension() == Some(OsStr::new("dll"))
                && dll.components().collect::<Vec<_>>().len() == 1
            {
                // match "mylib.dll" but not "debug/mylib.dll" or "manual_link/mylib.dll"

                dll.to_str().map(|s| dlls.push(s.to_owned()));
            }
        } else if let Ok(lib) = file_path.strip_prefix(&lib_prefix) {
            if lib.extension() == Some(OsStr::new(&vcpkg_target.target_triplet.lib_suffix))
                && lib.components().collect::<Vec<_>>().len() == 1
            {
                if let Some(lib) = vcpkg_target.link_name_for_lib(lib) {
                    libs.push(lib);
                }
            }
        }
    }

    // Load .pc files for hints about intra-port library ordering.
    let pkg_config_prefix = vcpkg_target
        .packages_path
        .join(format!("{}_{}", port, vcpkg_target.target_triplet.triplet))
        .join("lib")
        .join("pkgconfig");
    // Try loading the pc files, if they are present. Not all ports have pkgconfig.
    if let Ok(pc_files) = PcFiles::load_pkgconfig_dir(vcpkg_target, &pkg_config_prefix) {
        // Use the .pc file data to potentially sort the libs to the correct order.
        libs = pc_files.fix_ordering(libs);
    }

    Ok((dlls, libs))
}

// load ports from the status file or one of the incremental updates
fn load_port_file(
    filename: &PathBuf,
    port_info: &mut Vec<BTreeMap<String, String>>,
) -> Result<(), Error> {
    let f = try!(
        File::open(&filename).map_err(|e| Error::VcpkgInstallation(format!(
            "Could not open status file at {}: {}",
            filename.display(),
            e
        )))
    );
    let file = BufReader::new(&f);
    let mut current: BTreeMap<String, String> = BTreeMap::new();
    for line in file.lines() {
        let line = line.unwrap();
        let parts = line.splitn(2, ": ").clone().collect::<Vec<_>>();
        if parts.len() == 2 {
            // a key: value line
            current.insert(parts[0].trim().into(), parts[1].trim().into());
        } else if line.len() == 0 {
            // end of section
            port_info.push(current.clone());
            current.clear();
        } else {
            // ignore all extension lines of the form
            //
            // Description: a package with a
            //   very long description
            //
            // the description key is not used so this is harmless but
            // this will eat extension lines for any multiline key which
            // could become an issue in future
        }
    }

    if !current.is_empty() {
        port_info.push(current);
    }

    Ok(())
}

fn load_ports(target: &VcpkgTarget) -> Result<BTreeMap<String, Port>, Error> {
    let mut ports: BTreeMap<String, Port> = BTreeMap::new();

    let mut port_info: Vec<BTreeMap<String, String>> = Vec::new();

    // load the main status file. It is not an error if this file does not
    // exist. If the only command that has been run in a Vcpkg installation
    // is a single `vcpkg install package` then there will likely be no
    // status file, only incremental updates. This is the typical case when
    // running in a CI environment.
    let status_filename = target.status_path.join("status");
    load_port_file(&status_filename, &mut port_info).ok();

    // load updates to the status file that have yet to be normalized
    let status_update_dir = target.status_path.join("updates");

    let paths = try!(
        fs::read_dir(status_update_dir).map_err(|e| Error::VcpkgInstallation(format!(
            "could not read status file updates dir: {}",
            e
        )))
    );

    // get all of the paths of the update files into a Vec<PathBuf>
    let mut paths = try!(paths
        .map(|rde| rde.map(|de| de.path())) // Result<DirEntry, io::Error> -> Result<PathBuf, io::Error>
        .collect::<Result<Vec<_>, _>>() // collect into Result<Vec<PathBuf>, io::Error>
        .map_err(|e| {
            Error::VcpkgInstallation(format!(
                "could not read status file update filenames: {}",
                e
            ))
        }));

    // Sort the paths and read them. This could be done directly from the iterator if
    // read_dir() guarantees that the files will be read in alpha order but that appears
    // to be unspecified as the underlying operating system calls used are unspecified
    // https://doc.rust-lang.org/nightly/std/fs/fn.read_dir.html#platform-specific-behavior
    paths.sort();
    for path in paths {
        //       println!("Name: {}", path.display());
        try!(load_port_file(&path, &mut port_info));
    }
    //println!("{:#?}", port_info);

    let mut seen_names = BTreeMap::new();
    for current in &port_info {
        // store them by name and arch, clobbering older details
        match (
            current.get("Package"),
            current.get("Architecture"),
            current.get("Feature"),
        ) {
            (Some(pkg), Some(arch), feature) => {
                seen_names.insert((pkg, arch, feature), current);
            }
            _ => {}
        }
    }

    for (&(name, arch, feature), current) in &seen_names {
        if **arch == target.target_triplet.triplet {
            let mut deps = if let Some(deps) = current.get("Depends") {
                deps.split(", ").map(|x| x.to_owned()).collect()
            } else {
                Vec::new()
            };

            if current
                .get("Status")
                .unwrap_or(&String::new())
                .ends_with(" installed")
            {
                match (current.get("Version"), feature) {
                    (Some(version), _) => {
                        // this failing here and bailing out causes everything to fail
                        let lib_info = try!(load_port_manifest(
                            &target.status_path,
                            &name,
                            version,
                            &target
                        ));
                        let port = Port {
                            dlls: lib_info.0,
                            libs: lib_info.1,
                            deps: deps,
                        };

                        ports.insert(name.to_string(), port);
                    }
                    (_, Some(_feature)) => match ports.get_mut(name) {
                        Some(ref mut port) => {
                            port.deps.append(&mut deps);
                        }
                        _ => {
                            println!("found a feature that had no corresponding port :-");
                            println!("current {:+?}", current);
                            continue;
                        }
                    },
                    (_, _) => {
                        println!("didn't know how to deal with status file entry :-");
                        println!("{:+?}", current);
                        continue;
                    }
                }
            }
        }
    }

    Ok(ports)
}

/// paths and triple for the chosen target
struct VcpkgTarget {
    lib_path: PathBuf,
    bin_path: PathBuf,
    include_path: PathBuf,

    // directory containing the status file
    status_path: PathBuf,
    // directory containing the install files per port.
    packages_path: PathBuf,

    // target-specific settings.
    target_triplet: TargetTriplet,
}

impl VcpkgTarget {
    fn link_name_for_lib(&self, filename: &std::path::Path) -> Option<String> {
        if self.target_triplet.strip_lib_prefix {
            filename.to_str().map(|s| s.to_owned())
        // filename
        //     .to_str()
        //     .map(|s| s.trim_left_matches("lib").to_owned())
        } else {
            filename.to_str().map(|s| s.to_owned())
        }
    }
}

impl Config {
    pub fn new() -> Config {
        Config {
            cargo_metadata: true,
            copy_dlls: true,
            ..Default::default()
        }
    }

    fn get_target_triplet(&mut self) -> Result<TargetTriplet, Error> {
        if self.target.is_none() {
            let target = if let Ok(triplet_str) = env::var("VCPKGRS_TRIPLET") {
                triplet_str.into()
            } else {
                try!(msvc_target())
            };
            self.target = Some(target);
        }

        Ok(self.target.as_ref().unwrap().clone())
    }

    /// Find the package `port_name` in a Vcpkg tree.
    ///
    /// Emits cargo metadata to link to libraries provided by the Vcpkg package/port
    /// named, and any (non-system) libraries that they depend on.
    ///
    /// This will select the architecture and linkage based on environment
    /// variables and build flags as described in the module docs, and any configuration
    /// set on the builder.
    pub fn find_package(&mut self, port_name: &str) -> Result<Library, Error> {
        // determine the target type, bailing out if it is not some
        // kind of msvc
        let msvc_target = try!(self.get_target_triplet());

        // bail out if requested to not try at all
        if env::var_os("VCPKGRS_DISABLE").is_some() {
            return Err(Error::DisabledByEnv("VCPKGRS_DISABLE".to_owned()));
        }

        // bail out if requested to not try at all (old)
        if env::var_os("NO_VCPKG").is_some() {
            return Err(Error::DisabledByEnv("NO_VCPKG".to_owned()));
        }

        // bail out if requested to skip this package
        let abort_var_name = format!("VCPKGRS_NO_{}", envify(port_name));
        if env::var_os(&abort_var_name).is_some() {
            return Err(Error::DisabledByEnv(abort_var_name));
        }

        // bail out if requested to skip this package (old)
        let abort_var_name = format!("{}_NO_VCPKG", envify(port_name));
        if env::var_os(&abort_var_name).is_some() {
            return Err(Error::DisabledByEnv(abort_var_name));
        }

        let vcpkg_target = try!(find_vcpkg_target(&self, &msvc_target));
        let mut required_port_order = Vec::new();

        // if no overrides have been selected, then the Vcpkg port name
        // is the the .lib name and the .dll name
        if self.required_libs.is_empty() {
            let ports = try!(load_ports(&vcpkg_target));

            if ports.get(&port_name.to_owned()).is_none() {
                return Err(Error::LibNotFound(format!(
                    "package {} is not installed for vcpkg triplet {}",
                    port_name.to_owned(),
                    vcpkg_target.target_triplet.triplet
                )));
            }

            // the complete set of ports required
            let mut required_ports: BTreeMap<String, Port> = BTreeMap::new();
            // working of ports that we need to include
            //        let mut ports_to_scan: BTreeSet<String> = BTreeSet::new();
            //        ports_to_scan.insert(port_name.to_owned());
            let mut ports_to_scan = vec![port_name.to_owned()]; //: Vec<String> = BTreeSet::new();

            while !ports_to_scan.is_empty() {
                let port_name = ports_to_scan.pop().unwrap();

                if required_ports.contains_key(&port_name) {
                    continue;
                }

                if let Some(port) = ports.get(&port_name) {
                    for dep in &port.deps {
                        ports_to_scan.push(dep.clone());
                    }
                    required_ports.insert(port_name.clone(), (*port).clone());
                    remove_item(&mut required_port_order, &port_name);
                    required_port_order.push(port_name);
                } else {
                    // what?
                }
            }

            // for port in ports {
            //     println!("port {:?}", port);
            // }
            // println!("== Looking for port {}", port_name);
            // for port in &required_port_order {
            //     println!("ordered required port {:?}", port);
            // }
            // println!("=============================");
            // for port in &required_ports {
            //     println!("required port {:?}", port);
            // }

            // if no overrides have been selected, then the Vcpkg port name
            // is the the .lib name and the .dll name
            if self.required_libs.is_empty() {
                for port_name in &required_port_order {
                    let port = required_ports.get(port_name).unwrap();
                    self.required_libs.extend(port.libs.iter().map(|s| {
                        Path::new(&s)
                            .file_stem()
                            .unwrap()
                            .to_string_lossy()
                            .into_owned()
                    }));
                    self.required_dlls
                        .extend(port.dlls.iter().cloned().map(|s| {
                            Path::new(&s)
                                .file_stem()
                                .unwrap()
                                .to_string_lossy()
                                .into_owned()
                        }));
                }
            }
        }
        // require explicit opt-in before using dynamically linked
        // variants, otherwise cargo install of various things will
        // stop working if Vcpkg is installed.
        if !vcpkg_target.target_triplet.is_static && !env::var_os("VCPKGRS_DYNAMIC").is_some() {
            return Err(Error::RequiredEnvMissing("VCPKGRS_DYNAMIC".to_owned()));
        }

        let mut lib = Library::new(
            vcpkg_target.target_triplet.is_static,
            &vcpkg_target.target_triplet.triplet,
        );

        if self.emit_includes {
            lib.cargo_metadata.push(format!(
                "cargo:include={}",
                vcpkg_target.include_path.display()
            ));
        }
        lib.include_paths.push(vcpkg_target.include_path.clone());

        lib.cargo_metadata.push(format!(
            "cargo:rustc-link-search=native={}",
            vcpkg_target
                .lib_path
                .to_str()
                .expect("failed to convert string type")
        ));
        lib.link_paths.push(vcpkg_target.lib_path.clone());
        if !vcpkg_target.target_triplet.is_static {
            lib.cargo_metadata.push(format!(
                "cargo:rustc-link-search=native={}",
                vcpkg_target
                    .bin_path
                    .to_str()
                    .expect("failed to convert string type")
            ));
            // this path is dropped by recent versions of cargo hence the copies to OUT_DIR below
            lib.dll_paths.push(vcpkg_target.bin_path.clone());
        }

        lib.ports = required_port_order;

        try!(self.emit_libs(&mut lib, &vcpkg_target));

        if self.copy_dlls {
            try!(self.do_dll_copy(&mut lib));
        }

        if self.cargo_metadata {
            for line in &lib.cargo_metadata {
                println!("{}", line);
            }
        }
        Ok(lib)
    }

    /// Define whether metadata should be emitted for cargo allowing it to
    /// automatically link the binary. Defaults to `true`.
    pub fn cargo_metadata(&mut self, cargo_metadata: bool) -> &mut Config {
        self.cargo_metadata = cargo_metadata;
        self
    }

    /// Define cargo:include= metadata should be emitted. Defaults to `false`.
    pub fn emit_includes(&mut self, emit_includes: bool) -> &mut Config {
        self.emit_includes = emit_includes;
        self
    }

    /// Should DLLs be copied to OUT_DIR?
    /// Defaults to `true`.
    pub fn copy_dlls(&mut self, copy_dlls: bool) -> &mut Config {
        self.copy_dlls = copy_dlls;
        self
    }

    /// Define which path to use as vcpkg root overriding the VCPKG_ROOT environment variable
    /// Default to `None`, which means use VCPKG_ROOT or try to find out automatically
    pub fn vcpkg_root(&mut self, vcpkg_root: PathBuf) -> &mut Config {
        self.vcpkg_root = Some(vcpkg_root);
        self
    }

    /// Specify target triplet. When triplet is not specified, inferred triplet from rust target is used.
    ///
    /// Specifying a triplet using `target_triplet` will override the default triplet for this crate. This
    /// cannot change the choice of triplet made by other crates, so a safer choice will be to set
    /// `VCPKGRS_TRIPLET` in the environment which will allow all crates to use a consistent set of
    /// external dependencies.
    pub fn target_triplet<S: AsRef<str>>(&mut self, triplet: S) -> &mut Config {
        self.target = Some(triplet.into());
        self
    }

    /// Find the library `port_name` in a Vcpkg tree.
    ///
    /// This will use all configuration previously set to select the
    /// architecture and linkage.
    /// Deprecated in favor of the find_package function
    #[doc(hidden)]
    pub fn probe(&mut self, port_name: &str) -> Result<Library, Error> {
        // determine the target type, bailing out if it is not some
        // kind of msvc
        let msvc_target = try!(self.get_target_triplet());

        // bail out if requested to not try at all
        if env::var_os("VCPKGRS_DISABLE").is_some() {
            return Err(Error::DisabledByEnv("VCPKGRS_DISABLE".to_owned()));
        }

        // bail out if requested to not try at all (old)
        if env::var_os("NO_VCPKG").is_some() {
            return Err(Error::DisabledByEnv("NO_VCPKG".to_owned()));
        }

        // bail out if requested to skip this package
        let abort_var_name = format!("VCPKGRS_NO_{}", envify(port_name));
        if env::var_os(&abort_var_name).is_some() {
            return Err(Error::DisabledByEnv(abort_var_name));
        }

        // bail out if requested to skip this package (old)
        let abort_var_name = format!("{}_NO_VCPKG", envify(port_name));
        if env::var_os(&abort_var_name).is_some() {
            return Err(Error::DisabledByEnv(abort_var_name));
        }

        // if no overrides have been selected, then the Vcpkg port name
        // is the the .lib name and the .dll name
        if self.required_libs.is_empty() {
            self.required_libs.push(port_name.to_owned());
            self.required_dlls.push(port_name.to_owned());
        }

        let vcpkg_target = try!(find_vcpkg_target(&self, &msvc_target));

        // require explicit opt-in before using dynamically linked
        // variants, otherwise cargo install of various things will
        // stop working if Vcpkg is installed.
        if !vcpkg_target.target_triplet.is_static && !env::var_os("VCPKGRS_DYNAMIC").is_some() {
            return Err(Error::RequiredEnvMissing("VCPKGRS_DYNAMIC".to_owned()));
        }

        let mut lib = Library::new(
            vcpkg_target.target_triplet.is_static,
            &vcpkg_target.target_triplet.triplet,
        );

        if self.emit_includes {
            lib.cargo_metadata.push(format!(
                "cargo:include={}",
                vcpkg_target.include_path.display()
            ));
        }
        lib.include_paths.push(vcpkg_target.include_path.clone());

        lib.cargo_metadata.push(format!(
            "cargo:rustc-link-search=native={}",
            vcpkg_target
                .lib_path
                .to_str()
                .expect("failed to convert string type")
        ));
        lib.link_paths.push(vcpkg_target.lib_path.clone());
        if !vcpkg_target.target_triplet.is_static {
            lib.cargo_metadata.push(format!(
                "cargo:rustc-link-search=native={}",
                vcpkg_target
                    .bin_path
                    .to_str()
                    .expect("failed to convert string type")
            ));
            // this path is dropped by recent versions of cargo hence the copies to OUT_DIR below
            lib.dll_paths.push(vcpkg_target.bin_path.clone());
        }

        try!(self.emit_libs(&mut lib, &vcpkg_target));

        if self.copy_dlls {
            try!(self.do_dll_copy(&mut lib));
        }

        if self.cargo_metadata {
            for line in &lib.cargo_metadata {
                println!("{}", line);
            }
        }
        Ok(lib)
    }

    fn emit_libs(&mut self, lib: &mut Library, vcpkg_target: &VcpkgTarget) -> Result<(), Error> {
        for required_lib in &self.required_libs {
            // this could use static-nobundle= for static libraries but it is apparently
            // not necessary to make the distinction for windows-msvc.

            let link_name = match vcpkg_target.target_triplet.strip_lib_prefix {
                true => required_lib.trim_left_matches("lib"),
                false => required_lib,
            };

            lib.cargo_metadata
                .push(format!("cargo:rustc-link-lib={}", link_name));

            lib.found_names.push(String::from(link_name));

            // verify that the library exists
            let mut lib_location = vcpkg_target.lib_path.clone();
            lib_location.push(required_lib.clone() + "." + &vcpkg_target.target_triplet.lib_suffix);

            if !lib_location.exists() {
                return Err(Error::LibNotFound(lib_location.display().to_string()));
            }
            lib.found_libs.push(lib_location);
        }

        if !vcpkg_target.target_triplet.is_static {
            for required_dll in &self.required_dlls {
                let mut dll_location = vcpkg_target.bin_path.clone();
                dll_location.push(required_dll.clone() + ".dll");

                // verify that the DLL exists
                if !dll_location.exists() {
                    return Err(Error::LibNotFound(dll_location.display().to_string()));
                }
                lib.found_dlls.push(dll_location);
            }
        }

        Ok(())
    }

    fn do_dll_copy(&mut self, lib: &mut Library) -> Result<(), Error> {
        if let Some(target_dir) = env::var_os("OUT_DIR") {
            if !lib.found_dlls.is_empty() {
                for file in &lib.found_dlls {
                    let mut dest_path = Path::new(target_dir.as_os_str()).to_path_buf();
                    dest_path.push(Path::new(file.file_name().unwrap()));
                    try!(
                        fs::copy(file, &dest_path).map_err(|_| Error::LibNotFound(format!(
                            "Can't copy file {} to {}",
                            file.to_string_lossy(),
                            dest_path.to_string_lossy()
                        )))
                    );
                    println!(
                        "vcpkg build helper copied {} to {}",
                        file.to_string_lossy(),
                        dest_path.to_string_lossy()
                    );
                }
                lib.cargo_metadata.push(format!(
                    "cargo:rustc-link-search=native={}",
                    env::var("OUT_DIR").unwrap()
                ));
                // work around https://github.com/rust-lang/cargo/issues/3957
                lib.cargo_metadata.push(format!(
                    "cargo:rustc-link-search={}",
                    env::var("OUT_DIR").unwrap()
                ));
            }
        } else {
            return Err(Error::LibNotFound("Unable to get OUT_DIR".to_owned()));
        }
        Ok(())
    }

    /// Override the name of the library to look for if it differs from the package name.
    ///
    /// It should not be necessary to use `lib_name` anymore. Calling `find_package` with a package name
    /// will result in the correct library names.
    /// This may be called more than once if multiple libs are required.
    /// All libs must be found for the probe to succeed. `.probe()` must
    /// be run with a different configuration to look for libraries under one of several names.
    /// `.libname("ssleay32")` will look for ssleay32.lib and also ssleay32.dll if
    /// dynamic linking is selected.
    pub fn lib_name(&mut self, lib_stem: &str) -> &mut Config {
        self.required_libs.push(lib_stem.to_owned());
        self.required_dlls.push(lib_stem.to_owned());
        self
    }

    /// Override the name of the library to look for if it differs from the package name.
    ///
    /// It should not be necessary to use `lib_names` anymore. Calling `find_package` with a package name
    /// will result in the correct library names.
    /// This may be called more than once if multiple libs are required.
    /// All libs must be found for the probe to succeed. `.probe()` must
    /// be run with a different configuration to look for libraries under one of several names.
    /// `.lib_names("libcurl_imp","curl")` will look for libcurl_imp.lib and also curl.dll if
    /// dynamic linking is selected.
    pub fn lib_names(&mut self, lib_stem: &str, dll_stem: &str) -> &mut Config {
        self.required_libs.push(lib_stem.to_owned());
        self.required_dlls.push(dll_stem.to_owned());
        self
    }
}

fn remove_item(cont: &mut Vec<String>, item: &String) -> Option<String> {
    match cont.iter().position(|x| *x == *item) {
        Some(pos) => Some(cont.remove(pos)),
        None => None,
    }
}

impl Library {
    fn new(is_static: bool, vcpkg_triplet: &str) -> Library {
        Library {
            link_paths: Vec::new(),
            dll_paths: Vec::new(),
            include_paths: Vec::new(),
            cargo_metadata: Vec::new(),
            is_static: is_static,
            found_dlls: Vec::new(),
            found_libs: Vec::new(),
            found_names: Vec::new(),
            ports: Vec::new(),
            vcpkg_triplet: vcpkg_triplet.to_string(),
        }
    }
}

fn envify(name: &str) -> String {
    name.chars()
        .map(|c| c.to_ascii_uppercase())
        .map(|c| if c == '-' { '_' } else { c })
        .collect()
}

fn msvc_target() -> Result<TargetTriplet, Error> {
    let is_definitely_dynamic = env::var("VCPKGRS_DYNAMIC").is_ok();
    let target = env::var("TARGET").unwrap_or(String::new());
    let is_static = env::var("CARGO_CFG_TARGET_FEATURE")
        .unwrap_or(String::new()) // rustc 1.10
        .contains("crt-static");
    if target == "x86_64-apple-darwin" {
        Ok(TargetTriplet {
            triplet: "x64-osx".into(),
            is_static: true,
            lib_suffix: "a".into(),
            strip_lib_prefix: true,
        })
    } else if target == "aarch64-apple-darwin" {
        Ok(TargetTriplet {
            triplet: "arm64-osx".into(),
            is_static: true,
            lib_suffix: "a".into(),
            strip_lib_prefix: true,
        })
    } else if target == "x86_64-unknown-linux-gnu" {
        Ok(TargetTriplet {
            triplet: "x64-linux".into(),
            is_static: true,
            lib_suffix: "a".into(),
            strip_lib_prefix: true,
        })
    } else if target == "aarch64-apple-ios" {
        Ok(TargetTriplet {
            triplet: "arm64-ios".into(),
            is_static: true,
            lib_suffix: "a".into(),
            strip_lib_prefix: true,
        })
    } else if !target.contains("-pc-windows-msvc") {
        Err(Error::NotMSVC)
    } else if target.starts_with("x86_64-") {
        if is_static {
            Ok(TargetTriplet {
                triplet: "x64-windows-static".into(),
                is_static: true,
                lib_suffix: "lib".into(),
                strip_lib_prefix: false,
            })
        } else if is_definitely_dynamic {
            Ok(TargetTriplet {
                triplet: "x64-windows".into(),
                is_static: false,
                lib_suffix: "lib".into(),
                strip_lib_prefix: false,
            })
        } else {
            Ok(TargetTriplet {
                triplet: "x64-windows-static-md".into(),
                is_static: true,
                lib_suffix: "lib".into(),
                strip_lib_prefix: false,
            })
        }
    } else if target.starts_with("aarch64") {
        if is_static {
            Ok(TargetTriplet {
                triplet: "arm64-windows-static".into(),
                is_static: true,
                lib_suffix: "lib".into(),
                strip_lib_prefix: false,
            })
        } else if is_definitely_dynamic {
            Ok(TargetTriplet {
                triplet: "arm64-windows".into(),
                is_static: false,
                lib_suffix: "lib".into(),
                strip_lib_prefix: false,
            })
        } else {
            Ok(TargetTriplet {
                triplet: "arm64-windows-static-md".into(),
                is_static: true,
                lib_suffix: "lib".into(),
                strip_lib_prefix: false,
            })
        }
    } else {
        // everything else is x86
        if is_static {
            Ok(TargetTriplet {
                triplet: "x86-windows-static".into(),
                is_static: true,
                lib_suffix: "lib".into(),
                strip_lib_prefix: false,
            })
        } else if is_definitely_dynamic {
            Ok(TargetTriplet {
                triplet: "x86-windows".into(),
                is_static: false,
                lib_suffix: "lib".into(),
                strip_lib_prefix: false,
            })
        } else {
            Ok(TargetTriplet {
                triplet: "x86-windows-static-md".into(),
                is_static: true,
                lib_suffix: "lib".into(),
                strip_lib_prefix: false,
            })
        }
    }
}

#[cfg(test)]
mod tests {

    extern crate tempdir;

    use super::*;
    use std::env;
    use std::sync::Mutex;

    lazy_static! {
        static ref LOCK: Mutex<()> = Mutex::new(());
    }

    #[test]
    fn do_nothing_for_unsupported_target() {
        let _g = LOCK.lock();
        env::set_var("VCPKG_ROOT", "/");
        env::set_var("TARGET", "x86_64-pc-windows-gnu");
        assert!(match ::probe_package("foo") {
            Err(Error::NotMSVC) => true,
            _ => false,
        });

        env::set_var("TARGET", "x86_64-pc-windows-gnu");
        assert_eq!(env::var("TARGET"), Ok("x86_64-pc-windows-gnu".to_string()));
        assert!(match ::probe_package("foo") {
            Err(Error::NotMSVC) => true,
            _ => false,
        });
        env::remove_var("TARGET");
        env::remove_var("VCPKG_ROOT");
    }

    #[test]
    fn do_nothing_for_bailout_variables_set() {
        let _g = LOCK.lock();
        env::set_var("VCPKG_ROOT", "/");
        env::set_var("TARGET", "x86_64-pc-windows-msvc");

        for &var in &[
            "VCPKGRS_DISABLE",
            "VCPKGRS_NO_FOO",
            "FOO_NO_VCPKG",
            "NO_VCPKG",
        ] {
            env::set_var(var, "1");
            assert!(match ::probe_package("foo") {
                Err(Error::DisabledByEnv(ref v)) if v == var => true,
                _ => false,
            });
            env::remove_var(var);
        }
        env::remove_var("TARGET");
        env::remove_var("VCPKG_ROOT");
    }

    // these tests are good but are leaning on a real vcpkg installation

    // #[test]
    // fn default_build_refuses_dynamic() {
    //     let _g = LOCK.lock();
    //     clean_env();
    //     env::set_var("VCPKG_ROOT", vcpkg_test_tree_loc("no-status"));
    //     env::set_var("TARGET", "x86_64-pc-windows-msvc");
    //     println!("Result is {:?}", ::find_package("libmysql"));
    //     assert!(match ::find_package("libmysql") {
    //         Err(Error::RequiredEnvMissing(ref v)) if v == "VCPKGRS_DYNAMIC" => true,
    //         _ => false,
    //     });
    //     clean_env();
    // }

    #[test]
    fn static_build_finds_lib() {
        let _g = LOCK.lock();
        clean_env();
        env::set_var("VCPKG_ROOT", vcpkg_test_tree_loc("normalized"));
        env::set_var("TARGET", "x86_64-pc-windows-msvc");
        let tmp_dir = tempdir::TempDir::new("vcpkg_tests").unwrap();
        env::set_var("OUT_DIR", tmp_dir.path());

        // CARGO_CFG_TARGET_FEATURE is set in response to
        // RUSTFLAGS=-Ctarget-feature=+crt-static. It would
        //  be nice to test that also.
        env::set_var("CARGO_CFG_TARGET_FEATURE", "crt-static");
        println!("Result is {:?}", ::find_package("libmysql"));
        assert!(match ::find_package("libmysql") {
            Ok(_) => true,
            _ => false,
        });
        clean_env();
    }

    #[test]
    fn dynamic_build_finds_lib() {
        let _g = LOCK.lock();
        clean_env();
        env::set_var("VCPKG_ROOT", vcpkg_test_tree_loc("no-status"));
        env::set_var("TARGET", "x86_64-pc-windows-msvc");
        env::set_var("VCPKGRS_DYNAMIC", "1");
        let tmp_dir = tempdir::TempDir::new("vcpkg_tests").unwrap();
        env::set_var("OUT_DIR", tmp_dir.path());

        println!("Result is {:?}", ::find_package("libmysql"));
        assert!(match ::find_package("libmysql") {
            Ok(_) => true,
            _ => false,
        });
        clean_env();
    }

    #[test]
    fn handle_multiline_description() {
        let _g = LOCK.lock();
        clean_env();
        env::set_var("VCPKG_ROOT", vcpkg_test_tree_loc("multiline-description"));
        env::set_var("TARGET", "i686-pc-windows-msvc");
        env::set_var("VCPKGRS_DYNAMIC", "1");
        let tmp_dir = tempdir::TempDir::new("vcpkg_tests").unwrap();
        env::set_var("OUT_DIR", tmp_dir.path());

        println!("Result is {:?}", ::find_package("graphite2"));
        assert!(match ::find_package("graphite2") {
            Ok(_) => true,
            _ => false,
        });
        clean_env();
    }

    #[test]
    fn link_libs_required_by_optional_features() {
        let _g = LOCK.lock();
        clean_env();
        env::set_var("VCPKG_ROOT", vcpkg_test_tree_loc("normalized"));
        env::set_var("TARGET", "i686-pc-windows-msvc");
        env::set_var("VCPKGRS_DYNAMIC", "1");
        let tmp_dir = tempdir::TempDir::new("vcpkg_tests").unwrap();
        env::set_var("OUT_DIR", tmp_dir.path());

        println!("Result is {:?}", ::find_package("harfbuzz"));
        assert!(match ::find_package("harfbuzz") {
            Ok(lib) => lib
                .cargo_metadata
                .iter()
                .find(|&x| x == "cargo:rustc-link-lib=icuuc")
                .is_some(),
            _ => false,
        });
        clean_env();
    }

    #[test]
    fn link_lib_name_is_correct() {
        let _g = LOCK.lock();

        for target in &[
            "x86_64-apple-darwin",
            "i686-pc-windows-msvc",
            //      "x86_64-pc-windows-msvc",
            //    "x86_64-unknown-linux-gnu",
        ] {
            clean_env();
            env::set_var("VCPKG_ROOT", vcpkg_test_tree_loc("normalized"));
            env::set_var("TARGET", target);
            env::set_var("VCPKGRS_DYNAMIC", "1");
            let tmp_dir = tempdir::TempDir::new("vcpkg_tests").unwrap();
            env::set_var("OUT_DIR", tmp_dir.path());

            println!("Result is {:?}", ::find_package("harfbuzz"));
            assert!(match ::find_package("harfbuzz") {
                Ok(lib) => lib
                    .cargo_metadata
                    .iter()
                    .find(|&x| x == "cargo:rustc-link-lib=harfbuzz")
                    .is_some(),
                _ => false,
            });
            clean_env();
        }
    }

    #[test]
    fn link_dependencies_after_port() {
        let _g = LOCK.lock();
        clean_env();
        env::set_var("VCPKG_ROOT", vcpkg_test_tree_loc("normalized"));
        env::set_var("TARGET", "i686-pc-windows-msvc");
        env::set_var("VCPKGRS_DYNAMIC", "1");
        let tmp_dir = tempdir::TempDir::new("vcpkg_tests").unwrap();
        env::set_var("OUT_DIR", tmp_dir.path());

        let lib = ::find_package("harfbuzz").unwrap();

        check_before(&lib, "freetype", "zlib");
        check_before(&lib, "freetype", "bzip2");
        check_before(&lib, "freetype", "libpng");
        check_before(&lib, "harfbuzz", "freetype");
        check_before(&lib, "harfbuzz", "ragel");
        check_before(&lib, "libpng", "zlib");

        clean_env();

        fn check_before(lib: &Library, earlier: &str, later: &str) {
            match (
                lib.ports.iter().position(|x| *x == *earlier),
                lib.ports.iter().position(|x| *x == *later),
            ) {
                (Some(earlier_pos), Some(later_pos)) if earlier_pos < later_pos => {
                    // ok
                }
                _ => {
                    println!(
                        "earlier: {}, later: {}\nLibrary found: {:#?}",
                        earlier, later, lib
                    );
                    panic!();
                }
            }
        }
    }

    #[test]
    fn custom_target_triplet_in_config() {
        let _g = LOCK.lock();

        clean_env();
        env::set_var("VCPKG_ROOT", vcpkg_test_tree_loc("normalized"));
        env::set_var("TARGET", "aarch64-apple-ios");
        env::set_var("VCPKGRS_DYNAMIC", "1");
        let tmp_dir = tempdir::TempDir::new("vcpkg_tests").unwrap();
        env::set_var("OUT_DIR", tmp_dir.path());

        let harfbuzz = ::Config::new()
            // For the sake of testing, force this build to try to
            // link to the arm64-osx libraries in preference to the
            // default of arm64-ios.
            .target_triplet("x64-osx")
            .find_package("harfbuzz");
        println!("Result with specifying target triplet is {:?}", &harfbuzz);
        let harfbuzz = harfbuzz.unwrap();
        assert_eq!(harfbuzz.vcpkg_triplet, "x64-osx");
        clean_env();
    }

    #[test]
    fn custom_target_triplet_by_env_no_default() {
        let _g = LOCK.lock();

        clean_env();
        env::set_var("VCPKG_ROOT", vcpkg_test_tree_loc("normalized"));
        env::set_var("TARGET", "aarch64-apple-doesnotexist");
        env::set_var("VCPKGRS_DYNAMIC", "1");
        let tmp_dir = tempdir::TempDir::new("vcpkg_tests").unwrap();
        env::set_var("OUT_DIR", tmp_dir.path());

        let harfbuzz = ::find_package("harfbuzz");
        println!("Result with inference is {:?}", &harfbuzz);
        assert!(harfbuzz.is_err());

        env::set_var("VCPKGRS_TRIPLET", "x64-osx");
        let harfbuzz = ::find_package("harfbuzz").unwrap();
        println!("Result with setting VCPKGRS_TRIPLET is {:?}", &harfbuzz);
        assert_eq!(harfbuzz.vcpkg_triplet, "x64-osx");
        clean_env();
    }

    #[test]
    fn custom_target_triplet_by_env_with_default() {
        let _g = LOCK.lock();

        clean_env();
        env::set_var("VCPKG_ROOT", vcpkg_test_tree_loc("normalized"));
        env::set_var("TARGET", "aarch64-apple-ios");
        env::set_var("VCPKGRS_DYNAMIC", "1");
        let tmp_dir = tempdir::TempDir::new("vcpkg_tests").unwrap();
        env::set_var("OUT_DIR", tmp_dir.path());

        let harfbuzz = ::find_package("harfbuzz").unwrap();
        println!("Result with inference is {:?}", &harfbuzz);
        assert_eq!(harfbuzz.vcpkg_triplet, "arm64-ios");

        env::set_var("VCPKGRS_TRIPLET", "x64-osx");
        let harfbuzz = ::find_package("harfbuzz").unwrap();
        println!("Result with setting VCPKGRS_TRIPLET is {:?}", &harfbuzz);
        assert_eq!(harfbuzz.vcpkg_triplet, "x64-osx");
        clean_env();
    }

    // #[test]
    // fn dynamic_build_package_specific_bailout() {
    //     clean_env();
    //     env::set_var("VCPKG_ROOT", vcpkg_test_tree_loc("no-status"));
    //     env::set_var("TARGET", "x86_64-pc-windows-msvc");
    //     env::set_var("VCPKGRS_DYNAMIC", "1");
    //     env::set_var("VCPKGRS_NO_LIBMYSQL", "1");

    //     println!("Result is {:?}", ::find_package("libmysql"));
    //     assert!(match ::find_package("libmysql") {
    //         Err(Error::DisabledByEnv(ref v)) if v == "VCPKGRS_NO_LIBMYSQL" => true,
    //         _ => false,
    //     });
    //     clean_env();
    // }

    // #[test]
    // fn dynamic_build_global_bailout() {
    //     clean_env();
    //     env::set_var("VCPKG_ROOT", vcpkg_test_tree_loc("no-status"));
    //     env::set_var("TARGET", "x86_64-pc-windows-msvc");
    //     env::set_var("VCPKGRS_DYNAMIC", "1");
    //     env::set_var("VCPKGRS_DISABLE", "1");

    //     println!("Result is {:?}", ::find_package("libmysql"));
    //     assert!(match ::find_package("libmysql") {
    //         Err(Error::DisabledByEnv(ref v)) if v == "VCPKGRS_DISABLE" => true,
    //         _ => false,
    //     });
    //     clean_env();
    // }

    #[test]
    fn pc_files_reordering() {
        let _g = LOCK.lock();
        clean_env();
        env::set_var("VCPKG_ROOT", vcpkg_test_tree_loc("normalized"));
        env::set_var("TARGET", "x86_64-unknown-linux-gnu");
        // env::set_var("VCPKGRS_DYNAMIC", "1");
        let tmp_dir = tempdir::TempDir::new("vcpkg_tests").unwrap();
        env::set_var("OUT_DIR", tmp_dir.path());

        let target_triplet = msvc_target().unwrap();

        // The brotli use-case.
        {
            let mut pc_files = PcFiles {
                files: HashMap::new(),
            };
            pc_files.files.insert(
                "libbrotlicommon".to_owned(),
                PcFile::from_str(
                    "libbrotlicommon",
                    "Libs: -lbrotlicommon-static\nRequires:",
                    &target_triplet,
                )
                .unwrap(),
            );
            pc_files.files.insert(
                "libbrotlienc".to_owned(),
                PcFile::from_str(
                    "libbrotlienc",
                    "Libs: -lbrotlienc-static\nRequires: libbrotlicommon",
                    &target_triplet,
                )
                .unwrap(),
            );
            pc_files.files.insert(
                "libbrotlidec".to_owned(),
                PcFile::from_str(
                    "brotlidec",
                    "Libs: -lbrotlidec-static\nRequires: libbrotlicommon >= 1.0.9",
                    &target_triplet,
                )
                .unwrap(),
            );
            // Note that the input is alphabetically sorted.
            let input_libs = vec![
                "libbrotlicommon-static.a".to_owned(),
                "libbrotlidec-static.a".to_owned(),
                "libbrotlienc-static.a".to_owned(),
            ];
            let output_libs = pc_files.fix_ordering(input_libs);
            assert_eq!(output_libs[0], "libbrotlidec-static.a");
            assert_eq!(output_libs[1], "libbrotlienc-static.a");
            assert_eq!(output_libs[2], "libbrotlicommon-static.a");
        }

        // Concoct elaborate dependency graph, try all variations of input sort.
        // Throw some (ignored) version dependencies as well as extra libs not represented in the
        // pc_files dataset.
        {
            let mut pc_files = PcFiles {
                files: HashMap::new(),
            };
            pc_files.files.insert(
                "libA".to_owned(),
                PcFile::from_str(
                    "libA",
                    "Libs: -lA\n\
                     Requires:",
                    &target_triplet,
                )
                .unwrap(),
            );
            pc_files.files.insert(
                "libB".to_owned(),
                PcFile::from_str(
                    "libB",
                    "Libs:  -lB -lm -pthread \n\
                     Requires: libA",
                    &target_triplet,
                )
                .unwrap(),
            );
            pc_files.files.insert(
                "libC".to_owned(),
                PcFile::from_str(
                    "libC",
                    "Libs: -lC -L${libdir}\n\
                     Requires: libB <=1.0 , libmysql-client = 0.9, ",
                    &target_triplet,
                )
                .unwrap(),
            );
            pc_files.files.insert(
                "libD".to_owned(),
                PcFile::from_str(
                    "libD",
                    "Libs: -Lpath/to/libs -Rplugins -lD\n\
                     Requires: libpostgres libC",
                    &target_triplet,
                )
                .unwrap(),
            );
            let permutations: Vec<Vec<&str>> = vec![
                vec!["libA.a", "libB.a", "libC.a", "libD.a"],
                vec!["libA.a", "libB.a", "libD.a", "libC.a"],
                vec!["libA.a", "libC.a", "libB.a", "libD.a"],
                vec!["libA.a", "libC.a", "libD.a", "libB.a"],
                vec!["libA.a", "libD.a", "libB.a", "libC.a"],
                vec!["libA.a", "libD.a", "libC.a", "libB.a"],
                //
                vec!["libB.a", "libA.a", "libC.a", "libD.a"],
                vec!["libB.a", "libA.a", "libD.a", "libC.a"],
                vec!["libB.a", "libC.a", "libA.a", "libD.a"],
                vec!["libB.a", "libC.a", "libD.a", "libA.a"],
                vec!["libB.a", "libD.a", "libA.a", "libC.a"],
                vec!["libB.a", "libD.a", "libC.a", "libA.a"],
                //
                vec!["libC.a", "libA.a", "libB.a", "libD.a"],
                vec!["libC.a", "libA.a", "libD.a", "libB.a"],
                vec!["libC.a", "libB.a", "libA.a", "libD.a"],
                vec!["libC.a", "libB.a", "libD.a", "libA.a"],
                vec!["libC.a", "libD.a", "libA.a", "libB.a"],
                vec!["libC.a", "libD.a", "libB.a", "libA.a"],
                //
                vec!["libD.a", "libA.a", "libB.a", "libC.a"],
                vec!["libD.a", "libA.a", "libC.a", "libB.a"],
                vec!["libD.a", "libB.a", "libA.a", "libC.a"],
                vec!["libD.a", "libB.a", "libC.a", "libA.a"],
                vec!["libD.a", "libC.a", "libA.a", "libB.a"],
                vec!["libD.a", "libC.a", "libB.a", "libA.a"],
            ];
            for permutation in permutations {
                let input_libs = vec![
                    permutation[0].to_owned(),
                    permutation[1].to_owned(),
                    permutation[2].to_owned(),
                    permutation[3].to_owned(),
                ];
                let output_libs = pc_files.fix_ordering(input_libs);
                assert_eq!(output_libs.len(), 4);
                assert_eq!(output_libs[0], "libD.a");
                assert_eq!(output_libs[1], "libC.a");
                assert_eq!(output_libs[2], "libB.a");
                assert_eq!(output_libs[3], "libA.a");
            }
        }

        // Test parsing of a couple different Requires: lines.
        {
            let pc_file = PcFile::from_str(
                "test",
                "Libs: -ltest\n\
                 Requires: cairo libpng",
                &target_triplet,
            )
            .unwrap();
            assert_eq!(pc_file.deps, vec!["cairo", "libpng"]);
            let pc_file = PcFile::from_str(
                "test",
                "Libs: -ltest\n\
                 Requires: cairo xcb >= 1.6 xcb-render >= 1.6",
                &target_triplet,
            )
            .unwrap();
            assert_eq!(pc_file.deps, vec!["cairo", "xcb", "xcb-render"]);
            let pc_file = PcFile::from_str(
                "test",
                "Libs: -ltest\n\
                 Requires: glib-2.0, gobject-2.0",
                &target_triplet,
            )
            .unwrap();
            assert_eq!(pc_file.deps, vec!["glib-2.0", "gobject-2.0"]);
            let pc_file = PcFile::from_str(
                "test",
                "Libs: -ltest\n\
                 Requires: glib-2.0 >=  2.58.0, gobject-2.0 >=  2.58.0",
                &target_triplet,
            )
            .unwrap();
            assert_eq!(pc_file.deps, vec!["glib-2.0", "gobject-2.0"]);
        }

        clean_env();
    }

    fn clean_env() {
        env::remove_var("TARGET");
        env::remove_var("VCPKG_ROOT");
        env::remove_var("VCPKGRS_DYNAMIC");
        env::remove_var("RUSTFLAGS");
        env::remove_var("CARGO_CFG_TARGET_FEATURE");
        env::remove_var("VCPKGRS_DISABLE");
        env::remove_var("VCPKGRS_NO_LIBMYSQL");
        env::remove_var("VCPKGRS_TRIPLET");
    }

    // path to a to vcpkg installation to test against
    fn vcpkg_test_tree_loc(name: &str) -> PathBuf {
        let mut path = PathBuf::new();
        path.push(env::var("CARGO_MANIFEST_DIR").unwrap());
        path.push("test-data");
        path.push(name);
        path
    }
}
