#[macro_use] extern crate log;
extern crate mustache;
extern crate regex;
extern crate toml;
extern crate uuid;
use mustache::MapBuilder;
use std::default::Default;
use regex::Regex;
use std::error::Error as StdError;
use std::fmt;
use std::fs::{self, File};
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use toml::Value;
use uuid::Uuid;
const CARGO_MANIFEST_FILE: &str = "Cargo.toml";
const CARGO: &str = "cargo";
const DEFAULT_LICENSE_FILE_NAME: &str = "LICENSE";
const SIGNTOOL: &str = "signtool";
const WIX: &str = "wix";
const WIX_COMPILER: &str = "candle";
const WIX_LINKER: &str = "light";
const WIX_SOURCE_FILE_EXTENSION: &str = "wxs";
const WIX_SOURCE_FILE_NAME: &str = "main";
static TEMPLATE: &str = include_str!("template.wxs");
fn write_template<W: Write>(writer: &mut W) -> Result<(), Error> {
let template = mustache::compile_str(TEMPLATE)?;
let data = MapBuilder::new()
.insert_str("upgrade-code-guid", Uuid::new_v4().hyphenated().to_string().to_uppercase())
.insert_str("path-component-guid", Uuid::new_v4().hyphenated().to_string().to_uppercase())
.build();
template.render_data(writer, &data)?;
Ok(())
}
pub fn print_template() -> Result<(), Error> {
write_template(&mut io::stdout())
}
pub fn init(force: bool) -> Result<(), Error> {
let mut main_wxs_path = PathBuf::from(WIX);
if !main_wxs_path.exists() {
fs::create_dir(&main_wxs_path)?;
}
main_wxs_path.push(WIX_SOURCE_FILE_NAME);
main_wxs_path.set_extension(WIX_SOURCE_FILE_EXTENSION);
if main_wxs_path.exists() && !force {
Err(Error::Generic(
format!("The '{}' file already exists. Use the '--force' flag to overwrite the contents.",
main_wxs_path.display())
))
} else {
let mut main_wxs = File::create(main_wxs_path)?;
write_template(&mut main_wxs)?;
Ok(())
}
}
#[derive(Debug)]
pub enum Error {
Command(&'static str, i32),
Generic(String),
Io(io::Error),
Manifest(String),
Mustache(mustache::Error),
Toml(toml::de::Error),
}
impl Error {
pub fn code(&self) -> i32 {
match *self{
Error::Command(..) => 1,
Error::Generic(..) => 2,
Error::Io(..) => 3,
Error::Manifest(..) => 4,
Error::Mustache(..) => 5,
Error::Toml(..) => 6,
}
}
}
impl StdError for Error {
fn description(&self) -> &str {
match *self {
Error::Command(..) => "Command",
Error::Generic(..) => "Generic",
Error::Io(..) => "Io",
Error::Manifest(..) => "Manifest",
Error::Mustache(..) => "Mustache",
Error::Toml(..) => "TOML",
}
}
fn cause(&self) -> Option<&StdError> {
match *self {
Error::Io(ref err) => Some(err),
Error::Toml(ref err) => Some(err),
Error::Mustache(ref err) => Some(err),
_ => None
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Error::Command(ref command, ref code) =>
write!(f, "The '{}' application failed with exit code = {}. Consider using the '--nocapture' flag to obtain more information.", command, code),
Error::Generic(ref msg) => write!(f, "{}", msg),
Error::Io(ref err) => write!(f, "{}", err),
Error::Manifest(ref var) =>
write!(f, "No '{}' field found in the package's manifest (Cargo.toml)", var),
Error::Mustache(ref err) => write!(f, "{}", err),
Error::Toml(ref err) => write!(f, "{}", err),
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::Io(err)
}
}
impl From<toml::de::Error> for Error {
fn from(err: toml::de::Error) -> Error {
Error::Toml(err)
}
}
impl From<mustache::Error> for Error {
fn from(err: mustache::Error) -> Error {
Error::Mustache(err)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Platform {
X86,
X64,
}
impl Platform {
pub fn arch(&self) -> &'static str {
match *self {
Platform::X86 => "i686",
Platform::X64 => "x86_64",
}
}
}
impl fmt::Display for Platform {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Platform::X86 => write!(f, "x86"),
Platform::X64 => write!(f, "x64"),
}
}
}
impl Default for Platform {
fn default() -> Self {
if cfg!(target_arch = "x86_64") {
Platform::X64
} else {
Platform::X86
}
}
}
#[derive(Debug, Clone)]
pub struct Wix {
binary_name: Option<String>,
capture_output: bool,
description: Option<String>,
input: Option<PathBuf>,
license_path: Option<PathBuf>,
manufacturer: Option<String>,
product_name: Option<String>,
sign: bool,
timestamp: Option<String>,
}
impl Wix {
pub fn new() -> Self {
Wix {
binary_name: None,
capture_output: true,
description: None,
input: None,
license_path: None,
manufacturer: None,
product_name: None,
sign: false,
timestamp: None,
}
}
pub fn binary_name(mut self, b: Option<&str>) -> Self {
self.binary_name = b.map(|s| String::from(s));
self
}
pub fn capture_output(mut self, c: bool) -> Self {
self.capture_output = c;
self
}
pub fn description(mut self, d: Option<&str>) -> Self {
self.description = d.map(|s| String::from(s));
self
}
pub fn input(mut self, i: Option<&str>) -> Self {
self.input = i.map(|i| PathBuf::from(i));
self
}
pub fn license_file(mut self, l: Option<&str>) -> Self {
self.license_path = l.map(|l| PathBuf::from(l));
self
}
pub fn manufacturer(mut self, m: Option<&str>) -> Self {
self.manufacturer = m.map(|s| String::from(s));
self
}
pub fn product_name(mut self, p: Option<&str>) -> Self {
self.product_name = p.map(|s| String::from(s));
self
}
pub fn sign(mut self, s: bool) -> Self {
self.sign = s;
self
}
pub fn timestamp(mut self, t: Option<&str>) -> Self {
self.timestamp = t.map(|t| String::from(t));
self
}
pub fn run(self) -> Result<(), Error> {
debug!("binary_name = {:?}", self.binary_name);
debug!("capture_output = {:?}", self.capture_output);
debug!("description = {:?}", self.description);
debug!("input = {:?}", self.input);
debug!("manufacturer = {:?}", self.manufacturer);
debug!("product_name = {:?}", self.product_name);
debug!("sign = {:?}", self.sign);
debug!("timestamp = {:?}", self.timestamp);
let cargo_file_path = Path::new(CARGO_MANIFEST_FILE);
debug!("cargo_file_path = {:?}", cargo_file_path);
let mut cargo_file = File::open(cargo_file_path)?;
let mut cargo_file_content = String::new();
cargo_file.read_to_string(&mut cargo_file_content)?;
let cargo_values = cargo_file_content.parse::<Value>()?;
let pkg_version = cargo_values
.get("package")
.and_then(|p| p.as_table())
.and_then(|t| t.get("version"))
.and_then(|v| v.as_str())
.ok_or(Error::Manifest(String::from("version")))?;
debug!("pkg_version = {:?}", pkg_version);
let product_name = if let Some(p) = self.product_name {
Ok(p)
} else {
cargo_values.get("package")
.and_then(|p| p.as_table())
.and_then(|t| t.get("name"))
.and_then(|n| n.as_str())
.map(|s| String::from(s))
.ok_or(Error::Manifest(String::from("name")))
}?;
debug!("product_name = {:?}", product_name);
let description = if let Some(d) = self.description {
Ok(d)
} else {
cargo_values.get("package")
.and_then(|p| p.as_table())
.and_then(|t| t.get("description"))
.and_then(|d| d.as_str())
.map(|s| String::from(s))
.ok_or(Error::Manifest(String::from("description")))
}?;
debug!("description = {:?}", description);
let license_name = if let Some(ref l) = self.license_path {
l.file_name()
.and_then(|f| f.to_str())
.map(|s| String::from(s))
.ok_or(Error::Generic(
format!("The '{}' license path does not contain a file name.", l.display())
))
} else {
cargo_values.get("package")
.and_then(|p| p.as_table())
.and_then(|t| t.get("license-file"))
.and_then(|l| l.as_str())
.and_then(|s| Path::new(s).file_name().and_then(|f| f.to_str()))
.or(Some("License.txt"))
.map(|s| String::from(s))
.ok_or(Error::Generic(
format!("The 'license-file' field value does not contain a file name.")
))
}?;
debug!("license_name = {:?}", license_name);
let license_source = self.license_path.unwrap_or(
cargo_values.get("package")
.and_then(|p| p.as_table())
.and_then(|t| t.get("license-file"))
.and_then(|l| l.as_str())
.map(|s| PathBuf::from(s))
.unwrap_or(PathBuf::from(DEFAULT_LICENSE_FILE_NAME))
);
debug!("license_source = {:?}", license_source);
let manufacturer = if let Some(m) = self.manufacturer {
Ok(m)
} else {
cargo_values.get("package")
.and_then(|p| p.as_table())
.and_then(|t| t.get("authors"))
.and_then(|a| a.as_array())
.and_then(|a| a.get(0))
.and_then(|f| f.as_str())
.and_then(|s| {
let re = Regex::new(r"<(.*?)>").unwrap();
Some(re.replace_all(s, ""))
})
.map(|s| String::from(s.trim()))
.ok_or(Error::Manifest(String::from("authors")))
}?;
debug!("manufacturer = {}", manufacturer);
let help_url = cargo_values
.get("package")
.and_then(|p| p.as_table())
.and_then(|t| t.get("documentation").or(t.get("homepage")).or(t.get("repository")))
.and_then(|h| h.as_str())
.ok_or(Error::Manifest(String::from("documentation")))?;
let binary_name = self.binary_name.unwrap_or(
cargo_values.get("bin")
.and_then(|b| b.as_table())
.and_then(|t| t.get("name"))
.and_then(|n| n.as_str())
.map(|s| String::from(s))
.unwrap_or(product_name.clone())
);
debug!("binary_name = {:?}", binary_name);
let platform = if cfg!(target_arch = "x86_64") {
Platform::X64
} else {
Platform::X86
};
debug!("platform = {:?}", platform);
let main_wxs = if let Some(p) = self.input {
if p.exists() {
if p.is_dir() {
Err(Error::Generic(format!("The '{}' path is not a file. Please check the path and ensure it is to a WiX Source (wxs) file.", p.display())))
} else {
trace!("Using the '{}' WiX source file", p.display());
Ok(p)
}
} else {
Err(Error::Generic(format!("The '{0}' file does not exist. Consider using the 'cargo wix --print-template > {0}' command to create it.", p.display())))
}
} else {
trace!("Using the default WiX source file");
let mut main_wxs = PathBuf::from(WIX);
main_wxs.push(WIX_SOURCE_FILE_NAME);
main_wxs.set_extension(WIX_SOURCE_FILE_EXTENSION);
if main_wxs.exists() {
Ok(main_wxs)
} else {
Err(Error::Generic(format!("The '{0}' file does not exist. Consider using the 'cargo wix --init' command to create it.", main_wxs.display())))
}
}?;
debug!("main_wxs = {:?}", main_wxs);
let mut main_wixobj = PathBuf::from("target");
main_wixobj.push(WIX);
main_wixobj.push("build");
main_wixobj.push(WIX_SOURCE_FILE_NAME);
main_wixobj.set_extension("wixobj");
debug!("main_wixobj = {:?}", main_wixobj);
let mut main_msi = PathBuf::from("target");
main_msi.push(WIX);
main_msi.push(&format!("{}-{}-{}.msi", product_name, pkg_version, platform.arch()));
debug!("main_msi = {:?}", main_msi);
info!("Building release binary");
let mut builder = Command::new(CARGO);
if self.capture_output {
trace!("Capturing the '{}' output", CARGO);
builder.stdout(Stdio::null());
builder.stderr(Stdio::null());
}
let status = builder.arg("build").arg("--release").status()?;
if !status.success() {
return Err(Error::Command(CARGO, status.code().unwrap_or(0)));
}
info!("Compiling installer");
let mut compiler = Command::new(WIX_COMPILER);
if self.capture_output {
trace!("Capturing the '{}' output", WIX_COMPILER);
compiler.stdout(Stdio::null());
compiler.stderr(Stdio::null());
}
let status = compiler
.arg(format!("-dVersion={}", pkg_version))
.arg(format!("-dPlatform={}", platform))
.arg(format!("-dProductName={}", product_name))
.arg(format!("-dBinaryName={}", binary_name))
.arg(format!("-dDescription={}", description))
.arg(format!("-dManufacturer={}", manufacturer))
.arg(format!("-dLicenseName={}", license_name))
.arg(format!("-dLicenseSource={}", license_source.display()))
.arg(format!("-dHelp={}", help_url))
.arg("-o")
.arg(&main_wixobj)
.arg(&main_wxs)
.status()?;
if !status.success() {
return Err(Error::Command(WIX_COMPILER, status.code().unwrap_or(0)));
}
info!("Linking the installer");
let mut linker = Command::new(WIX_LINKER);
if self.capture_output {
trace!("Capturing the '{}' output", WIX_LINKER);
linker.stdout(Stdio::null());
linker.stderr(Stdio::null());
}
let status = linker
.arg("-ext")
.arg("WixUIExtension")
.arg("-cultures:en-us")
.arg(&main_wixobj)
.arg("-out")
.arg(&main_msi)
.status()?;
if !status.success() {
return Err(Error::Command(WIX_LINKER, status.code().unwrap_or(0)));
}
if self.sign {
info!("Signing the installer");
let mut signer = Command::new(SIGNTOOL);
if self.capture_output {
trace!("Capturing the {} output", SIGNTOOL);
signer.stdout(Stdio::null());
signer.stderr(Stdio::null());
}
signer.arg("sign").arg("/a");
if let Some(t) = self.timestamp {
trace!("Using the '{}' timestamp server to sign the installer", t);
signer.arg("/t");
signer.arg(t);
}
let status = signer.arg(&main_msi).status()?;
if !status.success() {
return Err(Error::Command(SIGNTOOL, status.code().unwrap_or(0)));
}
}
Ok(())
}
}
impl Default for Wix {
fn default() -> Self {
Wix::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_are_correct() {
let wix = Wix::new();
assert_eq!(wix.binary_name, None);
assert!(wix.capture_output);
assert_eq!(wix.description, None);
assert_eq!(wix.input, None);
assert_eq!(wix.manufacturer, None);
assert_eq!(wix.product_name, None);
assert!(!wix.sign);
assert_eq!(wix.timestamp, None);
}
#[test]
fn capture_output_works() {
let wix = Wix::new().capture_output(false);
assert!(!wix.capture_output);
}
#[test]
fn input_works() {
const EXPECTED: &str = "test.wxs";
let wix = Wix::new().input(Some(EXPECTED));
assert_eq!(wix.input, Some(PathBuf::from(EXPECTED)));
}
#[test]
fn manufacturer_works() {
const EXPECTED: &str = "Tester";
let wix = Wix::new().manufacturer(Some(EXPECTED));
assert_eq!(wix.manufacturer, Some(String::from(EXPECTED)));
}
#[test]
fn sign_works() {
let wix = Wix::new().sign(true);
assert!(wix.sign);
}
#[test]
fn timestamp_works() {
const EXPECTED: &str = "http://timestamp.comodoca.com/";
let wix = Wix::new().timestamp(Some(EXPECTED));
assert_eq!(wix.timestamp, Some(String::from(EXPECTED)));
}
#[test]
fn strip_email_works() {
const EXPECTED: &str = "Christopher R. Field";
let re = Regex::new(r"<(.*?)>").unwrap();
let actual = re.replace_all("Christopher R. Field <cfield2@gmail.com>", "");
assert_eq!(actual.trim(), EXPECTED);
}
#[test]
fn strip_email_works_without_email() {
const EXPECTED: &str = "Christopher R. Field";
let re = Regex::new(r"<(.*?)>").unwrap();
let actual = re.replace_all("Christopher R. Field", "");
assert_eq!(actual.trim(), EXPECTED);
}
#[test]
fn strip_email_works_with_only_email() {
const EXPECTED: &str = "cfield2@gmail.com";
let re = Regex::new(r"<(.*?)>").unwrap();
let actual = re.replace_all("cfield2@gmail.com", "");
assert_eq!(actual.trim(), EXPECTED);
}
}