diff --git a/.github/workflows/windows-store.yml b/.github/workflows/windows-store.yml
new file mode 100644
index 0000000..4192e79
--- /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.0.LTS"
+
+ - 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/docs/app/privacy/page.tsx b/docs/app/privacy/page.tsx
index 182ea39..575983e 100644
--- a/docs/app/privacy/page.tsx
+++ b/docs/app/privacy/page.tsx
@@ -31,7 +31,7 @@ export default function PrivacyPage() {
This policy explains what moves between devices, what stays local, and what is sent
only when you choose to share diagnostics or a bug report.
- Effective July 16, 2026 · Version 1.0
+ Effective July 16, 2026 · Version 1.1
@@ -65,8 +65,10 @@ export default function PrivacyPage() {
This policy covers the official VniDrop website, the VniDrop applications for
Android, iOS, macOS, Windows, and Linux, and the diagnostics service configured by
- the official project. In this policy, “VniDrop,” “we,” and “us” refer to the
- maintainers of the official VniDrop project and the official builds they distribute.
+ the official project. For an official release, VniDrop’s data controller is the
+ individual publisher named in the applicable app-store listing. In this policy,
+ “VniDrop,” “we,” and “us” also include the maintainers acting on that publisher’s
+ behalf. The publisher can be reached at support@sudosy.fr.
VniDrop is open-source software. A build distributed or operated by someone else
@@ -131,12 +133,13 @@ export default function PrivacyPage() {
Optional diagnostics and bug reports
Automatic product diagnostics
- When an official build includes diagnostics, automatic usage events and crash
- reports are disabled until you enable “Share diagnostics.” If enabled, VniDrop may
- send an anonymous installation ID, app version, platform, sparse event names and
- properties, crash type and message, a redacted stack trace, timestamps, and recent
- in-app breadcrumbs. You can turn this off at any time; doing so also removes pending
- local crash reports.
+ Official releases indicate in the app settings whether automatic product
+ diagnostics are included. When included, automatic usage events and crash reports
+ are disabled until you enable “Share diagnostics.” If enabled, VniDrop may send an
+ anonymous installation ID, app version, platform, sparse event names and properties,
+ crash type and message, a redacted stack trace, timestamps, and recent in-app
+ breadcrumbs. You can turn this off at any time; doing so also removes pending local
+ crash reports.
User-submitted bug reports
@@ -165,11 +168,10 @@ export default function PrivacyPage() {
nearby devices.
- The hosting and security infrastructure may process routine request information—such
+ Vercel hosts the static site, while Cloudflare proxies requests and provides DNS and
+ security services for the domain. They may process routine request information—such
as IP address, time, requested page, referrer, and browser user agent—to deliver the
- site, maintain reliability, and prevent abuse. The live hosting provider must be
- identified in this policy before public deployment if it differs from the providers
- described below.
+ site, maintain reliability, and prevent abuse.
@@ -210,11 +212,19 @@ export default function PrivacyPage() {
Relays process connection metadata but cannot decrypt transfer contents.
+
+
Vercel
+
+ Hosts and serves the static VniDrop website and processes routine request and
+ delivery metadata.
+
+
Cloudflare
- The project’s diagnostics design uses Cloudflare Workers, D1, and R2.
- Cloudflare also processes source IPs for request delivery and abuse controls.
+ Proxies website requests and provides DNS, security, and abuse controls. When
+ the optional diagnostics service is configured, it uses Cloudflare Workers, D1,
+ and R2.
@@ -250,6 +260,14 @@ export default function PrivacyPage() {
Cloudflare
,{" "}
+
+ Vercel
+
+ ,{" "}
Google
@@ -366,18 +384,12 @@ export default function PrivacyPage() {
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"
) {