Merge pull request #18 from vnidrop/feat/release-configuration
ci: add Windows Store release pipeline
157
.github/workflows/windows-store.yml
vendored
Normal file
@@ -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
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
</p>
|
||||
<p className="privacy-meta">Effective July 16, 2026 · Version 1.0</p>
|
||||
<p className="privacy-meta">Effective July 16, 2026 · Version 1.1</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -65,8 +65,10 @@ export default function PrivacyPage() {
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
VniDrop is open-source software. A build distributed or operated by someone else
|
||||
@@ -131,12 +133,13 @@ export default function PrivacyPage() {
|
||||
<h2>Optional diagnostics and bug reports</h2>
|
||||
<h3>Automatic product diagnostics</h3>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<h3>User-submitted bug reports</h3>
|
||||
<p>
|
||||
@@ -165,11 +168,10 @@ export default function PrivacyPage() {
|
||||
nearby devices.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -210,11 +212,19 @@ export default function PrivacyPage() {
|
||||
Relays process connection metadata but cannot decrypt transfer contents.
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Vercel</dt>
|
||||
<dd>
|
||||
Hosts and serves the static VniDrop website and processes routine request and
|
||||
delivery metadata.
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Cloudflare</dt>
|
||||
<dd>
|
||||
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.
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
@@ -250,6 +260,14 @@ export default function PrivacyPage() {
|
||||
Cloudflare
|
||||
</a>
|
||||
,{" "}
|
||||
<a
|
||||
href="https://vercel.com/legal/privacy-notice"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Vercel
|
||||
</a>
|
||||
,{" "}
|
||||
<a href="https://policies.google.com/privacy" target="_blank" rel="noreferrer">
|
||||
Google
|
||||
</a>
|
||||
@@ -366,18 +384,12 @@ export default function PrivacyPage() {
|
||||
<section id="contact" className="policy-section policy-contact">
|
||||
<h2>Contact</h2>
|
||||
<p>
|
||||
VniDrop is currently maintained as an open-source project and does not yet publish a
|
||||
dedicated privacy email or postal address. For a privacy question or request, open a
|
||||
request in the project issue tracker. Do not put an invitation, file content,
|
||||
credentials, or other sensitive information in a public issue.
|
||||
For a privacy question, rights request, or support request, email
|
||||
support@sudosy.fr. Do not put an invitation, file content, credentials, or other
|
||||
sensitive information in a public issue.
|
||||
</p>
|
||||
<a
|
||||
className="privacy-contact-link"
|
||||
href="https://github.com/vnidrop/vnidrop/issues/new"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Contact the maintainers
|
||||
<a className="privacy-contact-link" href="mailto:support@sudosy.fr">
|
||||
Email support@sudosy.fr
|
||||
</a>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
@@ -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
|
||||
|
||||
56
packaging/windows/AppxManifest.xml
Normal file
@@ -0,0 +1,56 @@
|
||||
<?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:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
|
||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||
IgnorableNamespaces="uap uap10 rescap">
|
||||
<Identity
|
||||
Name="SudosyLabs.Vnidrop"
|
||||
Publisher="CN=6456DC8E-2C31-44BD-AACC-2E6813C833CB"
|
||||
Version="__VERSION__"
|
||||
ProcessorArchitecture="x64" />
|
||||
<Properties>
|
||||
<DisplayName>Vnidrop</DisplayName>
|
||||
<PublisherDisplayName>Sudosy Labs</PublisherDisplayName>
|
||||
<Description>Send files directly across your devices.</Description>
|
||||
<Logo>Assets\StoreLogo.png</Logo>
|
||||
</Properties>
|
||||
<Resources>
|
||||
<Resource Language="en-US" />
|
||||
</Resources>
|
||||
<Dependencies>
|
||||
<TargetDeviceFamily
|
||||
Name="Windows.Desktop"
|
||||
MinVersion="10.0.19041.0"
|
||||
MaxVersionTested="10.0.26100.0" />
|
||||
</Dependencies>
|
||||
<Capabilities>
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
</Capabilities>
|
||||
<Applications>
|
||||
<Application
|
||||
Id="VniDrop"
|
||||
Executable="VniDrop.exe"
|
||||
uap10:RuntimeBehavior="packagedClassicApp"
|
||||
uap10:TrustLevel="mediumIL">
|
||||
<uap:VisualElements
|
||||
DisplayName="Vnidrop"
|
||||
Description="Send files directly across your devices."
|
||||
Square150x150Logo="Assets\Square150x150Logo.png"
|
||||
Square44x44Logo="Assets\Square44x44Logo.png"
|
||||
BackgroundColor="transparent" />
|
||||
<Extensions>
|
||||
<uap:Extension Category="windows.fileTypeAssociation">
|
||||
<uap:FileTypeAssociation Name="vnidropinvitation">
|
||||
<uap:DisplayName>VniDrop Invitation</uap:DisplayName>
|
||||
<uap:Logo>Assets\Square44x44Logo.png</uap:Logo>
|
||||
<uap:SupportedFileTypes>
|
||||
<uap:FileType>.vnd</uap:FileType>
|
||||
</uap:SupportedFileTypes>
|
||||
</uap:FileTypeAssociation>
|
||||
</uap:Extension>
|
||||
</Extensions>
|
||||
</Application>
|
||||
</Applications>
|
||||
</Package>
|
||||
BIN
packaging/windows/Assets/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
packaging/windows/Assets/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
BIN
packaging/windows/Assets/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
105
packaging/windows/README.md
Normal file
@@ -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)
|
||||
280
packaging/windows/build-msix.ps1
Normal file
@@ -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"
|
||||
@@ -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<RustTarget> {
|
||||
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<Jar>("jvmJar") {
|
||||
manifest.attributes["Implementation-Version"] = appVersion
|
||||
}
|
||||
|
||||
val verifyHostCargoTaskSelection = tasks.register<VerifyHostCargoTaskSelection>(
|
||||
"verifyHostCargoTaskSelection"
|
||||
) {
|
||||
|
||||