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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 32 additions & 19 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
name: Build and Release ProtoFlow (Windows)
name: Build and Release ProtoFlow (MSIX)

on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
build_version:
description: "Version for MSIX package (e.g. v0.3.2)"
required: false
default: "v0.0.0-dev"

jobs:
build:
name: Build installer for Windows
name: Build MSIX for Windows
runs-on: windows-latest

steps:
Expand All @@ -24,27 +30,32 @@ jobs:
with:
node-version: "20"

- name: Build app (onedir)
run: .\\scripts\\build_windows.ps1

- name: Install Inno Setup
run: choco install innosetup -y

- name: Check installer icon
run: dir installer

- name: Build installer (Inno Setup)
- name: Build MSIX package
env:
MSIX_CERT_PFX_BASE64: ${{ secrets.MSIX_CERT_PFX_BASE64 }}
MSIX_CERT_PASSWORD: ${{ secrets.MSIX_CERT_PASSWORD }}
MSIX_SKIP_SIGN: ${{ secrets.MSIX_SKIP_SIGN }}
run: |
& "C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe" "installer\\ProtoFlow.iss" /DMyAppVersion=${{ github.ref_name }}
$version = "${{ github.event.inputs.build_version }}"
if ([string]::IsNullOrWhiteSpace($version)) { $version = "${{ github.ref_name }}" }
.\\scripts\\build_msix.ps1 `
-Version "$version" `
-IdentityName "${{ secrets.MSIX_IDENTITY_NAME }}" `
-Publisher "${{ secrets.MSIX_PUBLISHER }}" `
-PublisherDisplayName "${{ secrets.MSIX_PUBLISHER_DISPLAY_NAME }}" `
-DisplayName "${{ secrets.MSIX_DISPLAY_NAME }}"

- name: Upload installer artifact
- name: Upload MSIX artifact
uses: actions/upload-artifact@v4
with:
name: ProtoFlow-${{ github.ref_name }}-win-installer
path: dist/installer/*.exe
name: ProtoFlow-${{ github.ref_name }}-msix
path: |
dist/msix/*.msix
dist/msix/cert/*.cer

release:
name: Publish Release
if: startsWith(github.ref, 'refs/tags/')
needs: build
runs-on: windows-latest

Expand All @@ -61,14 +72,16 @@ jobs:
tag_name: ${{ github.ref_name }}
name: "ProtoFlow ${{ github.ref_name }}"
body: |
Auto-generated Windows installer for ProtoFlow.
Auto-generated MSIX package for ProtoFlow.
Includes Qt WebEngine UI + Vue bundle.
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Upload installer to Release
- name: Upload MSIX to Release
uses: softprops/action-gh-release@v1
with:
files: dist/**/*.exe
files: |
dist/**/*.msix
dist/**/*.cer
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
51 changes: 51 additions & 0 deletions installer/msix/AppxManifest.xml.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?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:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap desktop6 rescap">
<Identity
Name="__IDENTITY_NAME__"
Publisher="__PUBLISHER__"
Version="__VERSION__"
ProcessorArchitecture="x64" />

<Properties>
<DisplayName>__DISPLAY_NAME__</DisplayName>
<PublisherDisplayName>__PUBLISHER_DISPLAY_NAME__</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>

<Resources>
<Resource Language="zh-cn" />
<Resource Language="en-us" />
</Resources>

<Dependencies>
<TargetDeviceFamily
Name="Windows.Desktop"
MinVersion="10.0.17763.0"
MaxVersionTested="10.0.22621.0" />
</Dependencies>

<Applications>
<Application Id="ProtoFlow" Executable="ProtoFlow.exe" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements
DisplayName="__DISPLAY_NAME__"
Description="ProtoFlow Desktop App"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile
Square310x310Logo="Assets\Square310x310Logo.png"
Wide310x150Logo="Assets\Wide310x150Logo.png" />
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
</Application>
</Applications>

<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>
250 changes: 250 additions & 0 deletions scripts/build_msix.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
param(
[string]$Python = "python",
[string]$Node = "npm",
[string]$Version = "0.0.0",
[string]$Publisher = "CN=ProtoFlowDev",
[string]$IdentityName = "ProtoFlow.Desktop",
[string]$DisplayName = "ProtoFlow",
[string]$PublisherDisplayName = "ProtoFlow",
[string]$CertPassword = "ProtoFlow.Dev.123!",
[switch]$SkipSign
)

$ErrorActionPreference = "Stop"

function Find-ToolPath {
param(
[Parameter(Mandatory = $true)][string]$ToolName,
[Parameter(Mandatory = $true)][string]$ExeName
)

$cmd = Get-Command $ToolName -ErrorAction SilentlyContinue
if ($cmd) {
return $cmd.Path
}

$kitsRoot = Join-Path ${env:ProgramFiles(x86)} "Windows Kits\10\bin"
if (-not (Test-Path -LiteralPath $kitsRoot)) {
throw "Windows SDK path not found: $kitsRoot"
}

$candidate = Get-ChildItem -Path $kitsRoot -Filter $ExeName -Recurse -ErrorAction SilentlyContinue |
Sort-Object FullName -Descending |
Select-Object -First 1

if (-not $candidate) {
throw "$ExeName not found in Windows SDK."
}
return $candidate.FullName
}

function Convert-ToMsixVersion {
param([Parameter(Mandatory = $true)][string]$RawVersion)
$value = $RawVersion.TrimStart("v", "V")
$parts = $value.Split(".")
$nums = @()
foreach ($p in $parts) {
if ($p -match "^\d+$") {
$nums += [int]$p
}
else {
$nums += 0
}
if ($nums.Count -ge 4) { break }
}
while ($nums.Count -lt 4) { $nums += 0 }
return "$($nums[0]).$($nums[1]).$($nums[2]).$($nums[3])"
}

function Coalesce-Setting {
param(
[string]$Value,
[string]$Fallback
)
if ($null -ne $Value -and $Value.Trim().Length -gt 0) {
return $Value.Trim()
}
return $Fallback
}

function Resize-Png {
param(
[Parameter(Mandatory = $true)][string]$Source,
[Parameter(Mandatory = $true)][string]$Target,
[Parameter(Mandatory = $true)][int]$Width,
[Parameter(Mandatory = $true)][int]$Height
)

Add-Type -AssemblyName System.Drawing
$img = [System.Drawing.Image]::FromFile($Source)
try {
$bmp = New-Object System.Drawing.Bitmap($Width, $Height)
$gfx = [System.Drawing.Graphics]::FromImage($bmp)
try {
$gfx.Clear([System.Drawing.Color]::Transparent)
$gfx.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality
$gfx.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
$gfx.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality

$scale = [Math]::Min($Width / $img.Width, $Height / $img.Height)
$w = [int][Math]::Round($img.Width * $scale)
$h = [int][Math]::Round($img.Height * $scale)
$x = [int](($Width - $w) / 2)
$y = [int](($Height - $h) / 2)
$gfx.DrawImage($img, $x, $y, $w, $h)

$dir = Split-Path -Parent $Target
if (-not (Test-Path -LiteralPath $dir)) {
New-Item -ItemType Directory -Path $dir | Out-Null
}
$bmp.Save($Target, [System.Drawing.Imaging.ImageFormat]::Png)
}
finally {
$gfx.Dispose()
$bmp.Dispose()
}
}
finally {
$img.Dispose()
}
}

function Is-TrueString {
param([string]$Value)
if ($null -eq $Value) {
return $false
}
$v = $Value.Trim().ToLowerInvariant()
return $v -in @("1", "true", "yes", "y", "on")
}

$IdentityName = Coalesce-Setting -Value $IdentityName -Fallback "ProtoFlow.Desktop"
$Publisher = Coalesce-Setting -Value $Publisher -Fallback "CN=ProtoFlowDev"
$DisplayName = Coalesce-Setting -Value $DisplayName -Fallback "ProtoFlow"
$PublisherDisplayName = Coalesce-Setting -Value $PublisherDisplayName -Fallback "ProtoFlow"
$CertPassword = Coalesce-Setting -Value $CertPassword -Fallback "ProtoFlow.Dev.123!"
$msixVersion = Convert-ToMsixVersion -RawVersion $Version
if (-not $PSBoundParameters.ContainsKey("SkipSign")) {
$SkipSign = Is-TrueString -Value $env:MSIX_SKIP_SIGN
}
$repoRoot = Resolve-Path "."
$distRoot = Join-Path $repoRoot "dist"
$appDist = Join-Path $distRoot "ProtoFlow"
$msixRoot = Join-Path $distRoot "msix"
$stageRoot = Join-Path $msixRoot "stage"
$assetsRoot = Join-Path $stageRoot "Assets"
$manifestTemplate = Join-Path $repoRoot "installer\msix\AppxManifest.xml.template"
$manifestPath = Join-Path $stageRoot "AppxManifest.xml"
$msixName = "ProtoFlow-$msixVersion.msix"
$msixPath = Join-Path $msixRoot $msixName

Write-Host "==> Build app payload"
& powershell -ExecutionPolicy Bypass -File ".\scripts\build_windows.ps1" -Python $Python -Node $Node

if (-not (Test-Path -LiteralPath $appDist)) {
throw "App dist not found: $appDist"
}
if (-not (Test-Path -LiteralPath $manifestTemplate)) {
throw "MSIX manifest template not found: $manifestTemplate"
}

Write-Host "==> Prepare MSIX staging directory"
if (Test-Path -LiteralPath $stageRoot) {
Remove-Item -LiteralPath $stageRoot -Recurse -Force
}
if (Test-Path -LiteralPath $msixPath) {
Remove-Item -LiteralPath $msixPath -Force
}
New-Item -ItemType Directory -Path $stageRoot | Out-Null
Copy-Item -Path (Join-Path $appDist "*") -Destination $stageRoot -Recurse -Force

Write-Host "==> Generate MSIX assets"
$logoSource = Join-Path $repoRoot "ui\assets\icons\logo_mark.png"
if (-not (Test-Path -LiteralPath $logoSource)) {
throw "Logo source missing: $logoSource"
}
Resize-Png -Source $logoSource -Target (Join-Path $assetsRoot "StoreLogo.png") -Width 50 -Height 50
Resize-Png -Source $logoSource -Target (Join-Path $assetsRoot "Square44x44Logo.png") -Width 44 -Height 44
Resize-Png -Source $logoSource -Target (Join-Path $assetsRoot "Square150x150Logo.png") -Width 150 -Height 150
Resize-Png -Source $logoSource -Target (Join-Path $assetsRoot "Square310x310Logo.png") -Width 310 -Height 310
Resize-Png -Source $logoSource -Target (Join-Path $assetsRoot "Wide310x150Logo.png") -Width 310 -Height 150
Resize-Png -Source $logoSource -Target (Join-Path $assetsRoot "SplashScreen.png") -Width 620 -Height 300

Write-Host "==> Render AppxManifest.xml"
$manifest = Get-Content -LiteralPath $manifestTemplate -Raw -Encoding UTF8
$manifest = $manifest.Replace("__IDENTITY_NAME__", $IdentityName)
$manifest = $manifest.Replace("__PUBLISHER__", $Publisher)
$manifest = $manifest.Replace("__VERSION__", $msixVersion)
$manifest = $manifest.Replace("__DISPLAY_NAME__", $DisplayName)
$manifest = $manifest.Replace("__PUBLISHER_DISPLAY_NAME__", $PublisherDisplayName)
Set-Content -LiteralPath $manifestPath -Value $manifest -Encoding UTF8

$makeAppx = Find-ToolPath -ToolName "makeappx" -ExeName "makeappx.exe"

Write-Host "==> Pack MSIX"
if (-not (Test-Path -LiteralPath $msixRoot)) {
New-Item -ItemType Directory -Path $msixRoot | Out-Null
}
& $makeAppx pack /d $stageRoot /p $msixPath /o | Out-Host

if (-not (Test-Path -LiteralPath $msixPath)) {
throw "MSIX package not generated: $msixPath"
}

if ($SkipSign) {
Write-Host "==> Skip signing (MSIX_SKIP_SIGN=true or -SkipSign)"
Write-Host "MSIX generated (unsigned): $msixPath"
return
}

$signTool = Find-ToolPath -ToolName "signtool" -ExeName "signtool.exe"

$certDir = Join-Path $msixRoot "cert"
if (-not (Test-Path -LiteralPath $certDir)) {
New-Item -ItemType Directory -Path $certDir | Out-Null
}
$certPfx = Join-Path $certDir "ProtoFlow_msix_signing.pfx"
$certCer = Join-Path $certDir "ProtoFlow_msix_signing.cer"

if ($env:MSIX_CERT_PFX_BASE64 -and $env:MSIX_CERT_PASSWORD) {
Write-Host "==> Use provided signing certificate"
[IO.File]::WriteAllBytes($certPfx, [Convert]::FromBase64String($env:MSIX_CERT_PFX_BASE64))
$CertPassword = $env:MSIX_CERT_PASSWORD
}
else {
Write-Host "==> Generate self-signed certificate for MSIX"
$cert = New-SelfSignedCertificate -Type Custom `
-KeyAlgorithm RSA `
-KeyLength 2048 `
-HashAlgorithm sha256 `
-NotAfter (Get-Date).AddYears(3) `
-Subject $Publisher `
-CertStoreLocation "Cert:\CurrentUser\My" `
-TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3")

if (-not $cert) {
throw "Unable to create self-signed certificate."
}

$securePwd = ConvertTo-SecureString -String $CertPassword -Force -AsPlainText
Export-PfxCertificate -Cert $cert -FilePath $certPfx -Password $securePwd | Out-Null
Export-Certificate -Cert $cert -FilePath $certCer | Out-Null
}

Write-Host "==> Sign MSIX"
$signArgs = @("sign", "/fd", "SHA256", "/f", $certPfx, "/p", $CertPassword, "/td", "SHA256")
try {
& $signTool @($signArgs + @("/tr", "http://timestamp.digicert.com", $msixPath)) | Out-Host
}
catch {
Write-Warning "Timestamp signing failed, retrying without timestamp."
& $signTool @($signArgs + @($msixPath)) | Out-Host
}

Write-Host "==> Verify signature"
& $signTool verify /pa $msixPath | Out-Host

Write-Host "MSIX generated: $msixPath"
if (Test-Path -LiteralPath $certCer) {
Write-Host "Certificate exported: $certCer"
}
Loading