diff --git a/.cargo/config.toml b/.cargo/config.toml index 806c651d..be37477a 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -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" diff --git a/.github/workflows/ci-windows-installer.yml b/.github/workflows/ci-windows-installer.yml index 7031f31e..b0660434 100644 --- a/.github/workflows/ci-windows-installer.yml +++ b/.github/workflows/ci-windows-installer.yml @@ -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 != '' @@ -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 != '' diff --git a/Cargo.toml b/Cargo.toml index 3b1635be..4e86a7a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 } diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 3fbba2d8..678d5787 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -48,6 +48,10 @@ fn run() -> Result<(), Box> { args.remove(0); new_migration(args) } + "msi-version" => { + args.remove(0); + msi_version(args) + } "-h" | "--help" | "help" => { print_usage(); Ok(()) @@ -160,6 +164,75 @@ fn new_migration(args: Vec) -> Result<(), Box> { Ok(()) } +fn msi_version(args: Vec) -> Result<(), Box> { + 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> { + 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::>(); + 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> { + 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> { let current_dir = env::current_dir()?; if current_dir.join("Cargo.toml").exists() && current_dir.join("crates/server").exists() { @@ -373,6 +446,7 @@ fn run_rustfmt(repo_root: &Path) -> Result<(), Box> { fn print_usage() { println!("Usage:"); println!(" cargo new-migration [name] [--version ] [--no-fmt]"); + println!(" cargo msi-version "); } fn print_new_migration_usage() { @@ -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 "); + 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()); + } +}