use {
crate::starlark::{
code_signing::{
handle_file_manifest_signable_events, handle_signable_event, SigningAction,
SigningContext,
},
file_content::FileContentWrapper,
file_manifest::FileManifestValue,
wix_msi_builder::WiXMsiBuilderValue,
},
anyhow::{anyhow, Context, Result},
simple_file_manifest::{FileEntry, FileManifest},
starlark::{
environment::TypeValues,
eval::call_stack::CallStack,
values::{
error::{RuntimeError, UnsupportedOperation, ValueError},
none::NoneType,
{Mutable, TypedValue, Value, ValueResult},
},
{
starlark_fun, starlark_module, starlark_parse_param_type, starlark_signature,
starlark_signature_extraction, starlark_signatures,
},
},
starlark_dialect_build_targets::{
get_context_value, optional_dict_arg, optional_str_arg, EnvironmentContext, ResolvedTarget,
ResolvedTargetValue, RunMode,
},
std::path::{Path, PathBuf},
tugger_code_signing::SigningDestination,
tugger_wix::{WiXInstallerBuilder, WiXSimpleMsiBuilder, WxsBuilder},
};
fn error_context<F, T>(label: &str, f: F) -> Result<T, ValueError>
where
F: FnOnce() -> anyhow::Result<T>,
{
f().map_err(|e| {
ValueError::Runtime(RuntimeError {
code: "TUGGER_WIX_INSTALLER",
message: format!("{:?}", e),
label: label.to_string(),
})
})
}
pub struct WiXInstallerValue {
pub inner: WiXInstallerBuilder,
pub filename: String,
}
impl TypedValue for WiXInstallerValue {
type Holder = Mutable<WiXInstallerValue>;
const TYPE: &'static str = "WiXInstaller";
fn values_for_descendant_check_and_freeze(&self) -> Box<dyn Iterator<Item = Value>> {
Box::new(std::iter::empty())
}
fn get_attr(&self, attribute: &str) -> ValueResult {
Ok(match attribute {
"arch" => Value::from(self.inner.arch()),
"install_files_root_directory_id" => {
Value::from(self.inner.install_files_root_directory_id())
}
"install_files_wxs_path" => {
Value::from(format!("{}", self.inner.install_files_wxs_path().display()))
}
_ => {
return Err(ValueError::OperationNotSupported {
op: UnsupportedOperation::GetAttr(attribute.to_string()),
left: Self::TYPE.to_string(),
right: None,
})
}
})
}
fn has_attr(&self, attribute: &str) -> Result<bool, ValueError> {
Ok(matches!(
attribute,
"arch" | "install_files_root_directory_id" | "install_files_wxs_path"
))
}
fn set_attr(&mut self, attribute: &str, value: Value) -> Result<(), ValueError> {
match attribute {
"arch" => {
self.inner.set_arch(value.to_string());
}
"install_files_root_directory_id" => {
self.inner
.set_install_files_root_directory_id(value.to_string());
}
"install_files_wxs_path" => {
self.inner.set_install_files_wxs_path(value.to_string());
}
attr => {
return Err(ValueError::OperationNotSupported {
op: UnsupportedOperation::SetAttr(attr.to_string()),
left: Self::TYPE.to_string(),
right: None,
})
}
}
Ok(())
}
}
impl WiXInstallerValue {
fn new_from_args(
type_values: &TypeValues,
id: String,
filename: String,
arch: String,
) -> ValueResult {
let build_context_value = get_context_value(type_values)?;
let context = build_context_value
.downcast_ref::<EnvironmentContext>()
.ok_or(ValueError::IncorrectParameterType)?;
let builder = WiXInstallerBuilder::new(id, arch, context.build_path());
Ok(Value::new(WiXInstallerValue {
inner: builder,
filename,
}))
}
fn add_build_files(&mut self, manifest: FileManifestValue) -> ValueResult {
const LABEL: &str = "WiXInstaller.add_build_files()";
let manifest = manifest.inner(LABEL)?;
error_context(LABEL, || {
self.inner
.add_extra_build_files(&manifest)
.context("adding extra build files from FileManifest")
})?;
Ok(Value::new(NoneType::None))
}
fn resolve_file_entry(&self, path: impl AsRef<Path>, force_read: bool) -> Result<FileEntry> {
let entry = FileEntry::try_from(path.as_ref())?;
Ok(if force_read {
entry.to_memory()?
} else {
entry
})
}
fn add_build_file(
&mut self,
install_path: String,
filesystem_path: String,
force_read: bool,
) -> ValueResult {
error_context("WiXInstaller.add_build_file()", || {
let entry = self
.resolve_file_entry(&filesystem_path, force_read)
.with_context(|| format!("resolving file entry: {}", filesystem_path))?;
self.inner
.add_extra_build_file(&install_path, entry)
.with_context(|| format!("adding extra build file to {}", install_path))
})?;
Ok(Value::new(NoneType::None))
}
fn add_install_file(
&mut self,
type_values: &TypeValues,
call_stack: &mut CallStack,
install_path: String,
filesystem_path: String,
force_read: bool,
) -> ValueResult {
const LABEL: &str = "WiXInstaller.add_install_file()";
let manifest = error_context(LABEL, || {
let mut manifest = FileManifest::default();
let entry = self
.resolve_file_entry(&filesystem_path, force_read)
.with_context(|| format!("resolving file entry from path {}", filesystem_path))?;
manifest
.add_file_entry(&install_path, entry)
.context("adding FileEntry to InstallManifest")?;
Ok(manifest)
})?;
self.add_install_files_from_manifest(type_values, call_stack, LABEL, &manifest)
}
fn add_install_files(
&mut self,
type_values: &TypeValues,
call_stack: &mut CallStack,
manifest: FileManifestValue,
) -> ValueResult {
const LABEL: &str = "WixInstaller.add_install_files()";
let manifest = manifest.inner(LABEL)?;
self.add_install_files_from_manifest(
type_values,
call_stack,
"WiXInstaller.add_install_files()",
&manifest,
)
}
fn add_install_files_from_manifest(
&mut self,
type_values: &TypeValues,
call_stack: &mut CallStack,
label: &'static str,
manifest: &FileManifest,
) -> ValueResult {
error_context(label, || {
let manifest = handle_file_manifest_signable_events(
type_values,
call_stack,
manifest,
label,
SigningAction::WindowsInstallerFileAdded,
)
.context("running code signing checks for FileManifest")?;
self.inner
.install_files_mut()
.add_manifest(&manifest)
.context("adding install files from FileManifest")
})?;
Ok(Value::new(NoneType::None))
}
fn add_msi_builder(&mut self, builder: WiXMsiBuilderValue) -> ValueResult {
const LABEL: &str = "WiXInstaller.add_msi_builder()";
let inner = builder.inner(LABEL)?;
error_context(LABEL, || {
inner
.builder
.add_to_installer_builder(&mut self.inner)
.context("adding WiXInstallerBuilder")
})?;
Ok(Value::new(NoneType::None))
}
fn add_simple_installer(
&mut self,
id_prefix: String,
product_name: String,
product_version: String,
product_manufacturer: String,
program_files: FileManifestValue,
) -> ValueResult {
const LABEL: &str = "WiXInstaller.add_simple_installer()";
let manifest = program_files.inner(LABEL)?;
error_context(LABEL, || {
let mut builder = WiXSimpleMsiBuilder::new(
&id_prefix,
&product_name,
&product_version,
&product_manufacturer,
);
builder
.add_program_files_manifest(&manifest)
.context("adding program files manifest")?;
builder
.add_to_installer_builder(&mut self.inner)
.context("adding to WiXInstallerBuilder")
})?;
Ok(Value::new(NoneType::None))
}
fn add_wxs_file(&mut self, path: String, preprocessor_parameters: Value) -> ValueResult {
optional_dict_arg(
"preprocessor_parameters",
"string",
"string",
&preprocessor_parameters,
)?;
let mut builder = error_context("WiXInstaller.add_wxs_file()", || {
WxsBuilder::from_path(path).context("constructing WxsBuilder from path")
})?;
match preprocessor_parameters.get_type() {
"dict" => {
for key in preprocessor_parameters.iter()?.iter() {
let k = key.to_string();
let v = preprocessor_parameters.at(key).unwrap().to_string();
builder.set_preprocessor_parameter(k, v);
}
}
"NoneType" => (),
_ => panic!("should have validated type above"),
}
self.inner.add_wxs(builder);
Ok(Value::new(NoneType::None))
}
fn materialize(
&mut self,
type_values: &TypeValues,
call_stack: &mut CallStack,
label: &'static str,
dest_dir: &Path,
) -> Result<PathBuf, ValueError> {
let installer_path = dest_dir.join(&self.filename);
error_context(label, || {
self.inner.set_build_path(dest_dir);
self.inner
.add_files_manifest_wxs()
.context("generating install_files manifest wxs")?;
self.inner.build(&installer_path).context("building")
})?;
let candidate = installer_path.as_path().into();
let mut context = SigningContext::new(
label,
SigningAction::WindowsInstallerCreation,
&self.filename,
&candidate,
);
context.set_path(&installer_path);
context.set_signing_destination(SigningDestination::File(installer_path.clone()));
handle_signable_event(type_values, call_stack, context)?;
Ok(installer_path)
}
fn materialize_temp_dir(
&mut self,
type_values: &TypeValues,
call_stack: &mut CallStack,
label: &'static str,
) -> Result<(FileEntry, String), ValueError> {
let build_path = {
let context_value = get_context_value(type_values)?;
let context = context_value
.downcast_ref::<EnvironmentContext>()
.ok_or(ValueError::IncorrectParameterType)?;
context.build_path().to_path_buf()
};
let dest_dir = error_context(label, || {
tempfile::Builder::new()
.prefix("wix-installer-")
.tempdir_in(&build_path)
.context("creating temp directory")
})?;
let installer_path = self.materialize(type_values, call_stack, label, dest_dir.path())?;
let entry = FileEntry::new_from_path(&installer_path, false);
let (entry, filename) = error_context(label, || {
let entry = entry
.to_memory()
.context("converting FileEntry to in-memory")?;
let filename = installer_path
.file_name()
.ok_or_else(|| anyhow!("unable to resolve file name of generated installer"))?;
Ok((entry, filename.to_string_lossy().to_string()))
})?;
Ok((entry, filename))
}
fn build(
&mut self,
type_values: &TypeValues,
call_stack: &mut CallStack,
target: String,
) -> ValueResult {
const LABEL: &str = "WixInstaller.build()";
let context_value = get_context_value(type_values)?;
let context = context_value
.downcast_ref::<EnvironmentContext>()
.ok_or(ValueError::IncorrectParameterType)?;
let output_path = context.target_build_path(&target);
let installer_path = self.materialize(type_values, call_stack, LABEL, &output_path)?;
Ok(Value::new(ResolvedTargetValue {
inner: ResolvedTarget {
run_mode: RunMode::Path {
path: installer_path,
},
output_path,
},
}))
}
fn set_variable(&mut self, key: String, value: Value) -> ValueResult {
let value = optional_str_arg("value", &value)?;
self.inner.set_variable(key, value);
Ok(Value::new(NoneType::None))
}
#[allow(clippy::wrong_self_convention)]
fn to_file_content(
&mut self,
type_values: &TypeValues,
call_stack: &mut CallStack,
) -> ValueResult {
const LABEL: &str = "WiXInstaller.to_file_content()";
let (entry, filename) = self.materialize_temp_dir(type_values, call_stack, LABEL)?;
Ok(FileContentWrapper {
content: entry,
filename,
}
.into())
}
fn write_to_directory(
&mut self,
type_values: &TypeValues,
call_stack: &mut CallStack,
path: String,
) -> ValueResult {
const LABEL: &str = "WiXInstaller.write_to_directory()";
let dest_dir = {
let context_value = get_context_value(type_values)?;
let context = context_value
.downcast_ref::<EnvironmentContext>()
.ok_or(ValueError::IncorrectParameterType)?;
context.resolve_path(path)
};
let (entry, filename) = self.materialize_temp_dir(type_values, call_stack, LABEL)?;
let installer_path = dest_dir.join(filename);
error_context(LABEL, || {
entry
.write_to_path(&installer_path)
.with_context(|| format!("writing installer to {}", installer_path.display()))
})?;
Ok(Value::from(format!("{}", installer_path.display())))
}
}
starlark_module! { wix_installer_module =>
#[allow(non_snake_case)]
WiXInstaller(env env, id: String, filename: String, arch: String = "x64".to_string()) {
WiXInstallerValue::new_from_args(env, id, filename, arch)
}
WiXInstaller.add_build_file(
this,
build_path: String,
filesystem_path: String,
force_read: bool = false
) {
let mut this = this.downcast_mut::<WiXInstallerValue>().unwrap().unwrap();
this.add_build_file(build_path, filesystem_path, force_read)
}
WiXInstaller.add_build_files(this, manifest: FileManifestValue) {
let mut this = this.downcast_mut::<WiXInstallerValue>().unwrap().unwrap();
this.add_build_files(manifest)
}
WiXInstaller.add_install_file(
env env,
call_stack cs,
this,
install_path: String,
filesystem_path: String,
force_read: bool = false
) {
let mut this = this.downcast_mut::<WiXInstallerValue>().unwrap().unwrap();
this.add_install_file(env, cs, install_path, filesystem_path, force_read)
}
WiXInstaller.add_install_files(
env env, call_stack cs, this, manifest: FileManifestValue) {
let mut this = this.downcast_mut::<WiXInstallerValue>().unwrap().unwrap();
this.add_install_files(env, cs, manifest)
}
WiXInstaller.add_msi_builder(this, builder: WiXMsiBuilderValue) {
let mut this = this.downcast_mut::<WiXInstallerValue>().unwrap().unwrap();
this.add_msi_builder(builder)
}
WiXInstaller.add_simple_installer(
this,
id_prefix: String,
product_name: String,
product_version: String,
product_manufacturer: String,
program_files: FileManifestValue
) {
let mut this = this.downcast_mut::<WiXInstallerValue>().unwrap().unwrap();
this.add_simple_installer(
id_prefix,
product_name,
product_version,
product_manufacturer,
program_files,
)
}
WiXInstaller.add_wxs_file(this, path: String, preprocessor_parameters = NoneType::None) {
let mut this = this.downcast_mut::<WiXInstallerValue>().unwrap().unwrap();
this.add_wxs_file(path, preprocessor_parameters)
}
WiXInstaller.build(env env, call_stack cs, this, target: String) {
let mut this = this.downcast_mut::<WiXInstallerValue>().unwrap().unwrap();
this.build(env, cs, target)
}
WiXInstaller.set_variable(this, key: String, value) {
let mut this = this.downcast_mut::<WiXInstallerValue>().unwrap().unwrap();
this.set_variable(key, value)
}
WiXInstaller.to_file_content(env env, call_stack cs, this) {
let mut this = this.downcast_mut::<WiXInstallerValue>().unwrap().unwrap();
this.to_file_content(env, cs)
}
WiXInstaller.write_to_directory(env env, call_stack cs, this, path: String) {
let mut this = this.downcast_mut::<WiXInstallerValue>().unwrap().unwrap();
this.write_to_directory(env, cs, path)
}
}
#[cfg(test)]
mod tests {
use {super::*, crate::starlark::testutil::*, anyhow::Result};
#[cfg(windows)]
use {crate::starlark::file_content::FileContentValue, tugger_common::testutil::*};
#[test]
fn test_constructor() -> Result<()> {
let mut env = StarlarkEnvironment::new()?;
let installer = env.eval("WiXInstaller('myapp', 'ignored')")?;
assert_eq!(installer.get_type(), WiXInstallerValue::TYPE);
let installer_value = env.eval("WiXInstaller('myapp', 'ignored', arch='arch')")?;
let installer = installer_value.downcast_ref::<WiXInstallerValue>().unwrap();
assert_eq!(installer.inner.arch(), "arch");
Ok(())
}
#[test]
fn test_attributes() -> Result<()> {
let mut env = StarlarkEnvironment::new()?;
env.eval("i = WiXInstaller('myapp', 'ignored')")?;
let arch = env.eval("i.arch")?;
assert_eq!(arch.to_string(), "x64");
env.eval("i.arch = 'x86'")?;
let arch = env.eval("i.arch")?;
assert_eq!(arch.to_string(), "x86");
Ok(())
}
#[test]
fn test_add_missing_file() -> Result<()> {
let mut env = StarlarkEnvironment::new()?;
env.eval("installer = WiXInstaller('myapp', 'ignored')")?;
assert!(env
.eval("installer.add_wxs_file('does-not-exist')")
.is_err());
Ok(())
}
#[test]
fn test_set_variable() -> Result<()> {
let mut env = StarlarkEnvironment::new()?;
env.eval("installer = WiXInstaller('myapp', 'ignored')")?;
env.eval("installer.set_variable('foo', None)")?;
env.eval("installer.set_variable('bar', 'baz')")?;
let installer_value = env.eval("installer")?;
let installer = installer_value.downcast_ref::<WiXInstallerValue>().unwrap();
let variables = installer.inner.variables().collect::<Vec<_>>();
assert_eq!(
variables,
vec![
(&"bar".to_string(), &Some("baz".to_string())),
(&"foo".to_string(), &None),
]
);
Ok(())
}
#[test]
fn test_add_simple_installer() -> Result<()> {
let mut env = StarlarkEnvironment::new()?;
env.eval("installer = WiXInstaller('myapp', 'ignored')")?;
env.eval(
"installer.add_simple_installer('myapp', 'myapp', '0.1', 'author', FileManifest())",
)?;
Ok(())
}
#[test]
fn test_add_build_files() -> Result<()> {
let mut env = StarlarkEnvironment::new()?;
env.eval("installer = WiXInstaller('myapp', 'ignored')")?;
env.eval("m = FileManifest()")?;
env.eval("installer.add_build_files(m)")?;
Ok(())
}
#[cfg(windows)]
#[test]
fn test_build_simple_installer() -> Result<()> {
let mut env = StarlarkEnvironment::new()?;
env.eval("installer = WiXInstaller('myapp', 'myapp.msi')")?;
env.eval(
"installer.add_simple_installer('myapp', 'myapp', '0.1', 'author', FileManifest())",
)?;
let resolved_value = env.eval("installer.build('test_build_simple_installer')")?;
assert_eq!(resolved_value.get_type(), "ResolvedTarget");
let resolved = resolved_value
.downcast_ref::<ResolvedTargetValue>()
.unwrap();
let context_value = get_context_value(&env.type_values).unwrap();
let context = context_value.downcast_ref::<EnvironmentContext>().unwrap();
let build_path = context.target_build_path("test_build_simple_installer");
let msi_path = build_path.join("myapp.msi");
assert_eq!(
resolved.inner.run_mode,
RunMode::Path {
path: msi_path.clone()
}
);
assert!(msi_path.exists());
Ok(())
}
#[cfg(windows)]
#[test]
fn to_file_content() -> Result<()> {
let mut env = StarlarkEnvironment::new()?;
env.eval("installer = WiXInstaller('myapp', 'myapp.msi')")?;
env.eval(
"installer.add_simple_installer('myapp', 'myapp', '0.1', 'author', FileManifest())",
)?;
let value = env.eval("installer.to_file_content()")?;
assert_eq!(value.get_type(), FileContentValue::TYPE);
Ok(())
}
#[cfg(windows)]
#[test]
fn write_to_directory() -> Result<()> {
let mut env = StarlarkEnvironment::new()?;
let dest_dir = DEFAULT_TEMP_DIR
.path()
.join("wix-installer-write-to-directory");
let dest_dir_s = dest_dir.to_string_lossy().replace('\\', "/");
env.eval("installer = WiXInstaller('myapp', 'myapp.msi')")?;
env.eval(
"installer.add_simple_installer('myapp', 'myapp', '0.1', 'author', FileManifest())",
)?;
let value = env.eval(&format!("installer.write_to_directory('{}')", dest_dir_s))?;
assert_eq!(value.get_type(), "string");
let path = PathBuf::from(value.to_string());
assert!(path.exists());
Ok(())
}
}