diff --git a/.github/workflows/windows-store.yml b/.github/workflows/windows-store.yml new file mode 100644 index 0000000..357f313 --- /dev/null +++ b/.github/workflows/windows-store.yml @@ -0,0 +1,157 @@ +name: Windows Store package + +on: + pull_request: + paths: + - ".github/workflows/windows-store.yml" + - "packaging/windows/**" + - "assets/windows/**" + - "desktopApp/**" + - "shared/**" + - "crates/vnidrop/**" + - "Cargo.toml" + - "Cargo.lock" + - "build.gradle.kts" + - "settings.gradle.kts" + - "gradle.properties" + - "gradle/**" + - "gradlew" + - "gradlew.bat" + push: + tags: + - "v*.*.*" + workflow_dispatch: + inputs: + version: + description: Release version in MAJOR.MINOR.PATCH form + required: true + default: "1.0.0" + type: string + +permissions: + contents: read + +concurrency: + group: windows-store-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + build-msix: + name: Build unsigned Store MSIX (x64) + runs-on: windows-2025 + timeout-minutes: 90 + env: + CARGO_TERM_COLOR: always + + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Set up JDK 21 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + distribution: temurin + java-version: "21.0.11+10" + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 + + - name: Set up Rust 1.91 + shell: pwsh + run: | + rustup toolchain install 1.91.0-x86_64-pc-windows-msvc --profile minimal + rustup default 1.91.0-x86_64-pc-windows-msvc + rustc --version --verbose + cargo --version + + - name: Cache Cargo + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: windows-x64-cargo-1.91.0-${{ hashFiles('Cargo.lock') }} + restore-keys: | + windows-x64-cargo-1.91.0- + + - name: Resolve Store version + id: version + shell: pwsh + env: + REQUESTED_VERSION: ${{ inputs.version || '1.0.0' }} + run: | + $version = $env:REQUESTED_VERSION + if ($env:GITHUB_REF_TYPE -eq "tag") { + if ($env:GITHUB_REF_NAME -notmatch "^v[0-9]+\.[0-9]+\.[0-9]+$") { + throw "Store release tags must use vMAJOR.MINOR.PATCH" + } + $version = $env:GITHUB_REF_NAME.Substring(1) + } + + if ($version -notmatch "^[0-9]+\.[0-9]+\.[0-9]+$") { + throw "Version must use MAJOR.MINOR.PATCH" + } + $parts = $version.Split(".") + for ($index = 0; $index -lt $parts.Count; $index++) { + $part = $parts[$index] + $number = 0 + if (-not [int]::TryParse($part, [ref] $number) -or $number.ToString() -ne $part) { + throw "Version components must be canonical integers" + } + if ($number -lt $(if ($index -eq 0) { 1 } else { 0 }) -or $number -gt 65535) { + throw "Version components must be between 0 and 65535, with a non-zero major" + } + } + + "app=$version" >> $env:GITHUB_OUTPUT + "package=$version.0" >> $env:GITHUB_OUTPUT + + - name: Test and build release app image + shell: pwsh + run: | + $arguments = @( + ":shared:jvmTest" + ":desktopApp:createReleaseDistributable" + "-Pvnidrop.version=${{ steps.version.outputs.app }}" + "-Pvnidrop.desktop.rustVariant=release" + "-Pvnidrop.diagnostics.included=false" + "--no-daemon" + "--no-configuration-cache" + "--stacktrace" + ) + & .\gradlew.bat @arguments + if ($LASTEXITCODE -ne 0) { + throw "Gradle release build failed with exit code $LASTEXITCODE" + } + + - name: Create and validate Store package + shell: pwsh + run: | + $arguments = @( + "-Version", "${{ steps.version.outputs.app }}" + "-AppImage", ".\desktopApp\build\compose\binaries\main-release\app\VniDrop" + "-OutputDirectory", ".\build\release\windows" + ) + & .\packaging\windows\build-msix.ps1 @arguments + + - name: Upload Store artifacts + if: github.event_name != 'pull_request' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: vnidrop-${{ steps.version.outputs.app }}-windows-store-x64 + path: build/release/windows/ + if-no-files-found: error + retention-days: 90 + compression-level: 0 + + - name: Summarize package + shell: pwsh + run: | + "### Windows Store package" >> $env:GITHUB_STEP_SUMMARY + "- App version: ${{ steps.version.outputs.app }}" >> $env:GITHUB_STEP_SUMMARY + "- MSIX version: ${{ steps.version.outputs.package }}" >> $env:GITHUB_STEP_SUMMARY + "- Architecture: x64" >> $env:GITHUB_STEP_SUMMARY + "- Signing: unsigned Store submission; Microsoft signs after certification" >> $env:GITHUB_STEP_SUMMARY diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts index deeb310..d1b896a 100644 --- a/desktopApp/build.gradle.kts +++ b/desktopApp/build.gradle.kts @@ -6,6 +6,20 @@ plugins { alias(libs.plugins.composeCompiler) } +val appVersion = providers.gradleProperty("vnidrop.version").get() +val appVersionParts = appVersion.split(".") +require( + appVersionParts.size == 3 && + appVersionParts.mapIndexed { index, part -> + val number = part.toIntOrNull() + number != null && + number.toString() == part && + number in (if (index == 0) 1 else 0)..65535 + }.all { it }, +) { + "vnidrop.version must be MAJOR.MINOR.PATCH with numeric components from 0 to 65535 and a non-zero major" +} + dependencies { implementation(projects.shared) @@ -20,18 +34,23 @@ dependencies { compose.desktop { application { mainClass = "com.vnidrop.app.MainKt" + buildTypes.release.proguard.isEnabled.set(false) nativeDistributions { - targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) - packageName = "com.vnidrop.app" - packageVersion = "1.0.0" + targetFormats(TargetFormat.Dmg, TargetFormat.Deb) + packageName = "VniDrop" + packageVersion = appVersion + description = "Send files directly across your devices" + vendor = "Sudosy Labs" macOS { + bundleID = "com.vnidrop.app" iconFile.set(project.file("../assets/macos/app-icon.icns")) } windows { iconFile.set(project.file("../assets/windows/app-icon.ico")) } linux { + packageName = "vnidrop" iconFile.set(project.file("../assets/linux/app-icon.png")) } fileAssociation( diff --git a/gradle.properties b/gradle.properties index 34d6c2f..35d3b69 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,6 +6,11 @@ kotlin.mpp.enableCInteropCommonization=true org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8 org.gradle.configuration-cache=true org.gradle.caching=true + +# Product version used by desktop packaging. Release workflows override this +# from the vMAJOR.MINOR.PATCH tag. +vnidrop.version=1.0.0 + #Android android.builtInKotlin=false android.newDsl=false diff --git a/packaging/windows/AppxManifest.xml b/packaging/windows/AppxManifest.xml new file mode 100644 index 0000000..5c00254 --- /dev/null +++ b/packaging/windows/AppxManifest.xml @@ -0,0 +1,56 @@ + + + + + Vnidrop + Sudosy Labs + Send files directly across your devices. + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + VniDrop Invitation + Assets\Square44x44Logo.png + + .vnd + + + + + + + diff --git a/packaging/windows/Assets/Square150x150Logo.png b/packaging/windows/Assets/Square150x150Logo.png new file mode 100644 index 0000000..1a4478a Binary files /dev/null and b/packaging/windows/Assets/Square150x150Logo.png differ diff --git a/packaging/windows/Assets/Square44x44Logo.png b/packaging/windows/Assets/Square44x44Logo.png new file mode 100644 index 0000000..f610f22 Binary files /dev/null and b/packaging/windows/Assets/Square44x44Logo.png differ diff --git a/packaging/windows/Assets/Square44x44Logo.targetsize-16_altform-unplated.png b/packaging/windows/Assets/Square44x44Logo.targetsize-16_altform-unplated.png new file mode 100644 index 0000000..ac16868 Binary files /dev/null and b/packaging/windows/Assets/Square44x44Logo.targetsize-16_altform-unplated.png differ diff --git a/packaging/windows/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/packaging/windows/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 0000000..5707cd7 Binary files /dev/null and b/packaging/windows/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/packaging/windows/Assets/Square44x44Logo.targetsize-256_altform-unplated.png b/packaging/windows/Assets/Square44x44Logo.targetsize-256_altform-unplated.png new file mode 100644 index 0000000..f9c0543 Binary files /dev/null and b/packaging/windows/Assets/Square44x44Logo.targetsize-256_altform-unplated.png differ diff --git a/packaging/windows/Assets/Square44x44Logo.targetsize-32_altform-unplated.png b/packaging/windows/Assets/Square44x44Logo.targetsize-32_altform-unplated.png new file mode 100644 index 0000000..677abb4 Binary files /dev/null and b/packaging/windows/Assets/Square44x44Logo.targetsize-32_altform-unplated.png differ diff --git a/packaging/windows/Assets/Square44x44Logo.targetsize-44_altform-unplated.png b/packaging/windows/Assets/Square44x44Logo.targetsize-44_altform-unplated.png new file mode 100644 index 0000000..14a9578 Binary files /dev/null and b/packaging/windows/Assets/Square44x44Logo.targetsize-44_altform-unplated.png differ diff --git a/packaging/windows/Assets/Square44x44Logo.targetsize-48_altform-unplated.png b/packaging/windows/Assets/Square44x44Logo.targetsize-48_altform-unplated.png new file mode 100644 index 0000000..833b185 Binary files /dev/null and b/packaging/windows/Assets/Square44x44Logo.targetsize-48_altform-unplated.png differ diff --git a/packaging/windows/Assets/StoreLogo.png b/packaging/windows/Assets/StoreLogo.png new file mode 100644 index 0000000..55c7ab8 Binary files /dev/null and b/packaging/windows/Assets/StoreLogo.png differ diff --git a/packaging/windows/README.md b/packaging/windows/README.md new file mode 100644 index 0000000..a7e87b0 --- /dev/null +++ b/packaging/windows/README.md @@ -0,0 +1,105 @@ +# Windows Microsoft Store packaging + +This directory turns the Compose Desktop Windows app image into the unsigned +MSIX artifacts accepted by Partner Center. Microsoft signs the package after +certification, so this build does not use a PFX, certificate, HSM, or signing +secret. + +## Product identity + +These values came from the Partner Center Product identity page and are +case-sensitive: + +| Field | Value | +| --- | --- | +| Package identity name | SudosyLabs.Vnidrop | +| Publisher | CN=6456DC8E-2C31-44BD-AACC-2E6813C833CB | +| Publisher display name | Sudosy Labs | +| Reserved Store name | Vnidrop | +| Application ID | VniDrop | +| Store ID | 9NJ5Q0FG7TGL | + +The package identity name, publisher, and Application ID must remain stable +after the first release. The manifest display name uses the exact reserved Store +name; the product's in-app branding and launcher remain `VniDrop`. + +The initial package targets Windows Desktop x64, Windows 10 version 2004 +(build 19041) or later. The fourth MSIX version component is reserved by the +Store, so app version 1.2.3 becomes package version 1.2.3.0. + +## GitHub Actions + +The Windows Store package workflow runs automatically for relevant pull +requests, for release tags matching vMAJOR.MINOR.PATCH, and by manual dispatch. +Pull requests build and validate without retaining an artifact. Tags and manual +runs retain: + +- VniDrop_VERSION_x64.msix +- VniDrop_VERSION_x64.msixupload +- build metadata +- SHA-256 checksums + +The preferred Partner Center upload is the msixupload file. It is an upload +envelope containing the x64 MSIX. The MSIX is intentionally unsigned and is not +a public sideloading artifact. Do not attach it to a public GitHub Release +unless an independent production-signing path is added. + +The workflow explicitly selects Gobley's release Rust variant and rejects a +package containing the debug native JAR. It also verifies the bundled JVM, +vnidrop.dll, app version, manifest identity, architecture, and launcher after +MakeAppx unpacks the finished package. + +## First Store release + +Microsoft's current GitHub Actions publishing flow is for updates to an +already-live free product. For the first release: + +1. Run this workflow from a release tag or by manual dispatch. +2. Download the retained artifact. +3. Test that exact build on an interactive Windows VM. Local installation needs + an ephemeral development signature trusted only by that VM; this is not a + production signing key. +4. Upload the msixupload file to the current Partner Center draft. +5. Confirm that Partner Center parses the expected identity, version, x64 + architecture, Windows.Desktop target, en-US language, and runFullTrust + capability. +6. Complete listing, screenshots, certification notes, and submit. + +Use this restricted-capability justification in Submission options: + +> VniDrop is a classic JVM desktop application that loads its bundled native +> Rust and JVM libraries and needs normal user-level filesystem and network +> access to transfer user-selected files directly between devices. + +After the first release is certified and live, Store publication can be added +as a separate protected job. Keep its Partner Center credentials in a GitHub +Environment, not in this build job: + +- AZURE_AD_TENANT_ID +- AZURE_AD_APPLICATION_CLIENT_ID +- AZURE_AD_APPLICATION_SECRET +- SELLER_ID + +The Store ID is a non-secret variable. + +## Manual build on Windows + +From the repository root: + +~~~powershell +.\gradlew.bat :shared:jvmTest :desktopApp:createReleaseDistributable -Pvnidrop.version=1.0.0 -Pvnidrop.desktop.rustVariant=release -Pvnidrop.diagnostics.included=false --no-daemon --no-configuration-cache --stacktrace + +.\packaging\windows\build-msix.ps1 -Version 1.0.0 -AppImage .\desktopApp\build\compose\binaries\main-release\app\VniDrop -OutputDirectory .\build\release\windows +~~~ + +The packaging script requires Windows SDK 10.0.26100.0. It uses MakePri to +index the scale-qualified visual assets, then MakeAppx with SHA-256 block maps +and manifest validation enabled. + +Microsoft references: + +- [MSIX Store package requirements](https://learn.microsoft.com/en-us/windows/apps/publish/publish-your-app/msix/app-package-requirements) +- [Manual desktop MSIX packaging](https://learn.microsoft.com/en-us/windows/msix/desktop/desktop-to-uwp-manual-conversion) +- [MakeAppx](https://learn.microsoft.com/en-us/windows/msix/package/create-app-package-with-makeappx-tool) +- [Uploading MSIX packages](https://learn.microsoft.com/en-us/windows/apps/publish/publish-your-app/msix/upload-app-packages) +- [GitHub Actions Store updates](https://learn.microsoft.com/en-us/windows/apps/publish/msstore-dev-cli/github-actions) diff --git a/packaging/windows/build-msix.ps1 b/packaging/windows/build-msix.ps1 new file mode 100644 index 0000000..dfe7027 --- /dev/null +++ b/packaging/windows/build-msix.ps1 @@ -0,0 +1,280 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string] $Version, + + [Parameter(Mandatory)] + [string] $AppImage, + + [Parameter(Mandatory)] + [string] $OutputDirectory, + + [string] $WindowsSdkVersion = "10.0.26100.0" +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +$ProgressPreference = "SilentlyContinue" + +function Assert-Condition { + param( + [bool] $Condition, + [string] $Message + ) + + if (-not $Condition) { + throw $Message + } +} + +function Invoke-Checked { + param( + [string] $FilePath, + [string[]] $Arguments + ) + + & $FilePath @Arguments + if ($LASTEXITCODE -ne 0) { + throw "$FilePath failed with exit code $LASTEXITCODE" + } +} + +function Read-ZipEntry { + param( + [string] $ArchivePath, + [string] $EntryPath + ) + + $archive = [System.IO.Compression.ZipFile]::OpenRead($ArchivePath) + try { + $entry = $archive.GetEntry($EntryPath) + if ($null -eq $entry) { + throw "$ArchivePath does not contain $EntryPath" + } + $reader = [System.IO.StreamReader]::new($entry.Open()) + try { + return $reader.ReadToEnd() + } + finally { + $reader.Dispose() + } + } + finally { + $archive.Dispose() + } +} + +function Get-ZipEntryLength { + param( + [string] $ArchivePath, + [string] $EntryPath + ) + + $archive = [System.IO.Compression.ZipFile]::OpenRead($ArchivePath) + try { + $entry = $archive.GetEntry($EntryPath) + if ($null -eq $entry) { + throw "$ArchivePath does not contain $EntryPath" + } + return $entry.Length + } + finally { + $archive.Dispose() + } +} + +if ([System.Environment]::OSVersion.Platform -ne [System.PlatformID]::Win32NT) { + throw "MSIX packaging must run on Windows" +} + +$versionParts = $Version.Split(".") +Assert-Condition ($versionParts.Count -eq 3) "Version must use MAJOR.MINOR.PATCH" +for ($index = 0; $index -lt $versionParts.Count; $index++) { + $part = $versionParts[$index] + $number = 0 + Assert-Condition ([int]::TryParse($part, [ref] $number)) "Version components must be integers" + Assert-Condition ($number.ToString() -eq $part) "Version components must not contain leading zeroes" + Assert-Condition ($number -ge $(if ($index -eq 0) { 1 } else { 0 }) -and $number -le 65535) "Version components must be between 0 and 65535, with a non-zero major" +} +$packageVersion = "$Version.0" + +$appImagePath = (Resolve-Path -LiteralPath $AppImage).Path +Assert-Condition (Test-Path -LiteralPath $appImagePath -PathType Container) "App image not found: $AppImage" +Assert-Condition (Test-Path -LiteralPath (Join-Path $appImagePath "VniDrop.exe") -PathType Leaf) "The app image does not contain VniDrop.exe" +Assert-Condition (Test-Path -LiteralPath (Join-Path $appImagePath "runtime\bin\server\jvm.dll") -PathType Leaf) "The app image does not contain its bundled JVM" + +Add-Type -AssemblyName System.IO.Compression.FileSystem +$appFiles = @(Get-ChildItem -LiteralPath $appImagePath -Recurse -File) +$debugRustJars = @($appFiles | Where-Object { $_.Name -match "^shared-win32-x86-64-debug-.+\.jar$" }) +Assert-Condition ($debugRustJars.Count -eq 0) "The app image contains a debug Rust runtime JAR" +$releaseRustJars = @($appFiles | Where-Object { $_.Name -match "^shared-win32-x86-64-(?!debug-).+\.jar$" }) +Assert-Condition ($releaseRustJars.Count -eq 1) "Expected exactly one release Rust runtime JAR" +$nativeDllLength = Get-ZipEntryLength -ArchivePath $releaseRustJars[0].FullName -EntryPath "win32-x86-64/vnidrop.dll" +Assert-Condition ($nativeDllLength -gt 0) "The release Rust runtime JAR contains an empty vnidrop.dll" + +$sharedJars = @($appFiles | Where-Object { $_.Name -match "^shared-jvm-.+\.jar$" }) +Assert-Condition ($sharedJars.Count -eq 1) "Expected exactly one shared JVM JAR" +$sharedManifest = Read-ZipEntry -ArchivePath $sharedJars[0].FullName -EntryPath "META-INF/MANIFEST.MF" +Assert-Condition ($sharedManifest -match "(?m)^Implementation-Version: $([regex]::Escape($Version))\r?$") "The packaged app version does not match $Version" + +$programFilesX86 = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::ProgramFilesX86) +$makeAppxPath = Join-Path $programFilesX86 "Windows Kits\10\bin\$WindowsSdkVersion\x64\MakeAppx.exe" +$makePriPath = Join-Path $programFilesX86 "Windows Kits\10\bin\$WindowsSdkVersion\x64\MakePri.exe" +Assert-Condition (Test-Path -LiteralPath $makeAppxPath -PathType Leaf) "MakeAppx.exe from Windows SDK $WindowsSdkVersion was not found" +Assert-Condition (Test-Path -LiteralPath $makePriPath -PathType Leaf) "MakePri.exe from Windows SDK $WindowsSdkVersion was not found" + +$outputPath = [System.IO.Path]::GetFullPath($OutputDirectory) +[System.IO.Directory]::CreateDirectory($outputPath) | Out-Null +$artifactBaseName = "VniDrop_" + $Version + "_x64" +$msixPath = Join-Path $outputPath "$artifactBaseName.msix" +$uploadPath = Join-Path $outputPath "$artifactBaseName.msixupload" +$buildInfoPath = Join-Path $outputPath "$artifactBaseName.build-info.json" +$checksumsPath = Join-Path $outputPath "SHA256SUMS" +@($msixPath, $uploadPath, $buildInfoPath, $checksumsPath) | + Where-Object { Test-Path -LiteralPath $_ } | + ForEach-Object { Remove-Item -LiteralPath $_ -Force } + +$stageRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("vnidrop-msix-" + [System.Guid]::NewGuid().ToString("N")) +$packageRoot = Join-Path $stageRoot "package" +$unpackedRoot = Join-Path $stageRoot "unpacked" +$priConfigPath = Join-Path $stageRoot "priconfig.xml" +[System.IO.Directory]::CreateDirectory($packageRoot) | Out-Null + +try { + Get-ChildItem -LiteralPath $appImagePath -Force | Copy-Item -Destination $packageRoot -Recurse -Force + Copy-Item -LiteralPath (Join-Path $PSScriptRoot "Assets") -Destination $packageRoot -Recurse -Force + + $manifestTemplate = Get-Content -LiteralPath (Join-Path $PSScriptRoot "AppxManifest.xml") -Raw + Assert-Condition (([regex]::Matches($manifestTemplate, "__VERSION__")).Count -eq 1) "AppxManifest.xml must contain exactly one __VERSION__ placeholder" + $manifestText = $manifestTemplate.Replace("__VERSION__", $packageVersion) + [System.IO.File]::WriteAllText( + (Join-Path $packageRoot "AppxManifest.xml"), + $manifestText, + [System.Text.UTF8Encoding]::new($false) + ) + + Invoke-Checked -FilePath $makePriPath -Arguments @( + "createconfig", + "/cf", $priConfigPath, + "/dq", "en-US", + "/o" + ) + Invoke-Checked -FilePath $makePriPath -Arguments @( + "new", + "/pr", $packageRoot, + "/cf", $priConfigPath, + "/mn", (Join-Path $packageRoot "AppxManifest.xml"), + "/of", (Join-Path $packageRoot "resources.pri"), + "/o" + ) + Assert-Condition ((Get-Item -LiteralPath (Join-Path $packageRoot "resources.pri")).Length -gt 0) "MakePri created an empty resources.pri" + + Invoke-Checked -FilePath $makeAppxPath -Arguments @( + "pack", + "/v", + "/h", "SHA256", + "/d", $packageRoot, + "/p", $msixPath, + "/o" + ) + Invoke-Checked -FilePath $makeAppxPath -Arguments @( + "unpack", + "/v", + "/p", $msixPath, + "/d", $unpackedRoot, + "/o" + ) + + [xml] $manifest = Get-Content -LiteralPath (Join-Path $unpackedRoot "AppxManifest.xml") -Raw + $namespaces = [System.Xml.XmlNamespaceManager]::new($manifest.NameTable) + $namespaces.AddNamespace("f", "http://schemas.microsoft.com/appx/manifest/foundation/windows10") + $namespaces.AddNamespace("uap", "http://schemas.microsoft.com/appx/manifest/uap/windows10") + $namespaces.AddNamespace("uap10", "http://schemas.microsoft.com/appx/manifest/uap/windows10/10") + $namespaces.AddNamespace("rescap", "http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities") + $identity = $manifest.SelectSingleNode("/f:Package/f:Identity", $namespaces) + Assert-Condition ($null -ne $identity) "The packed manifest has no Identity" + Assert-Condition ($identity.GetAttribute("Name") -eq "SudosyLabs.Vnidrop") "The packed package identity name is incorrect" + Assert-Condition ($identity.GetAttribute("Publisher") -eq "CN=6456DC8E-2C31-44BD-AACC-2E6813C833CB") "The packed publisher identity is incorrect" + Assert-Condition ($identity.GetAttribute("Version") -eq $packageVersion) "The packed package version is incorrect" + Assert-Condition ($identity.GetAttribute("ProcessorArchitecture") -eq "x64") "The packed package architecture is not x64" + Assert-Condition ($manifest.SelectSingleNode("/f:Package/f:Properties/f:DisplayName", $namespaces).InnerText -eq "Vnidrop") "The packed display name does not match the reserved Store name" + Assert-Condition ($manifest.SelectSingleNode("/f:Package/f:Properties/f:PublisherDisplayName", $namespaces).InnerText -eq "Sudosy Labs") "The packed publisher display name is incorrect" + $targetFamily = $manifest.SelectSingleNode("/f:Package/f:Dependencies/f:TargetDeviceFamily", $namespaces) + Assert-Condition ($null -ne $targetFamily) "The packed manifest has no target device family" + Assert-Condition ($targetFamily.GetAttribute("Name") -eq "Windows.Desktop") "The packed package does not target Windows.Desktop" + Assert-Condition ($null -ne $manifest.SelectSingleNode("/f:Package/f:Capabilities/rescap:Capability[@Name='runFullTrust']", $namespaces)) "The packed package does not declare runFullTrust" + + $application = $manifest.SelectSingleNode("/f:Package/f:Applications/f:Application", $namespaces) + Assert-Condition ($null -ne $application) "The packed manifest has no Application" + Assert-Condition ($application.GetAttribute("Id") -eq "VniDrop") "The packed application ID is incorrect" + $executable = $application.GetAttribute("Executable") + Assert-Condition ($executable -eq "VniDrop.exe") "The packed manifest executable is incorrect" + Assert-Condition ($application.GetAttribute("RuntimeBehavior", "http://schemas.microsoft.com/appx/manifest/uap/windows10/10") -eq "packagedClassicApp") "The packed runtime behavior is incorrect" + Assert-Condition ($application.GetAttribute("TrustLevel", "http://schemas.microsoft.com/appx/manifest/uap/windows10/10") -eq "mediumIL") "The packed trust level is incorrect" + Assert-Condition ($manifest.SelectSingleNode("/f:Package/f:Applications/f:Application/f:Extensions/uap:Extension/uap:FileTypeAssociation/uap:SupportedFileTypes/uap:FileType[text()='.vnd']", $namespaces) -ne $null) "The packed package is missing the .vnd file association" + Assert-Condition (Test-Path -LiteralPath (Join-Path $unpackedRoot $executable) -PathType Leaf) "The packed executable is missing" + Assert-Condition (Test-Path -LiteralPath (Join-Path $unpackedRoot "resources.pri") -PathType Leaf) "The packed resource index is missing" +} +finally { + if (Test-Path -LiteralPath $stageRoot) { + Remove-Item -LiteralPath $stageRoot -Recurse -Force + } +} + +$temporaryZip = Join-Path $outputPath "$artifactBaseName.zip" +if (Test-Path -LiteralPath $temporaryZip) { + Remove-Item -LiteralPath $temporaryZip -Force +} +Compress-Archive -LiteralPath $msixPath -DestinationPath $temporaryZip -CompressionLevel Optimal +Move-Item -LiteralPath $temporaryZip -Destination $uploadPath + +$repoRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..\..")).Path +$wrapperProperties = Get-Content -LiteralPath (Join-Path $repoRoot "gradle\wrapper\gradle-wrapper.properties") +$gradleDistribution = $wrapperProperties | Where-Object { $_.StartsWith("distributionUrl=") } | Select-Object -First 1 +$sourceCommit = [System.Environment]::GetEnvironmentVariable("GITHUB_SHA") +if ([string]::IsNullOrWhiteSpace($sourceCommit)) { + $sourceCommit = "local" +} +$sourceRef = [System.Environment]::GetEnvironmentVariable("GITHUB_REF") +if ([string]::IsNullOrWhiteSpace($sourceRef)) { + $sourceRef = "local" +} + +$buildInfo = [ordered] @{ + appVersion = $Version + packageVersion = $packageVersion + architecture = "x64" + identityName = "SudosyLabs.Vnidrop" + publisher = "CN=6456DC8E-2C31-44BD-AACC-2E6813C833CB" + storeId = "9NJ5Q0FG7TGL" + sourceCommit = $sourceCommit + sourceRef = $sourceRef + runnerImage = [System.Environment]::GetEnvironmentVariable("ImageOS") + runnerImageVersion = [System.Environment]::GetEnvironmentVariable("ImageVersion") + javaVersion = ((& java --version | Select-Object -First 1) | Out-String).Trim() + rustVersion = ((& rustc --version) | Out-String).Trim() + cargoVersion = ((& cargo --version) | Out-String).Trim() + gradleDistribution = $gradleDistribution + windowsSdkVersion = $WindowsSdkVersion + makeAppxVersion = (Get-Item -LiteralPath $makeAppxPath).VersionInfo.FileVersion + makePriVersion = (Get-Item -LiteralPath $makePriPath).VersionInfo.FileVersion + unsignedForMicrosoftStore = $true + builtAtUtc = [System.DateTimeOffset]::UtcNow.ToString("O") +} +[System.IO.File]::WriteAllText( + $buildInfoPath, + ($buildInfo | ConvertTo-Json -Depth 4), + [System.Text.UTF8Encoding]::new($false) +) + +$checksumTargets = @($msixPath, $uploadPath, $buildInfoPath) +[string[]] $checksumLines = $checksumTargets | ForEach-Object { + $hash = Get-FileHash -LiteralPath $_ -Algorithm SHA256 + $hash.Hash.ToLowerInvariant() + " " + [System.IO.Path]::GetFileName($_) +} +[System.IO.File]::WriteAllLines($checksumsPath, $checksumLines, [System.Text.Encoding]::ASCII) + +Write-Host "Created unsigned Microsoft Store artifacts:" +Write-Host " $msixPath" +Write-Host " $uploadPath" +Write-Host " $checksumsPath" diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 04cd882..9ce57a7 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -8,11 +8,13 @@ import gobley.gradle.GobleyHost import gobley.gradle.rust.targets.RustAndroidTarget import gobley.gradle.rust.targets.RustAppleMobileTarget import gobley.gradle.rust.targets.RustTarget +import gobley.gradle.Variant import org.gradle.api.DefaultTask import org.gradle.api.provider.ListProperty import org.gradle.api.tasks.Input import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.TaskAction +import org.gradle.jvm.tasks.Jar import org.jetbrains.kotlin.gradle.dsl.JvmTarget abstract class VerifyHostCargoTaskSelection : DefaultTask() { @@ -37,6 +39,17 @@ plugins { alias(libs.plugins.kotlinAtomicfu) } +val appVersion = providers.gradleProperty("vnidrop.version").get() +val desktopRustVariant = providers.gradleProperty("vnidrop.desktop.rustVariant") + .map { value -> + when (value.trim().lowercase()) { + "debug" -> Variant.Debug + "release" -> Variant.Release + else -> error("vnidrop.desktop.rustVariant must be either debug or release") + } + } + .orElse(Variant.Debug) + // Compile-time switches (gradle.properties or -P…). // included=false: no Share-diagnostics toggle, no telemetry/crash auto-upload stack. // endpoint/key both empty: transport is NoOp (safe default until Cloudflare is deployed). @@ -185,6 +198,7 @@ val hostCargoTargets = buildSet { cargo { packageDirectory = layout.projectDirectory.dir("../crates/vnidrop") publishJvmArtifacts = true + jvmVariant.set(desktopRustVariant) androidTargetsToBuild.set(setOf(RustAndroidTarget.Arm64, RustAndroidTarget.X64)) builds.jvm { variants { @@ -219,9 +233,15 @@ cargo { uniffi { generateFromLibrary { namespace = "vnidrop" + build.set(GobleyHost.current.rustTarget) + variant.set(desktopRustVariant) } } +tasks.named("jvmJar") { + manifest.attributes["Implementation-Version"] = appVersion +} + val verifyHostCargoTaskSelection = tasks.register( "verifyHostCargoTaskSelection" ) {