use std::path::{Path, PathBuf};
use std::process::Command;
use super::settings::{PackOptions, resolve_degradable};
use super::{Artifact, PackError, SignTier, run_tool};
use crate::meta::Project;
use crate::ops::{self, status};
use crate::targets::Target;
const DEV_PUBLISHER: &str = "CN=Day Development";
pub fn stage_payload(
project: &Project,
target: &'static Target,
opts: &PackOptions,
) -> Result<PathBuf, PackError> {
let outcome = ops::build(project, target, &opts.profile).map_err(PackError::Other)?;
let stage = project.root.join("build/day/pack/windows-payload");
let _ = std::fs::remove_dir_all(&stage);
std::fs::create_dir_all(&stage).map_err(|e| PackError::Other(e.to_string()))?;
let name = &project.manifest.app.name;
std::fs::copy(&outcome.artifact, stage.join(format!("{name}.exe")))
.map_err(|e| PackError::Other(e.to_string()))?;
for dir in ["assets", "images", "fonts"] {
let src = project.root.join(dir);
if src.is_dir() {
super::copy_tree(&src, &stage.join(dir)).map_err(PackError::Other)?;
}
}
if let Some(ico) = crate::resources::app_icon(project, "winui") {
let _ = std::fs::copy(&ico, stage.join(format!("{name}.ico")));
}
Ok(stage)
}
pub fn pack(
project: &Project,
opts: &PackOptions,
payload: &Path,
dist: &Path,
) -> Result<Artifact, PackError> {
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 makeappx = windows_kit_tool("makeappx.exe").ok_or_else(|| {
PackError::Other(
"makeappx.exe not found — install the Windows 10/11 SDK (or set DAY_WINDOWS_KIT)"
.into(),
)
})?;
let signing = resolve_signing(project)?;
let publisher = signing.publisher.clone();
let stage = project.root.join("build/day/pack/windows-msix");
let _ = std::fs::remove_dir_all(&stage);
super::copy_tree(payload, &stage).map_err(PackError::Other)?;
let assets_dir = stage.join("Assets");
std::fs::create_dir_all(&assets_dir).map_err(|e| PackError::Other(e.to_string()))?;
let logo = project
.root
.join("icons/windows/day-icon-256.png")
.exists()
.then(|| project.root.join("icons/windows/day-icon-256.png"))
.or_else(|| {
crate::resources::app_icon(project, "gtk") });
if let Some(png) = logo {
for slot in [
"StoreLogo.png",
"Square150x150Logo.png",
"Square44x44Logo.png",
] {
let _ = std::fs::copy(&png, assets_dir.join(slot));
}
}
std::fs::write(
stage.join("AppxManifest.xml"),
appx_manifest(&project.manifest.app.id, &title, version, name, &publisher),
)
.map_err(|e| PackError::Other(e.to_string()))?;
let msix = dist.join(format!("{name}-{version}.msix"));
let _ = std::fs::remove_file(&msix);
status("Packing", "makeappx pack");
run_tool(
Command::new(&makeappx)
.args(["pack", "/o", "/d"])
.arg(&stage)
.arg("/p")
.arg(&msix),
"makeappx",
)
.map_err(PackError::Other)?;
let tier = if opts.no_sign {
status("Signing", "skipped (--no-sign)");
SignTier::Unsigned
} else {
sign_file(&signing, &msix).map_err(PackError::Sign)?
};
Ok(Artifact {
path: msix,
kind: "msix",
sha256: String::new(),
tier,
})
}
pub(crate) fn appx_manifest(
id: &str,
title: &str,
version: &str,
exe: &str,
publisher: &str,
) -> String {
let four_part = {
let mut parts: Vec<&str> = version.split(['.', '-', '+']).take(3).collect();
while parts.len() < 3 {
parts.push("0");
}
format!("{}.0", parts.join("."))
};
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="{id}" Publisher="{publisher}" Version="{four_part}" ProcessorArchitecture="x64"/>
<Properties>
<DisplayName>{title}</DisplayName>
<PublisherDisplayName>{publisher_display}</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.22621.0"/>
</Dependencies>
<Resources><Resource Language="en-us"/></Resources>
<Applications>
<Application Id="App" Executable="{exe}.exe" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements DisplayName="{title}" Description="{title}"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png" Square44x44Logo="Assets\Square44x44Logo.png"/>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust"/>
</Capabilities>
</Package>
"#,
publisher_display = publisher.trim_start_matches("CN=")
)
}
pub(crate) struct WindowsSignSettings {
pub provider: Provider,
pub publisher: String,
pub timestamp_url: String,
}
pub(crate) enum Provider {
SelfSignedDev,
CertStore {
thumbprint: String,
},
AzureArtifactSigning {
endpoint: String,
account: String,
profile: String,
dlib: String,
},
}
pub(crate) fn resolve_signing(project: &Project) -> Result<WindowsSignSettings, PackError> {
let win = project
.manifest
.signing
.as_ref()
.and_then(|s| s.windows.as_ref());
let Some(win) = win else {
return Ok(WindowsSignSettings {
provider: Provider::SelfSignedDev,
publisher: DEV_PUBLISHER.into(),
timestamp_url: String::new(),
});
};
let publisher = resolve_field(win.publisher.as_ref(), "signing.windows.publisher")?
.unwrap_or_else(|| DEV_PUBLISHER.into());
let timestamp = resolve_field(win.timestamp_url.as_ref(), "signing.windows.timestamp-url")?;
let provider = match win.provider.as_str() {
"self-signed-dev" => Provider::SelfSignedDev,
"signtool-cert-store" => {
match resolve_field(win.thumbprint.as_ref(), "signing.windows.thumbprint")? {
Some(thumbprint) => Provider::CertStore { thumbprint },
None => {
status(
"Warning",
"signtool-cert-store unresolved — self-signed dev cert",
);
Provider::SelfSignedDev
}
}
}
"azure-artifact-signing" => {
let fields = (
resolve_field(win.endpoint.as_ref(), "signing.windows.endpoint")?,
resolve_field(win.account.as_ref(), "signing.windows.account")?,
resolve_field(win.profile.as_ref(), "signing.windows.profile")?,
resolve_field(win.dlib.as_ref(), "signing.windows.dlib")?,
);
match fields {
(Some(endpoint), Some(account), Some(profile), Some(dlib)) => {
Provider::AzureArtifactSigning {
endpoint,
account,
profile,
dlib,
}
}
_ => {
status(
"Warning",
"azure-artifact-signing needs endpoint/account/profile/dlib — self-signed dev cert",
);
Provider::SelfSignedDev
}
}
}
other => {
return Err(PackError::Sign(format!(
"signing.windows.provider {other:?} — expected self-signed-dev | signtool-cert-store | azure-artifact-signing"
)));
}
};
let timestamp_url = timestamp.unwrap_or_else(|| match &provider {
Provider::AzureArtifactSigning { .. } => "http://timestamp.acs.microsoft.com".into(),
_ => "http://timestamp.digicert.com".into(),
});
Ok(WindowsSignSettings {
provider,
publisher,
timestamp_url,
})
}
fn resolve_field(v: Option<&String>, what: &str) -> Result<Option<String>, PackError> {
match v {
None => Ok(None),
Some(raw) => resolve_degradable(raw, what).map_err(PackError::Sign),
}
}
pub(crate) fn sign_file(settings: &WindowsSignSettings, file: &Path) -> Result<SignTier, String> {
let signtool = windows_kit_tool("signtool.exe")
.ok_or("signtool.exe not found — install the Windows SDK (or set DAY_WINDOWS_KIT)")?;
match &settings.provider {
Provider::SelfSignedDev => {
let thumbprint = ensure_dev_cert(&settings.publisher)?;
status("Signing", "signtool (self-signed dev cert)");
run_tool(
Command::new(&signtool)
.args(["sign", "/fd", "SHA256", "/sha1", &thumbprint])
.arg(file),
"signtool (dev)",
)?;
Ok(SignTier::DevSigned)
}
Provider::CertStore { thumbprint } => {
status("Signing", "signtool (certificate store)");
run_tool(
Command::new(&signtool)
.args(["sign", "/fd", "SHA256", "/sha1", thumbprint])
.args(["/tr", &settings.timestamp_url, "/td", "SHA256"])
.arg(file),
"signtool",
)?;
Ok(SignTier::Release)
}
Provider::AzureArtifactSigning {
endpoint,
account,
profile,
dlib,
} => {
let metadata = file.with_extension("signing-metadata.json");
std::fs::write(
&metadata,
serde_json::json!({
"Endpoint": endpoint,
"CodeSigningAccountName": account,
"CertificateProfileName": profile,
})
.to_string(),
)
.map_err(|e| e.to_string())?;
status("Signing", "signtool (Azure Artifact Signing)");
let result = run_tool(
Command::new(&signtool)
.args(["sign", "/v", "/fd", "SHA256"])
.args(["/tr", &settings.timestamp_url, "/td", "SHA256"])
.arg("/dlib")
.arg(dlib)
.arg("/dmdf")
.arg(&metadata)
.arg(file),
"signtool (azure)",
);
let _ = std::fs::remove_file(&metadata);
result?;
Ok(SignTier::Release)
}
}
}
fn ensure_dev_cert(publisher: &str) -> Result<String, String> {
let script = format!(
"$c = Get-ChildItem Cert:\\CurrentUser\\My | Where-Object {{ $_.Subject -eq '{publisher}' }} | Select-Object -First 1; \
if (-not $c) {{ $c = New-SelfSignedCertificate -Type Custom -Subject '{publisher}' -KeyUsage DigitalSignature \
-FriendlyName 'Day dev signing' -CertStoreLocation Cert:\\CurrentUser\\My \
-TextExtension @('2.5.29.37={{text}}1.3.6.1.5.5.7.3.3','2.5.29.19={{text}}') }}; \
$c.Thumbprint"
);
let out = Command::new("powershell")
.args(["-NoProfile", "-NonInteractive", "-Command", &script])
.output()
.map_err(|e| format!("powershell: {e}"))?;
if !out.status.success() {
return Err(format!(
"dev cert creation failed:\n{}",
String::from_utf8_lossy(&out.stderr)
));
}
let thumb = String::from_utf8_lossy(&out.stdout).trim().to_string();
if thumb.len() != 40 {
return Err(format!("unexpected thumbprint output: {thumb:?}"));
}
Ok(thumb)
}
pub(crate) fn windows_kit_tool(tool: &str) -> Option<PathBuf> {
day_toolchain::windows_kit_tool(tool)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn appx_manifest_shape() {
let m = appx_manifest(
"dev.daybrite.showcase",
"Day Showcase",
"0.1.0",
"showcase",
"CN=Day Development",
);
assert!(m.contains(r#"Version="0.1.0.0""#));
assert!(m.contains(r#"Publisher="CN=Day Development""#));
assert!(m.contains(r#"Executable="showcase.exe""#));
assert!(m.contains("runFullTrust"));
assert!(m.contains("<PublisherDisplayName>Day Development</PublisherDisplayName>"));
}
}