Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

[alias]
new-migration = "run --quiet -p xtask -- new-migration"
msi-version = "run --quiet -p xtask -- msi-version"

# [target.x86_64-unknown-linux-gnu]
# linker = "x86_64-linux-gnu-gcc"
Expand Down
49 changes: 39 additions & 10 deletions .github/workflows/ci-windows-installer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ jobs:
if ($env:INPUT_RELEASE_VERSION) {
cargo bin cargo-set-version --version
}
cargo bin cargo-wix --version

- name: Update Version
if: env.INPUT_RELEASE_VERSION != ''
Expand Down Expand Up @@ -128,18 +127,48 @@ jobs:
".\assets" `
".\target\${{ matrix.target }}\release\koko.exe"

- name: Resolve MSI package version
shell: pwsh
run: |
$metadata = cargo metadata --locked --no-deps --format-version 1 | ConvertFrom-Json
$packageVersion = ($metadata.packages | Where-Object { $_.name -eq 'koko' } | Select-Object -First 1).version
if (-not $packageVersion) {
throw 'Failed to resolve koko package version from cargo metadata'
}
$msiPackageVersion = cargo msi-version $packageVersion
"MSI_PACKAGE_VERSION=$msiPackageVersion" >> $env:GITHUB_ENV
"Package version: $packageVersion"
"MSI package version: $msiPackageVersion"

- name: Package Windows installer
shell: pwsh
run: |
cargo bin cargo-wix `
--toolset modern `
--target ${{ matrix.target }} `
--no-build `
--target-bin-dir "${{ github.workspace }}\target\${{ matrix.target }}\release" `
--package koko `
--output "artifacts\koko-${{ matrix.target }}-installer.msi" `
--nocapture `
crates/server/Cargo.toml
$target = '${{ matrix.target }}'
$wixArch = switch -Wildcard ($target) {
'aarch64-*' { 'arm64'; break }
'x86_64-*' { 'x64'; break }
default { throw "Unsupported WiX target architecture for $target" }
}
$cargoTargetDir = Join-Path $env:GITHUB_WORKSPACE 'target'
$cargoTargetBinDir = Join-Path $cargoTargetDir "$target\release"

wix build `
-arch $wixArch `
-d "Version=$env:MSI_PACKAGE_VERSION" `
-d "Platform=$wixArch" `
-d "Profile=release" `
-d 'TargetEnv=msvc' `
-d "TargetTriple=$target" `
-d 'CargoProfile=release' `
-d 'TargetVendor=pc' `
-d "CargoTargetDir=$cargoTargetDir" `
-d "CargoTargetBinDir=$cargoTargetBinDir" `
-pdbtype none `
-culture en-us `
-ext WixToolset.UI.wixext `
-ext WixToolset.Util.wixext `
-o "artifacts\koko-$target-installer.msi" `
crates\server\wix\main.wxs

- name: Sign Windows installer
if: env.INPUT_PUBLISH_RELEASE == 'true' && env.INPUT_AZURE_SIGNING_ACCOUNT != ''
Expand Down
1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,3 @@ cargo-run-bin = "=1.7.5"
cargo-deny = { version = "=0.19.9", locked = true }
cargo-edit = { version = "=0.13.11", bins = ["cargo-set-version"], locked = true }
cargo-tarpaulin = { version = "=0.35.5", locked = true }
cargo-wix = { version = "=0.3.9", git = "https://github.com/volks73/cargo-wix.git", rev = "fde983c2e901970267e76b8fd68120fdd5457a57", locked = true }
118 changes: 118 additions & 0 deletions xtask/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ fn run() -> Result<(), Box<dyn Error>> {
args.remove(0);
new_migration(args)
}
"msi-version" => {
args.remove(0);
msi_version(args)
}
"-h" | "--help" | "help" => {
print_usage();
Ok(())
Expand Down Expand Up @@ -160,6 +164,75 @@ fn new_migration(args: Vec<String>) -> Result<(), Box<dyn Error>> {
Ok(())
}

fn msi_version(args: Vec<String>) -> Result<(), Box<dyn Error>> {
let mut version = None;

for arg in args {
match arg.as_str() {
"-h" | "--help" => {
print_msi_version_usage();
return Ok(());
}
_ if arg.starts_with('-') => return Err(format!("unknown option `{arg}`").into()),
_ if version.is_none() => version = Some(arg),
_ => return Err(format!("unexpected extra argument `{arg}`").into()),
}
}

let version = version.ok_or("missing version")?;
println!("{}", msi_package_version(&version)?);

Ok(())
}

fn msi_package_version(version: &str) -> Result<String, Box<dyn Error>> {
let version = version.trim().strip_prefix('v').unwrap_or(version.trim());
if version.is_empty() {
return Err("version cannot be empty".into());
}
if version.contains('-') || version.contains('+') {
return Err(
"MSI package versions must be derived from a release version without prerelease or \
build metadata"
.into(),
);
}

let parts = version.split('.').collect::<Vec<_>>();
if parts.len() != 3 {
return Err(format!("version `{version}` must have exactly three numeric parts").into());
}

let major = parse_version_part(parts[0], "major")?;
let minor = parse_version_part(parts[1], "minor")?;
let patch = parse_version_part(parts[2], "patch")?;

// Match Sunshine's four-field Windows package shape by splitting HHMMSS.
// WiX warns when CalVer exceeds MSI ProductVersion limits, but cargo-wix
// fails before WiX can build the installer.
let build = patch / 100;
let revision = patch % 100;

Ok(format!("{major}.{minor}.{build}.{revision}"))
}

fn parse_version_part(
part: &str,
label: &str,
) -> Result<u64, Box<dyn Error>> {
if part.is_empty() {
return Err(format!("{label} version part cannot be empty").into());
}
if part.len() > 1 && part.starts_with('0') {
return Err(format!("{label} version part `{part}` must not contain leading zeros").into());
}
if !part.chars().all(|character| character.is_ascii_digit()) {
return Err(format!("{label} version part `{part}` must be numeric").into());
}

Ok(part.parse()?)
}

fn repo_root() -> Result<PathBuf, Box<dyn Error>> {
let current_dir = env::current_dir()?;
if current_dir.join("Cargo.toml").exists() && current_dir.join("crates/server").exists() {
Expand Down Expand Up @@ -373,6 +446,7 @@ fn run_rustfmt(repo_root: &Path) -> Result<(), Box<dyn Error>> {
fn print_usage() {
println!("Usage:");
println!(" cargo new-migration [name] [--version <hex>] [--no-fmt]");
println!(" cargo msi-version <version>");
}

fn print_new_migration_usage() {
Expand All @@ -383,3 +457,47 @@ fn print_new_migration_usage() {
println!(" cargo new-migration add_media_flags");
println!(" cargo new-migration add_media_flags --version facefeed1234");
}

fn print_msi_version_usage() {
println!("Usage:");
println!(" cargo msi-version <version>");
println!();
println!("Examples:");
println!(" cargo msi-version 2026.624.125525");
}

#[cfg(test)]
mod tests {
use super::msi_package_version;

#[test]
fn msi_package_version_splits_calver_time() {
assert_eq!(
msi_package_version("2026.624.125525").unwrap(),
"2026.624.1255.25"
);
}

#[test]
fn msi_package_version_allows_v_prefix() {
assert_eq!(
msi_package_version("v2026.622.124200").unwrap(),
"2026.622.1242.0"
);
}

#[test]
fn msi_package_version_handles_workspace_default() {
assert_eq!(msi_package_version("0.0.0").unwrap(), "0.0.0.0");
}

#[test]
fn msi_package_version_rejects_prerelease_versions() {
assert!(msi_package_version("2026.624.125525-beta.1").is_err());
}

#[test]
fn msi_package_version_rejects_leading_zeros() {
assert!(msi_package_version("2026.0624.125525").is_err());
}
}
Loading