use std::path::{Path, PathBuf};
use std::process::Command;
use super::settings::PackOptions;
use super::{Artifact, PackError, SignTier, run_tool};
use crate::meta::Project;
use crate::ops::status;
pub fn pack(
project: &Project,
opts: &PackOptions,
payload: &Path,
dist: &Path,
) -> Result<Artifact, PackError> {
let makensis = find_makensis().ok_or_else(|| {
PackError::Other(
"makensis not found — install NSIS (choco install nsis / apt install nsis / brew install makensis), or set DAY_MAKENSIS (docs/environment.md)"
.into(),
)
})?;
let name = &project.manifest.app.name;
let title = project
.manifest
.app
.title
.clone()
.unwrap_or_else(|| name.clone());
let version = &project.manifest.app.version;
let work = project.root.join("build/day/pack/windows-nsis");
std::fs::create_dir_all(&work).map_err(|e| PackError::Other(e.to_string()))?;
let setup = dist.join(format!("{name}-{version}-setup.exe"));
let _ = std::fs::remove_file(&setup);
let nsi = work.join("installer.nsi");
std::fs::write(
&nsi,
render_nsi(
&title,
name,
version,
&project.manifest.app.id,
payload,
&setup,
),
)
.map_err(|e| PackError::Other(e.to_string()))?;
status("Packing", "makensis");
run_tool(Command::new(&makensis).arg("-V2").arg(&nsi), "makensis").map_err(PackError::Other)?;
if !setup.exists() {
return Err(PackError::Other(format!(
"makensis produced no installer at {}",
setup.display()
)));
}
let tier = if opts.no_sign || !cfg!(windows) {
if !cfg!(windows) && !opts.no_sign {
status(
"Signing",
"skipped (signtool is Windows-only; cross-compiled installer is unsigned)",
);
} else {
status("Signing", "skipped (--no-sign)");
}
SignTier::Unsigned
} else {
let settings = super::msix::resolve_signing(project)?;
super::msix::sign_file(&settings, &setup).map_err(PackError::Sign)?
};
Ok(Artifact {
path: setup,
kind: "nsis",
sha256: String::new(),
tier,
})
}
pub(crate) fn render_nsi(
title: &str,
name: &str,
version: &str,
id: &str,
payload: &Path,
out: &Path,
) -> String {
let payload_s = payload.display().to_string().replace('/', "\\");
let out_s = out.display().to_string().replace('/', "\\");
format!(
r#"; generated by day pack — do not edit
Unicode true
Name "{title}"
OutFile "{out_s}"
InstallDir "$LOCALAPPDATA\{title}"
RequestExecutionLevel user
SetCompressor /SOLID lzma
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\{id}"
Page directory
Page instfiles
UninstPage uninstConfirm
UninstPage instfiles
Section "Install"
SetOutPath "$INSTDIR"
File /r "{payload_s}\*.*"
WriteUninstaller "$INSTDIR\uninstall.exe"
CreateShortcut "$SMPROGRAMS\{title}.lnk" "$INSTDIR\{name}.exe"
WriteRegStr HKCU "${{UNINST_KEY}}" "DisplayName" "{title}"
WriteRegStr HKCU "${{UNINST_KEY}}" "DisplayVersion" "{version}"
WriteRegStr HKCU "${{UNINST_KEY}}" "DisplayIcon" "$INSTDIR\{name}.exe"
WriteRegStr HKCU "${{UNINST_KEY}}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
WriteRegStr HKCU "${{UNINST_KEY}}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
WriteRegDWORD HKCU "${{UNINST_KEY}}" "NoModify" 1
WriteRegDWORD HKCU "${{UNINST_KEY}}" "NoRepair" 1
SectionEnd
Section "Uninstall"
Delete "$SMPROGRAMS\{title}.lnk"
RMDir /r "$INSTDIR"
DeleteRegKey HKCU "${{UNINST_KEY}}"
SectionEnd
"#
)
}
fn find_makensis() -> Option<PathBuf> {
day_toolchain::makensis()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nsi_shape() {
let nsi = render_nsi(
"Day Showcase",
"showcase",
"0.1.0",
"dev.daybrite.showcase",
Path::new("C:/work/payload"),
Path::new("C:/dist/showcase-0.1.0-setup.exe"),
);
assert!(nsi.contains(r#"File /r "C:\work\payload\*.*""#));
assert!(nsi.contains("RequestExecutionLevel user"));
assert!(
nsi.contains(
r#"CreateShortcut "$SMPROGRAMS\Day Showcase.lnk" "$INSTDIR\showcase.exe""#
)
);
assert!(nsi.contains(r#"Uninstall\dev.daybrite.showcase""#));
}
}