use std::fs;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
use crate::error::ShunError;
#[derive(Debug, Clone)]
pub struct MsixInputs<'a> {
pub identity_name: &'a str,
pub publisher: &'a str,
pub display_name: &'a str,
pub description: &'a str,
pub version: &'a str,
pub executable: &'a Path,
pub logo_png: Option<&'a [u8]>,
pub logo_background: Option<&'a str>,
}
pub fn find_makeappx() -> Option<PathBuf> {
#[cfg(windows)]
{
let kits_root = std::env::var("ProgramFiles(x86)")
.ok()
.map(|root| Path::new(&root).join("Windows Kits").join("10").join("bin"))?;
let mut versions: Vec<PathBuf> = fs::read_dir(&kits_root)
.ok()?
.filter_map(Result::ok)
.map(|e| e.path())
.filter(|p| p.is_dir())
.collect();
versions.sort();
for version in versions.iter().rev() {
for arch in ["x64", "arm64", "x86"] {
let candidate = version.join(arch).join("makeappx.exe");
if candidate.is_file() {
return Some(candidate);
}
}
}
None
}
#[cfg(not(windows))]
{
None
}
}
fn copy_payload_dir(source: &Path, staging: &Path) -> Result<(), ShunError> {
for entry in WalkDir::new(source).into_iter().filter_map(Result::ok) {
if !entry.file_type().is_file() {
continue;
}
let relative = entry
.path()
.strip_prefix(source)
.map_err(|e| ShunError::Config(format!("payload walk drift: {e}")))?;
let normalized = relative.to_string_lossy().replace('\\', "/");
let target = staging.join(&normalized);
if let Some(parent) = target.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(entry.path(), &target)?;
}
Ok(())
}
pub fn write_manifest(staging: &Path, inputs: &MsixInputs<'_>) -> Result<(), ShunError> {
let mut segments: Vec<String> = inputs.version.split('.').map(str::to_string).collect();
while segments.len() < 4 {
segments.push("0".to_string());
}
let msix_version = segments.join(".");
let description = if inputs.description.is_empty() {
format!("{} — delivered by shun", inputs.display_name)
} else {
inputs.description.to_string()
};
let background = inputs.logo_background.unwrap_or("transparent");
let manifest = format!(
r#"<?xml version="1.0" encoding="utf-8"?>
<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities">
<Identity Name="{name}" Publisher="{publisher}" Version="{msix_version}" ProcessorArchitecture="x64" />
<Properties>
<DisplayName>{display}</DisplayName>
<PublisherDisplayName>{publisher_display}</PublisherDisplayName>
<Logo>assets/logo.png</Logo>
<Description>{description}</Description>
</Properties>
<Resources>
<Resource Language="en-us" />
</Resources>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.26100.0" />
</Dependencies>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
<Applications>
<Application Id="App" Executable="{executable}" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements DisplayName="{display}" Description="{description}" BackgroundColor="{background}" Square150x150Logo="assets/logo.png" Square44x44Logo="assets/logo.png" />
</Application>
</Applications>
</Package>
"#,
name = inputs.identity_name,
publisher = xml_escape(inputs.publisher),
msix_version = msix_version,
display = xml_escape(inputs.display_name),
publisher_display = xml_escape(inputs.publisher),
description = xml_escape(&description),
background = xml_escape(background),
executable = inputs.executable.to_string_lossy().replace('\\', "/"),
);
let assets = staging.join("assets");
fs::create_dir_all(&assets)?;
if let Some(logo) = inputs.logo_png {
fs::write(assets.join("logo.png"), logo)?;
}
fs::write(staging.join("AppxManifest.xml"), manifest)?;
Ok(())
}
fn xml_escape(value: &str) -> String {
value
.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}
pub fn pack(staging: &Path, out_msix: &Path) -> Result<(), ShunError> {
let Some(makeappx) = find_makeappx() else {
return Err(ShunError::Config(
"MakeAppx.exe not found — install the Windows SDK (or the MSIX Packaging Tool) \
to produce MSIX artifacts"
.into(),
));
};
let out_msix = out_msix.with_extension("msix");
if out_msix.exists() {
fs::remove_file(&out_msix)?;
}
let status = std::process::Command::new(makeappx)
.args(["pack", "/o", "/d"])
.arg(staging)
.arg("/p")
.arg(&out_msix)
.status()?;
if !status.success() {
return Err(ShunError::Config(format!(
"makeappx failed (exit {status:?})"
)));
}
Ok(())
}
pub fn build_msix(
payload_dir: &Path,
out_msix: &Path,
inputs: &MsixInputs<'_>,
) -> Result<PathBuf, ShunError> {
let staging = out_msix
.parent()
.unwrap_or(Path::new("."))
.join(format!("{}.msix-staging", inputs.identity_name));
fs::create_dir_all(&staging)?;
copy_payload_dir(payload_dir, &staging)?;
write_manifest(&staging, inputs)?;
pack(&staging, out_msix)?;
Ok(out_msix.to_path_buf())
}