mirror of
https://github.com/ZoiteChat/zoitechat.git
synced 2026-11-11 09:55:40 +00:00
Compare commits
1
Commits
test
..
4302a30647
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4302a30647 |
No files matched your search
@@ -64,20 +64,6 @@ jobs:
|
|||||||
run: python3 packaging/macos/build.py --build-number '${{ github.run_number }}'
|
run: python3 packaging/macos/build.py --build-number '${{ github.run_number }}'
|
||||||
- name: Verify relocated startup
|
- name: Verify relocated startup
|
||||||
run: python3 packaging/macos/smoke.py dist/ZoiteChat.app
|
run: python3 packaging/macos/smoke.py dist/ZoiteChat.app
|
||||||
# Desktop capture can be unavailable on a runner image. Preserve the
|
|
||||||
# existing build gate and publish the separate visual-check outcome.
|
|
||||||
- name: Exercise IRC and capture the macOS application window
|
|
||||||
continue-on-error: true
|
|
||||||
timeout-minutes: 5
|
|
||||||
run: python3 packaging/macos/visual-smoke.py dist/ZoiteChat.app
|
|
||||||
- name: Upload screenshot and visual-check evidence
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
|
||||||
with:
|
|
||||||
name: ZoiteChat-macos-${{ matrix.arch }}-visual-check
|
|
||||||
path: dist/visual-check/
|
|
||||||
if-no-files-found: ignore
|
|
||||||
retention-days: 14
|
|
||||||
- name: Create artifact preserving the application signature
|
- name: Create artifact preserving the application signature
|
||||||
run: ditto -c -k --keepParent dist/ZoiteChat.app dist/ZoiteChat-${{ matrix.arch }}.zip
|
run: ditto -c -k --keepParent dist/ZoiteChat.app dist/ZoiteChat-${{ matrix.arch }}.zip
|
||||||
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
||||||
|
|||||||
@@ -1,428 +0,0 @@
|
|||||||
name: Windows MSI/MSIX Build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
- 'release/**'
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
windows_msi_build:
|
|
||||||
runs-on: windows-2022
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
id-token: write
|
|
||||||
attestations: write
|
|
||||||
artifact-metadata: write
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
platform: [x64]
|
|
||||||
arch: [x64]
|
|
||||||
fail-fast: false
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- uses: actions/setup-python@v6
|
|
||||||
with:
|
|
||||||
python-version: '3.14.3'
|
|
||||||
architecture: ${{ matrix.arch }}
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
New-Item -Name "deps" -ItemType "Directory" -Force | Out-Null
|
|
||||||
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
python -m pip install cffi
|
|
||||||
python -m pip install zstandard
|
|
||||||
|
|
||||||
$ProgressPreference = 'SilentlyContinue'
|
|
||||||
function Download-WithRetry {
|
|
||||||
param(
|
|
||||||
[string]$Url,
|
|
||||||
[string]$OutFile,
|
|
||||||
[int]$MaxAttempts = 5,
|
|
||||||
[int]$InitialDelaySeconds = 2
|
|
||||||
)
|
|
||||||
for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
|
|
||||||
try {
|
|
||||||
Invoke-WebRequest -Uri $Url -OutFile $OutFile -ErrorAction Stop
|
|
||||||
return
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
if ($attempt -eq $MaxAttempts) {
|
|
||||||
throw
|
|
||||||
}
|
|
||||||
Start-Sleep -Seconds ($InitialDelaySeconds * [math]::Pow(2, $attempt - 1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# The existing solution includes installer.vcxproj, so Inno Setup is
|
|
||||||
# still required to build the unmodified solution. This workflow does
|
|
||||||
# not publish that EXE; it packages the same staged rel tree as MSI/MSIX.
|
|
||||||
Download-WithRetry -Url https://github.com/jrsoftware/issrc/releases/download/is-6_7_1/innosetup-6.7.1.exe -OutFile deps\innosetup-unicode.exe
|
|
||||||
& deps\innosetup-unicode.exe /VERYSILENT | Out-Null
|
|
||||||
|
|
||||||
# Use the same GTK runtime as the existing Windows EXE workflow.
|
|
||||||
Download-WithRetry -Url https://github.com/ZoiteChat/gvsbuild/releases/download/zoitechat-2.20.0-alpha.1/GTK3_Gvsbuild_zoitechat-2.20.0-alpha.1_x64.zip -OutFile deps\gtk-${{ matrix.arch }}.zip
|
|
||||||
Expand-Archive -LiteralPath deps\gtk-${{ matrix.arch }}.zip -DestinationPath C:\gtk-build\gtk\x64\release -Force
|
|
||||||
|
|
||||||
# The GTK3 bundle does not contain librsvg. Restore the renderer,
|
|
||||||
# pixbuf loader, and license from the matching GTK4 bundle. Do not
|
|
||||||
# replace the rest of the GTK3 runtime with GTK4 libraries.
|
|
||||||
Download-WithRetry -Url https://github.com/ZoiteChat/gvsbuild/releases/download/zoitechat-2.20.0-alpha.1/GTK4_Gvsbuild_zoitechat-2.20.0-alpha.1_x64.zip -OutFile deps\svg-runtime.zip
|
|
||||||
if ((Get-FileHash deps\svg-runtime.zip -Algorithm SHA256).Hash -ne 'fb657f9ebae3a6807defb15e66091d643a703ee66cb37488e3e11e5646f45adb') {
|
|
||||||
throw 'Unexpected SVG runtime archive checksum'
|
|
||||||
}
|
|
||||||
$svgArchive = [System.IO.Compression.ZipFile]::OpenRead((Resolve-Path deps\svg-runtime.zip))
|
|
||||||
try {
|
|
||||||
foreach ($name in @('bin/rsvg-2-2.dll', 'lib/gdk-pixbuf-2.0/2.10.0/loaders/pixbufloader_svg.dll', 'share/doc/librsvg/COPYING.LIB')) {
|
|
||||||
$entry = $svgArchive.GetEntry($name)
|
|
||||||
if ($null -eq $entry) { throw "Missing SVG runtime file: $name" }
|
|
||||||
$destination = Join-Path 'C:\gtk-build\gtk\x64\release' $name
|
|
||||||
New-Item -ItemType Directory -Path (Split-Path $destination) -Force | Out-Null
|
|
||||||
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $destination, $true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally { $svgArchive.Dispose() }
|
|
||||||
|
|
||||||
# Ensure SVG remains loadable before compiling or packaging.
|
|
||||||
& C:\gtk-build\gtk\x64\release\bin\gdk-pixbuf-query-loaders.exe --update-cache
|
|
||||||
if ($LASTEXITCODE -ne 0) { throw 'Could not generate the pixbuf loader cache' }
|
|
||||||
if (-not (Select-String -LiteralPath 'C:\gtk-build\gtk\x64\release\lib\gdk-pixbuf-2.0\2.10.0\loaders.cache' -SimpleMatch '"svg"')) {
|
|
||||||
throw 'SVG loader could not be loaded with the GTK3 runtime'
|
|
||||||
}
|
|
||||||
|
|
||||||
Download-WithRetry -Url https://repo.msys2.org/mingw/x86_64/mingw-w64-x86_64-hicolor-icon-theme-0.18-1-any.pkg.tar.zst -OutFile deps\hicolor-icon-theme.pkg.tar.zst
|
|
||||||
python -c "import tarfile,zstandard,pathlib;archive=pathlib.Path(r'deps\\hicolor-icon-theme.pkg.tar.zst');target=pathlib.Path(r'C:\\gtk-build\\gtk\\x64\\release');dctx=zstandard.ZstdDecompressor();f=archive.open('rb');reader=dctx.stream_reader(f);tf=tarfile.open(fileobj=reader,mode='r|');[tf.extract(m,path=target) for m in tf if m.name.startswith('mingw64/share/icons/hicolor/')];tf.close();reader.close();f.close()"
|
|
||||||
|
|
||||||
Download-WithRetry -Url https://repo.msys2.org/mingw/x86_64/mingw-w64-x86_64-libarchive-3.8.6-1-any.pkg.tar.zst -OutFile deps\libarchive.pkg.tar.zst
|
|
||||||
python -c "import tarfile,zstandard,pathlib;archive=pathlib.Path(r'deps\\libarchive.pkg.tar.zst');target=pathlib.Path(r'C:\\gtk-build\\gtk\\x64\\release');dctx=zstandard.ZstdDecompressor();f=archive.open('rb');reader=dctx.stream_reader(f);tf=tarfile.open(fileobj=reader,mode='r|');[tf.extract(m,path=target) for m in tf if m.name.startswith(('mingw64/include/archive','mingw64/lib/libarchive','mingw64/bin/libarchive'))];tf.close();reader.close();f.close()"
|
|
||||||
|
|
||||||
if (Test-Path C:\gtk-build\gtk\x64\release\mingw64\share\icons\hicolor) {
|
|
||||||
New-Item -Path C:\gtk-build\gtk\x64\release\share\icons -ItemType Directory -Force | Out-Null
|
|
||||||
Copy-Item -Path C:\gtk-build\gtk\x64\release\mingw64\share\icons\hicolor -Destination C:\gtk-build\gtk\x64\release\share\icons\hicolor -Recurse -Force
|
|
||||||
}
|
|
||||||
if (Test-Path C:\gtk-build\gtk\x64\release\mingw64\include) {
|
|
||||||
New-Item -Path C:\gtk-build\gtk\x64\release\include -ItemType Directory -Force | Out-Null
|
|
||||||
Copy-Item -Path C:\gtk-build\gtk\x64\release\mingw64\include\archive* -Destination C:\gtk-build\gtk\x64\release\include -Recurse -Force
|
|
||||||
}
|
|
||||||
if (Test-Path C:\gtk-build\gtk\x64\release\mingw64\lib) {
|
|
||||||
New-Item -Path C:\gtk-build\gtk\x64\release\lib -ItemType Directory -Force | Out-Null
|
|
||||||
Copy-Item -Path C:\gtk-build\gtk\x64\release\mingw64\lib\libarchive* -Destination C:\gtk-build\gtk\x64\release\lib -Force
|
|
||||||
}
|
|
||||||
if (Test-Path C:\gtk-build\gtk\x64\release\mingw64\bin) {
|
|
||||||
New-Item -Path C:\gtk-build\gtk\x64\release\bin -ItemType Directory -Force | Out-Null
|
|
||||||
Copy-Item -Path C:\gtk-build\gtk\x64\release\mingw64\bin\libarchive*.dll -Destination C:\gtk-build\gtk\x64\release\bin -Force
|
|
||||||
}
|
|
||||||
if (Test-Path C:\gtk-build\gtk\x64\release\mingw64) {
|
|
||||||
Remove-Item -Path C:\gtk-build\gtk\x64\release\mingw64 -Recurse -Force
|
|
||||||
}
|
|
||||||
|
|
||||||
Download-WithRetry -Url https://github.com/ZoiteChat/gvsbuild/releases/download/zoitechat-2.18.0-pre4/gendef20260315.7z -OutFile deps\gendef.7z
|
|
||||||
& 7z.exe x deps\gendef.7z -oC:\gtk-build
|
|
||||||
|
|
||||||
Download-WithRetry -Url https://github.com/ZoiteChat/gvsbuild/releases/download/zoitechat-2.18.0-pre4/WinSparkle-20260315.7z -OutFile deps\WinSparkle.7z
|
|
||||||
& 7z.exe x deps\WinSparkle.7z -oC:\gtk-build\WinSparkle
|
|
||||||
|
|
||||||
Download-WithRetry -Url https://github.com/ZoiteChat/gvsbuild/releases/download/zoitechat-2.18.0-pre4/perl-5.42.0.1-${{ matrix.arch }}.7z -OutFile deps\perl-${{ matrix.arch }}.7z
|
|
||||||
& 7z.exe x deps\perl-${{ matrix.arch }}.7z -oC:\gtk-build\perl-5.42.0.1\${{ matrix.platform }}
|
|
||||||
|
|
||||||
$pyRoot = $env:pythonLocation
|
|
||||||
if (-not $pyRoot) { $pyRoot = & python -c "import sys; print(sys.prefix)" }
|
|
||||||
|
|
||||||
foreach ($pyDir in @("C:\gtk-build\python-3.14.3", "C:\gtk-build\python-3.14")) {
|
|
||||||
New-Item -Path $pyDir -ItemType Directory -Force | Out-Null
|
|
||||||
$target = Join-Path $pyDir "${{ matrix.platform }}"
|
|
||||||
if (Test-Path $target) { Remove-Item $target -Recurse -Force }
|
|
||||||
New-Item -Path $pyDir -Name "${{ matrix.platform }}" -ItemType Junction -Value $pyRoot | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
- uses: actions/setup-python@v6
|
|
||||||
with:
|
|
||||||
python-version: '3.8.10'
|
|
||||||
architecture: ${{ matrix.arch }}
|
|
||||||
|
|
||||||
- name: Prepare Python 3.8
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
python -m pip install cffi
|
|
||||||
|
|
||||||
$pyRoot = $env:pythonLocation
|
|
||||||
if (-not $pyRoot) { $pyRoot = & python -c "import sys; print(sys.prefix)" }
|
|
||||||
|
|
||||||
foreach ($pyDir in @("C:\gtk-build\python-3.8.10", "C:\gtk-build\python-3.8")) {
|
|
||||||
New-Item -Path $pyDir -ItemType Directory -Force | Out-Null
|
|
||||||
$target = Join-Path $pyDir "${{ matrix.platform }}"
|
|
||||||
if (Test-Path $target) { Remove-Item $target -Recurse -Force }
|
|
||||||
New-Item -Path $pyDir -Name "${{ matrix.platform }}" -ItemType Junction -Value $pyRoot | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
shell: cmd
|
|
||||||
run: |
|
|
||||||
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat"
|
|
||||||
|
|
||||||
set "PYTHON_DIR=C:\gtk-build\python-3.14.3\${{ matrix.platform }}"
|
|
||||||
if not exist "%PYTHON_DIR%\libs\python314.lib" (
|
|
||||||
echo Missing %PYTHON_DIR%\libs\python314.lib
|
|
||||||
dir "%PYTHON_DIR%\libs"
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
set "PYTHON38_DIR=C:\gtk-build\python-3.8\${{ matrix.platform }}"
|
|
||||||
if not exist "%PYTHON38_DIR%\libs\python38.lib" (
|
|
||||||
echo Missing %PYTHON38_DIR%\libs\python38.lib
|
|
||||||
dir "%PYTHON38_DIR%\libs"
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
if not exist "%PYTHON38_DIR%\Lib\site-packages\_cffi_backend.cp38*.pyd" (
|
|
||||||
echo Missing %PYTHON38_DIR%\Lib\site-packages\_cffi_backend.cp38*.pyd
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
set "LIB=%PYTHON_DIR%\libs;%LIB%"
|
|
||||||
set "INCLUDE=%PYTHON_DIR%\include;%INCLUDE%"
|
|
||||||
|
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "$archiveLib='C:\gtk-build\gtk\x64\release\lib\libarchive.lib'; if (-not (Test-Path $archiveLib)) { $archiveDll = Get-ChildItem 'C:\gtk-build\gtk\x64\release\bin\libarchive*.dll' | Select-Object -First 1; if ($archiveDll) { Push-Location 'C:\gtk-build\gtk\x64\release\lib'; & 'C:\gtk-build\gendef\gendef.exe' $archiveDll.FullName | Out-Null; $archiveDef = Get-ChildItem 'libarchive*.def' | Select-Object -First 1; if ($archiveDef) { & lib /def:$archiveDef.Name /machine:${{ matrix.platform }} /out:libarchive.lib | Out-Null }; Pop-Location } }"
|
|
||||||
|
|
||||||
msbuild win32\zoitechat.sln /m /verbosity:minimal /p:Configuration=Release /p:Platform=${{ matrix.platform }}
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
|
|
||||||
if not exist "..\zoitechat-build\${{ matrix.platform }}\rel\plugins\hcpython38.dll" (
|
|
||||||
echo hcpython38.dll was not built
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
if not exist "..\zoitechat-build\${{ matrix.platform }}\rel\_cffi_backend.cp38*.pyd" (
|
|
||||||
echo _cffi_backend for Python 3.8 was not staged for the installer
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
# Master added native emoji font staging and tests after the release
|
|
||||||
# branch split. Run these before packaging so MSI/MSIX use verified files.
|
|
||||||
- name: Test emoji catalog, font coverage and picker
|
|
||||||
shell: cmd
|
|
||||||
run: |
|
|
||||||
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat"
|
|
||||||
set "PATH=%CD%\..\zoitechat-build\x64\rel;%PATH%"
|
|
||||||
set "GDK_PIXBUF_MODULE_FILE=%CD%\..\zoitechat-build\x64\rel\lib\gdk-pixbuf-2.0\2.10.0\loaders.cache"
|
|
||||||
set "ZOITECHAT_EMOJI_FONT=%CD%\..\zoitechat-build\x64\rel\share\fonts\zoitechat\NotoColorEmoji.ttf"
|
|
||||||
fc /b data\fonts\NotoColorEmoji.ttf "%ZOITECHAT_EMOJI_FONT%"
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
for %%T in (data font ui) do (
|
|
||||||
msbuild win32\emoji-tests.vcxproj /m /verbosity:minimal /p:Configuration=Release /p:Platform=x64 /p:EmojiTestKind=%%T
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
..\zoitechat-build\x64\tests\emoji-%%T-tests.exe --tap
|
|
||||||
if errorlevel 1 exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
- name: Install WiX Toolset
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
# Keep this pinned. WiX v6 introduced OSMF terms and WiX v7 adds
|
|
||||||
# explicit EULA acceptance; do not silently accept new terms in CI.
|
|
||||||
dotnet tool install --tool-path .wix wix --version 5.0.2
|
|
||||||
if ($LASTEXITCODE -ne 0) { throw 'Failed to install WiX Toolset 5.0.2' }
|
|
||||||
& .\.wix\wix.exe --version
|
|
||||||
if ($LASTEXITCODE -ne 0) { throw 'WiX Toolset could not be executed' }
|
|
||||||
|
|
||||||
- name: Build MSI
|
|
||||||
id: build_msi
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
|
|
||||||
$rawVersion = (Get-Content VERSION -Raw).Trim()
|
|
||||||
if ($rawVersion -notmatch '^(\d+)\.(\d+)\.(\d+)') {
|
|
||||||
throw "VERSION '$rawVersion' does not begin with an MSI-compatible major.minor.patch version"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Keep the full development version in the filename, while MSI's
|
|
||||||
# ProductVersion is numeric (e.g. 2.20.0-dev -> 2.20.0).
|
|
||||||
$msiVersion = "$($Matches[1]).$($Matches[2]).$($Matches[3])"
|
|
||||||
$safeVersion = $rawVersion -replace '[^0-9A-Za-z._-]', '-'
|
|
||||||
$sourceDir = (Resolve-Path "..\zoitechat-build\${{ matrix.platform }}\rel").Path
|
|
||||||
$outputPath = Join-Path $PWD "ZoiteChat-${safeVersion}_${{ matrix.arch }}.msi"
|
|
||||||
|
|
||||||
if (-not (Test-Path (Join-Path $sourceDir 'zoitechat.exe'))) {
|
|
||||||
throw "Staged ZoiteChat executable is missing from $sourceDir"
|
|
||||||
}
|
|
||||||
|
|
||||||
& .\.wix\wix.exe build win32\wix\zoitechat.wxs `
|
|
||||||
-arch x64 `
|
|
||||||
-d "SourceDir=$sourceDir" `
|
|
||||||
-d "ProductVersion=$msiVersion" `
|
|
||||||
-out $outputPath
|
|
||||||
if ($LASTEXITCODE -ne 0) { throw 'WiX MSI build failed' }
|
|
||||||
|
|
||||||
if (-not (Test-Path $outputPath)) {
|
|
||||||
throw "Expected MSI was not created: $outputPath"
|
|
||||||
}
|
|
||||||
|
|
||||||
"path=$outputPath" >> $env:GITHUB_OUTPUT
|
|
||||||
"filename=$(Split-Path $outputPath -Leaf)" >> $env:GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Test MSI administrative extraction
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
$msi = '${{ steps.build_msi.outputs.path }}'
|
|
||||||
$adminImage = Join-Path $PWD 'msi-admin-image'
|
|
||||||
New-Item -ItemType Directory -Path $adminImage -Force | Out-Null
|
|
||||||
|
|
||||||
$arguments = "/a `"$msi`" /qn TARGETDIR=`"$adminImage`""
|
|
||||||
$process = Start-Process -FilePath msiexec.exe -ArgumentList $arguments -Wait -PassThru
|
|
||||||
if ($process.ExitCode -ne 0) {
|
|
||||||
throw "MSI administrative extraction failed with exit code $($process.ExitCode)"
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($requiredFile in @(
|
|
||||||
'zoitechat.exe',
|
|
||||||
'hclua.dll',
|
|
||||||
'hcupd.dll',
|
|
||||||
'WinSparkle.dll',
|
|
||||||
'hcchecksum.dll',
|
|
||||||
'hcexec.dll',
|
|
||||||
'hcfishlim.dll',
|
|
||||||
'hcsysinfo.dll',
|
|
||||||
'system.png',
|
|
||||||
'hcperl.dll',
|
|
||||||
'hcpython3.dll',
|
|
||||||
'hcpython38.dll',
|
|
||||||
'NotoColorEmoji.ttf',
|
|
||||||
'OFL.txt'
|
|
||||||
)) {
|
|
||||||
$match = Get-ChildItem -Path $adminImage -Filter $requiredFile -File -Recurse | Select-Object -First 1
|
|
||||||
if ($null -eq $match) {
|
|
||||||
throw "Administrative image does not contain required normal-install file: $requiredFile"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($requiredPattern in @('_cffi_backend.cp314*.pyd', '_cffi_backend.cp38*.pyd')) {
|
|
||||||
$match = Get-ChildItem -Path $adminImage -Filter $requiredPattern -File -Recurse | Select-Object -First 1
|
|
||||||
if ($null -eq $match) {
|
|
||||||
throw "Administrative image does not contain required Python support file: $requiredPattern"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($requiredPythonFile in @('zoitechat.py', '_zoitechat.py', 'hexchat.py', 'xchat.py')) {
|
|
||||||
$match = Get-ChildItem -Path $adminImage -Filter $requiredPythonFile -File -Recurse |
|
|
||||||
Where-Object { $_.Directory.Name -eq 'python' } |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($null -eq $match) {
|
|
||||||
throw "Administrative image does not contain required Python bridge file: $requiredPythonFile"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($excludedFile in @(
|
|
||||||
'portable-mode',
|
|
||||||
'zoitechat-text.exe'
|
|
||||||
)) {
|
|
||||||
$match = Get-ChildItem -Path $adminImage -Filter $excludedFile -File -Recurse | Select-Object -First 1
|
|
||||||
if ($null -ne $match) {
|
|
||||||
throw "MSI incorrectly contains Inno custom/portable-only file: $excludedFile"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Build MSIX
|
|
||||||
id: build_msix
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
|
|
||||||
$rawVersion = (Get-Content VERSION -Raw).Trim()
|
|
||||||
if ($rawVersion -notmatch '^(\d+)\.(\d+)\.(\d+)') {
|
|
||||||
throw "VERSION '$rawVersion' does not begin with a Store-compatible major.minor.build version"
|
|
||||||
}
|
|
||||||
|
|
||||||
$versionParts = @(
|
|
||||||
[int]$Matches[1],
|
|
||||||
[int]$Matches[2],
|
|
||||||
[int]$Matches[3]
|
|
||||||
)
|
|
||||||
if ($versionParts[0] -eq 0) {
|
|
||||||
throw 'The Microsoft Store requires a non-zero major package version.'
|
|
||||||
}
|
|
||||||
foreach ($part in $versionParts) {
|
|
||||||
if ($part -lt 0 -or $part -gt 65535) {
|
|
||||||
throw "MSIX version component '$part' is outside the Store-supported 0-65535 range."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Partner Center reserves the fourth field as 0. A -dev version
|
|
||||||
# is packaged as a valid 2.20.0.0, without changing VERSION.
|
|
||||||
$packageVersion = "$($versionParts[0]).$($versionParts[1]).$($versionParts[2]).0"
|
|
||||||
$safeVersion = $rawVersion -replace '[^0-9A-Za-z._-]', '-'
|
|
||||||
$sourceDir = (Resolve-Path "..\zoitechat-build\${{ matrix.platform }}\rel").Path
|
|
||||||
$outputPath = Join-Path $PWD "ZoiteChat-${safeVersion}_${{ matrix.arch }}.msix"
|
|
||||||
|
|
||||||
# Reserved ZoiteChat identity from the release branch. Adjust only
|
|
||||||
# if the Product identity page in Partner Center changes.
|
|
||||||
$identityName = 'deependtilde.ZoiteChat'
|
|
||||||
$publisher = 'CN=6C7766BB-6AC6-488B-A09E-E3F8E68E63E4'
|
|
||||||
$publisherDisplayName = 'deepend_tilde'
|
|
||||||
$artifactName = 'MSIX Store Package ${{ matrix.arch }}'
|
|
||||||
|
|
||||||
Write-Host "Building Store MSIX for identity: $identityName"
|
|
||||||
|
|
||||||
& .\win32\msix\build-msix.ps1 `
|
|
||||||
-SourceDir $sourceDir `
|
|
||||||
-OutputPath $outputPath `
|
|
||||||
-PackageVersion $packageVersion `
|
|
||||||
-IdentityName $identityName `
|
|
||||||
-Publisher $publisher `
|
|
||||||
-PublisherDisplayName $publisherDisplayName `
|
|
||||||
-SelfSign
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw 'MSIX build script failed'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $outputPath)) {
|
|
||||||
throw "Expected MSIX was not created: $outputPath"
|
|
||||||
}
|
|
||||||
|
|
||||||
"path=$outputPath" >> $env:GITHUB_OUTPUT
|
|
||||||
"filename=$(Split-Path $outputPath -Leaf)" >> $env:GITHUB_OUTPUT
|
|
||||||
"package_version=$packageVersion" >> $env:GITHUB_OUTPUT
|
|
||||||
"store_ready=true" >> $env:GITHUB_OUTPUT
|
|
||||||
"artifact_name=$artifactName" >> $env:GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Upload MSI Installer
|
|
||||||
id: upload_msi
|
|
||||||
uses: actions/upload-artifact@v6
|
|
||||||
with:
|
|
||||||
name: MSI Installer ${{ matrix.arch }}
|
|
||||||
path: ${{ steps.build_msi.outputs.path }}
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Attest MSI Installer (Artifact Attestation)
|
|
||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
|
|
||||||
uses: actions/attest-build-provenance@v3
|
|
||||||
with:
|
|
||||||
subject-name: MSI Installer ${{ matrix.arch }}
|
|
||||||
subject-digest: sha256:${{ steps.upload_msi.outputs.artifact-digest }}
|
|
||||||
|
|
||||||
- name: Upload MSIX Package
|
|
||||||
id: upload_msix
|
|
||||||
uses: actions/upload-artifact@v6
|
|
||||||
with:
|
|
||||||
name: ${{ steps.build_msix.outputs.artifact_name }}
|
|
||||||
path: ${{ steps.build_msix.outputs.path }}
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Attest MSIX Package (Artifact Attestation)
|
|
||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
|
|
||||||
uses: actions/attest-build-provenance@v3
|
|
||||||
with:
|
|
||||||
subject-name: ${{ steps.build_msix.outputs.artifact_name }}
|
|
||||||
subject-digest: sha256:${{ steps.upload_msix.outputs.artifact-digest }}
|
|
||||||
@@ -59,42 +59,6 @@ The smoke check copies the app to a path containing spaces, verifies its signatu
|
|||||||
runs `--version`, and keeps an isolated GUI process alive for ten seconds without
|
runs `--version`, and keeps an isolated GUI process alive for ten seconds without
|
||||||
connecting to IRC. It tests startup, not interaction or long-running stability.
|
connecting to IRC. It tests startup, not interaction or long-running stability.
|
||||||
|
|
||||||
### Screenshots and a loopback IRC check without owning a Mac
|
|
||||||
|
|
||||||
The ordinary ARM and Intel workflow jobs also run a separate visual check.
|
|
||||||
After pushing this change to a pull request, open the new **macOS build** run
|
|
||||||
and download `ZoiteChat-macos-arm64-visual-check` and
|
|
||||||
`ZoiteChat-macos-x86_64-visual-check` from **Artifacts**. Each contains:
|
|
||||||
|
|
||||||
- `zoitechat.png`: the actual running application's window on that architecture.
|
|
||||||
- `result.json`: the checks completed, window dimensions, architecture and commit.
|
|
||||||
- `irc.log`: the temporary loopback server's registration, channel and PING/PONG traffic.
|
|
||||||
- `startup.log`: the application's startup diagnostics.
|
|
||||||
|
|
||||||
The check copies the built app to a path containing spaces, verifies its
|
|
||||||
signature, starts it with temporary settings, connects to a server bound only
|
|
||||||
to `127.0.0.1`, joins `#ci-smoke`, receives sample text and verifies its PONG
|
|
||||||
response. It captures the largest visible window owned by that app's process
|
|
||||||
and rejects failed or nearly uniform captures. No public IRC server or account
|
|
||||||
is used. The original application artifact is not modified.
|
|
||||||
|
|
||||||
This is a supplemental check with `continue-on-error`: a green build alone
|
|
||||||
does **not** mean the visual check passed. Inspect `result.json` for
|
|
||||||
`"status": "passed"` and view the PNG. If the runner has no desktop or denies
|
|
||||||
Screen Recording, the step reports failure and publishes diagnostics instead
|
|
||||||
of claiming a screenshot. No signing secrets are needed. The check runs in
|
|
||||||
the ordinary build job; it does not alter the release-signing job.
|
|
||||||
|
|
||||||
It can also run on a Mac with a logged-in desktop:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
python3 packaging/macos/visual-smoke.py dist/ZoiteChat.app --output dist/visual-check
|
|
||||||
```
|
|
||||||
|
|
||||||
Use a new output directory for each run. This verifies a narrow set of startup,
|
|
||||||
IRC and rendering behavior; it does not test keyboard/clipboard interaction,
|
|
||||||
TLS, notifications, Gatekeeper distribution, or long-running stability.
|
|
||||||
|
|
||||||
The audit verifies the entire dependency closure, including GTK image/input
|
The audit verifies the entire dependency closure, including GTK image/input
|
||||||
modules, GIO TLS, Enchant providers, OpenSSL providers and ZoiteChat plugins. It
|
modules, GIO TLS, Enchant providers, OpenSSL providers and ZoiteChat plugins. It
|
||||||
fails on missing libraries, external symlinks/search paths, architecture mismatches
|
fails on missing libraries, external symlinks/search paths, architecture mismatches
|
||||||
|
|||||||
+10
-26
@@ -12,7 +12,6 @@ import re
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
MACH_MAGIC = {bytes.fromhex(h) for h in ("feedface", "cefaedfe", "feedfacf", "cffaedfe", "cafebabe", "bebafeca", "cafebabf", "bfbafeca")}
|
MACH_MAGIC = {bytes.fromhex(h) for h in ("feedface", "cefaedfe", "feedfacf", "cffaedfe", "cafebabe", "bebafeca", "cafebabf", "bfbafeca")}
|
||||||
@@ -124,29 +123,6 @@ def copy_tree(source: Path, target: Path) -> None:
|
|||||||
shutil.copytree(source, target, dirs_exist_ok=True, symlinks=False)
|
shutil.copytree(source, target, dirs_exist_ok=True, symlinks=False)
|
||||||
|
|
||||||
|
|
||||||
def write_fontconfig(resources: Path, fontconfig_prefix: Path) -> None:
|
|
||||||
# Noto Color Emoji has fixed-size color bitmaps. Fontconfig supplies the
|
|
||||||
# requested-size/strike-size matrix; without it glyphs render at 109px.
|
|
||||||
rule = fontconfig_prefix / "share/fontconfig/conf.avail/10-scale-bitmap-fonts.conf"
|
|
||||||
if not rule.is_file():
|
|
||||||
raise RuntimeError(f"Required Fontconfig bitmap scaling rules not found: {rule}")
|
|
||||||
resources.mkdir(parents=True, exist_ok=True)
|
|
||||||
config = ET.fromstring('''<fontconfig>
|
|
||||||
<dir>/System/Library/Fonts</dir><dir>/Library/Fonts</dir><dir>~/Library/Fonts</dir>
|
|
||||||
<cachedir prefix="xdg">fontconfig</cachedir>
|
|
||||||
<alias><family>sans-serif</family><prefer><family>Helvetica</family></prefer></alias>
|
|
||||||
<alias><family>monospace</family><prefer><family>Menlo</family></prefer></alias>
|
|
||||||
</fontconfig>
|
|
||||||
''')
|
|
||||||
# Embed the standard rules so they work after relocation without depending
|
|
||||||
# on Homebrew or Fontconfig's host configuration search paths.
|
|
||||||
for element in reversed(list(ET.parse(rule).getroot())):
|
|
||||||
config.insert(0, element)
|
|
||||||
(resources / "fonts.conf").write_text(
|
|
||||||
'<?xml version="1.0"?>\n<!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">\n' +
|
|
||||||
ET.tostring(config, encoding="unicode") + "\n", encoding="utf-8")
|
|
||||||
|
|
||||||
|
|
||||||
def audit(app: Path) -> dict[str, object]:
|
def audit(app: Path) -> dict[str, object]:
|
||||||
binary = app / "Contents/MacOS/zoitechat"
|
binary = app / "Contents/MacOS/zoitechat"
|
||||||
architectures = set(run("lipo", "-archs", binary).split())
|
architectures = set(run("lipo", "-archs", binary).split())
|
||||||
@@ -204,7 +180,7 @@ def package(stage: Path, app: Path, build_number: str) -> None:
|
|||||||
seeds.append((source, resources / "lib/zoitechat/plugins" / source.name))
|
seeds.append((source, resources / "lib/zoitechat/plugins" / source.name))
|
||||||
|
|
||||||
prefixes = {formula: Path(run("brew", "--prefix", formula)) for formula in (
|
prefixes = {formula: Path(run("brew", "--prefix", formula)) for formula in (
|
||||||
"gtk+3", "gdk-pixbuf", "glib", "glib-networking", "librsvg", "enchant", "adwaita-icon-theme", "hicolor-icon-theme", "gsettings-desktop-schemas", "ca-certificates", "openssl@3", "lua", "libarchive", "gtk-mac-integration", "fontconfig")}
|
"gtk+3", "gdk-pixbuf", "glib", "glib-networking", "librsvg", "enchant", "adwaita-icon-theme", "hicolor-icon-theme", "gsettings-desktop-schemas", "ca-certificates", "openssl@3", "lua", "libarchive", "gtk-mac-integration")}
|
||||||
# Runtime-loaded code is not visible in the main executable's otool output.
|
# Runtime-loaded code is not visible in the main executable's otool output.
|
||||||
module_groups = [
|
module_groups = [
|
||||||
(prefixes["gdk-pixbuf"] / "lib/gdk-pixbuf-2.0/2.10.0/loaders", "lib/gdk-pixbuf-2.0/2.10.0/loaders"),
|
(prefixes["gdk-pixbuf"] / "lib/gdk-pixbuf-2.0/2.10.0/loaders", "lib/gdk-pixbuf-2.0/2.10.0/loaders"),
|
||||||
@@ -265,7 +241,15 @@ def package(stage: Path, app: Path, build_number: str) -> None:
|
|||||||
data = data.replace(str(source), "@ZOITECHAT_RESOURCES@/" + str(target.relative_to(resources)))
|
data = data.replace(str(source), "@ZOITECHAT_RESOURCES@/" + str(target.relative_to(resources)))
|
||||||
(resources / cache).write_text(data + "\n")
|
(resources / cache).write_text(data + "\n")
|
||||||
# The private font map loads the bundled Noto font; ordinary GTK fonts stay local.
|
# The private font map loads the bundled Noto font; ordinary GTK fonts stay local.
|
||||||
write_fontconfig(resources, prefixes["fontconfig"])
|
(resources / "fonts.conf").write_text('''<?xml version="1.0"?>
|
||||||
|
<!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">
|
||||||
|
<fontconfig>
|
||||||
|
<dir>/System/Library/Fonts</dir><dir>/Library/Fonts</dir><dir>~/Library/Fonts</dir>
|
||||||
|
<cachedir prefix="xdg">fontconfig</cachedir>
|
||||||
|
<alias><family>sans-serif</family><prefer><family>Helvetica</family></prefer></alias>
|
||||||
|
<alias><family>monospace</family><prefer><family>Menlo</family></prefer></alias>
|
||||||
|
</fontconfig>
|
||||||
|
''')
|
||||||
(resources / "zoitechat-bundle").write_text("1\n")
|
(resources / "zoitechat-bundle").write_text("1\n")
|
||||||
licenses = resources / "licenses"
|
licenses = resources / "licenses"
|
||||||
licenses.mkdir()
|
licenses.mkdir()
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
// Capture only the tested process's window; a desktop-only image is not evidence.
|
|
||||||
import AppKit
|
|
||||||
import CoreGraphics
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
func fail(_ message: String) -> Never {
|
|
||||||
FileHandle.standardError.write(Data((message + "\n").utf8))
|
|
||||||
exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
guard CommandLine.arguments.count == 3,
|
|
||||||
let pid = Int32(CommandLine.arguments[1]) else {
|
|
||||||
fail("Usage: capture-window PID OUTPUT.png")
|
|
||||||
}
|
|
||||||
let output = URL(fileURLWithPath: CommandLine.arguments[2])
|
|
||||||
let deadline = Date().addingTimeInterval(30)
|
|
||||||
var chosen: [String: Any]?
|
|
||||||
while Date() < deadline {
|
|
||||||
guard let app = NSRunningApplication(processIdentifier: pid), !app.isTerminated else {
|
|
||||||
fail("ZoiteChat is not running")
|
|
||||||
}
|
|
||||||
app.activate(options: [])
|
|
||||||
RunLoop.current.run(until: Date().addingTimeInterval(0.2))
|
|
||||||
let windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements],
|
|
||||||
kCGNullWindowID) as? [[String: Any]] ?? []
|
|
||||||
let owned = windows.filter { window in
|
|
||||||
guard (window[kCGWindowOwnerPID as String] as? NSNumber)?.int32Value == pid,
|
|
||||||
(window[kCGWindowLayer as String] as? NSNumber)?.intValue == 0,
|
|
||||||
let bounds = window[kCGWindowBounds as String] as? [String: Any],
|
|
||||||
let width = bounds["Width"] as? NSNumber,
|
|
||||||
let height = bounds["Height"] as? NSNumber else { return false }
|
|
||||||
return width.doubleValue >= 600 && height.doubleValue >= 300
|
|
||||||
}
|
|
||||||
chosen = owned.max { left, right in
|
|
||||||
func area(_ window: [String: Any]) -> Double {
|
|
||||||
let bounds = window[kCGWindowBounds as String] as? [String: NSNumber] ?? [:]
|
|
||||||
return (bounds["Width"]?.doubleValue ?? 0) * (bounds["Height"]?.doubleValue ?? 0)
|
|
||||||
}
|
|
||||||
return area(left) < area(right)
|
|
||||||
}
|
|
||||||
if chosen != nil { break }
|
|
||||||
}
|
|
||||||
guard let window = chosen,
|
|
||||||
let identifier = window[kCGWindowNumber as String] as? NSNumber else {
|
|
||||||
fail("No visible ZoiteChat window found in this runner's desktop session")
|
|
||||||
}
|
|
||||||
let capture = Process()
|
|
||||||
capture.executableURL = URL(fileURLWithPath: "/usr/sbin/screencapture")
|
|
||||||
capture.arguments = ["-x", "-o", "-t", "png", "-l", identifier.stringValue, output.path]
|
|
||||||
do { try capture.run() } catch { fail("Cannot start screencapture: \(error)") }
|
|
||||||
capture.waitUntilExit()
|
|
||||||
guard capture.terminationStatus == 0,
|
|
||||||
let png = try? Data(contentsOf: output),
|
|
||||||
let bitmap = NSBitmapImageRep(data: png),
|
|
||||||
bitmap.pixelsWide >= 600, bitmap.pixelsHigh >= 300 else {
|
|
||||||
fail("Application window capture failed; check runner Screen Recording permissions")
|
|
||||||
}
|
|
||||||
var colors = Set<String>()
|
|
||||||
for y in stride(from: 0, to: bitmap.pixelsHigh, by: max(1, bitmap.pixelsHigh / 60)) {
|
|
||||||
for x in stride(from: 0, to: bitmap.pixelsWide, by: max(1, bitmap.pixelsWide / 80)) {
|
|
||||||
if let color = bitmap.colorAt(x: x, y: y)?.usingColorSpace(.deviceRGB) {
|
|
||||||
colors.insert("\(Int(color.redComponent * 255)),\(Int(color.greenComponent * 255)),\(Int(color.blueComponent * 255))")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
guard colors.count >= 8 else { fail("Window capture is blank or nearly uniform") }
|
|
||||||
let evidence: [String: Any] = ["pid": pid, "window_id": identifier.intValue,
|
|
||||||
"width": bitmap.pixelsWide, "height": bitmap.pixelsHigh, "sampled_colors": colors.count]
|
|
||||||
do {
|
|
||||||
let data = try JSONSerialization.data(withJSONObject: evidence, options: [.sortedKeys])
|
|
||||||
print(String(decoding: data, as: UTF8.self))
|
|
||||||
} catch { fail("Cannot serialize screenshot evidence: \(error)") }
|
|
||||||
@@ -1,63 +1,16 @@
|
|||||||
"""Dependency parsing, relocation guards and release audit tests without a Mac SDK."""
|
"""Dependency parsing, relocation guards and release audit tests without a Mac SDK."""
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import os
|
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from xml.sax.saxutils import escape
|
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
import bundle
|
import bundle
|
||||||
|
|
||||||
|
|
||||||
class BundleTests(unittest.TestCase):
|
class BundleTests(unittest.TestCase):
|
||||||
def test_missing_bitmap_scaling_rules_fail_packaging(self):
|
|
||||||
with tempfile.TemporaryDirectory() as temporary:
|
|
||||||
directory = Path(temporary)
|
|
||||||
with self.assertRaisesRegex(RuntimeError, "bitmap scaling rules not found"):
|
|
||||||
bundle.write_fontconfig(directory / "Resources", directory / "missing-prefix")
|
|
||||||
|
|
||||||
@unittest.skipUnless(shutil.which("fc-match"), "requires Fontconfig's matching tool")
|
|
||||||
def test_bundled_bitmap_scaling_survives_relocation(self):
|
|
||||||
prefix = Path(bundle.run("brew", "--prefix", "fontconfig")) if sys.platform == "darwin" else Path("/usr")
|
|
||||||
rule = prefix / "share/fontconfig/conf.avail/10-scale-bitmap-fonts.conf"
|
|
||||||
if not rule.is_file():
|
|
||||||
self.skipTest("Fontconfig bitmap scaling configuration unavailable")
|
|
||||||
font = bundle.ROOT / "data/fonts/NotoColorEmoji.ttf"
|
|
||||||
with tempfile.TemporaryDirectory(prefix="zoitechat font scaling ") as temporary:
|
|
||||||
directory = Path(temporary)
|
|
||||||
source = directory / "build-prefix/share/fontconfig/conf.avail" / rule.name
|
|
||||||
source.parent.mkdir(parents=True)
|
|
||||||
shutil.copy2(rule, source)
|
|
||||||
resources = directory / "Resources"
|
|
||||||
bundle.write_fontconfig(resources, directory / "build-prefix")
|
|
||||||
relocated = directory / "Relocated Resources"
|
|
||||||
shutil.move(resources, relocated)
|
|
||||||
source.unlink()
|
|
||||||
# Exercise the shipped configuration without consulting host font
|
|
||||||
# rules. Only the bundled font is visible after reset-dirs.
|
|
||||||
wrapper = directory / "probe.conf"
|
|
||||||
wrapper.write_text(
|
|
||||||
'<fontconfig><include>' + escape(str(relocated / "fonts.conf")) +
|
|
||||||
'</include><reset-dirs/><dir>' + escape(str(font.parent)) + '</dir></fontconfig>')
|
|
||||||
env = os.environ.copy()
|
|
||||||
env["FONTCONFIG_FILE"] = str(wrapper)
|
|
||||||
env["XDG_CACHE_HOME"] = str(directory / "cache")
|
|
||||||
for requested in (12, 16, 24):
|
|
||||||
output = subprocess.check_output(
|
|
||||||
["fc-match", "-f", "%{pixelsize}\\n%{matrix}\\n%{file}\\n",
|
|
||||||
f"Noto Color Emoji:pixelsize={requested}"], env=env, cwd=directory, text=True)
|
|
||||||
pixels, matrix, matched = output.splitlines()
|
|
||||||
self.assertEqual(Path(matched).resolve(), font.resolve())
|
|
||||||
values = [float(value) for value in matrix.split()]
|
|
||||||
self.assertEqual(len(values), 4, "bitmap scaling matrix missing")
|
|
||||||
self.assertAlmostEqual(float(pixels) * values[0], requested, places=3)
|
|
||||||
self.assertAlmostEqual(float(pixels) * values[3], requested, places=3)
|
|
||||||
self.assertEqual(values[1:3], [0.0, 0.0])
|
|
||||||
|
|
||||||
@unittest.skipUnless(sys.platform == "darwin", "requires Apple's Mach-O tools")
|
@unittest.skipUnless(sys.platform == "darwin", "requires Apple's Mach-O tools")
|
||||||
def test_signed_binary_relocation_and_resigning(self):
|
def test_signed_binary_relocation_and_resigning(self):
|
||||||
with tempfile.TemporaryDirectory(prefix="zoitechat relocation ") as temporary:
|
with tempfile.TemporaryDirectory(prefix="zoitechat relocation ") as temporary:
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
"""Check that visual-test evidence requires a real IRC handshake and reply."""
|
|
||||||
import importlib.util
|
|
||||||
from pathlib import Path
|
|
||||||
import socket
|
|
||||||
import threading
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
spec = importlib.util.spec_from_file_location(
|
|
||||||
"visual_smoke", Path(__file__).resolve().parents[1] / "visual-smoke.py")
|
|
||||||
visual = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(visual)
|
|
||||||
|
|
||||||
|
|
||||||
class VisualSmokeTests(unittest.TestCase):
|
|
||||||
def test_tls_clienthello_is_rejected_without_waiting_for_a_newline(self):
|
|
||||||
with visual.IRCServer() as server:
|
|
||||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
||||||
thread.start()
|
|
||||||
try:
|
|
||||||
with socket.create_connection(server.server_address, timeout=3) as client:
|
|
||||||
# A TLS handshake record, with no IRC line terminator.
|
|
||||||
client.sendall(b"\x16\x03\x03\x00\x00")
|
|
||||||
self.assertEqual(client.recv(1), b"")
|
|
||||||
self.assertIn("Received TLS", server.error)
|
|
||||||
self.assertFalse(server.joined.is_set())
|
|
||||||
self.assertFalse(server.pong.is_set())
|
|
||||||
finally:
|
|
||||||
server.shutdown()
|
|
||||||
thread.join(timeout=3)
|
|
||||||
|
|
||||||
def test_registration_join_and_challenge_reply(self):
|
|
||||||
with visual.IRCServer() as server:
|
|
||||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
||||||
thread.start()
|
|
||||||
try:
|
|
||||||
with socket.create_connection(server.server_address, timeout=3) as client:
|
|
||||||
stream = client.makefile("rb")
|
|
||||||
|
|
||||||
def until(marker):
|
|
||||||
lines = []
|
|
||||||
for _ in range(30):
|
|
||||||
line = stream.readline().decode("utf-8").rstrip()
|
|
||||||
self.assertTrue(line, "server closed before expected response")
|
|
||||||
lines.append(line)
|
|
||||||
if marker in line:
|
|
||||||
return lines
|
|
||||||
self.fail("expected response missing: " + marker)
|
|
||||||
|
|
||||||
client.sendall(b"CAP LS 302\r\nNICK MacTester\r\nUSER ci 0 * :CI\r\n")
|
|
||||||
until(" CAP * LS :")
|
|
||||||
self.assertFalse(server.joined.is_set())
|
|
||||||
client.sendall(b"CAP END\r\n")
|
|
||||||
until(" 376 MacTester ")
|
|
||||||
client.sendall(b"JOIN #ci-smoke\r\n")
|
|
||||||
messages = until("PING :" + server.token)
|
|
||||||
self.assertTrue(any("PRIVMSG #ci-smoke" in line for line in messages))
|
|
||||||
self.assertTrue(server.joined.wait(timeout=1))
|
|
||||||
self.assertFalse(server.pong.is_set())
|
|
||||||
client.sendall(b"PONG :wrong-token\r\nPING :barrier\r\n")
|
|
||||||
until("PONG :barrier")
|
|
||||||
self.assertFalse(server.pong.is_set())
|
|
||||||
client.sendall(("PONG :" + server.token + "\r\n").encode())
|
|
||||||
self.assertTrue(server.pong.wait(timeout=1))
|
|
||||||
client.sendall(b"QUIT\r\n")
|
|
||||||
stream.close()
|
|
||||||
finally:
|
|
||||||
server.shutdown()
|
|
||||||
thread.join(timeout=3)
|
|
||||||
|
|
||||||
def test_early_disconnect_is_not_success(self):
|
|
||||||
with visual.IRCServer() as server:
|
|
||||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
||||||
thread.start()
|
|
||||||
try:
|
|
||||||
with socket.create_connection(server.server_address, timeout=3) as client:
|
|
||||||
client.sendall(b"QUIT\r\n")
|
|
||||||
self.assertFalse(server.joined.is_set())
|
|
||||||
self.assertFalse(server.pong.is_set())
|
|
||||||
finally:
|
|
||||||
server.shutdown()
|
|
||||||
thread.join(timeout=3)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Exercise a relocated Mac bundle against loopback IRC and capture its window."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
import platform
|
|
||||||
import secrets
|
|
||||||
import shutil
|
|
||||||
import socketserver
|
|
||||||
import subprocess
|
|
||||||
import tempfile
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
class IRCServer(socketserver.ThreadingTCPServer):
|
|
||||||
daemon_threads = True
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.joined = threading.Event()
|
|
||||||
self.pong = threading.Event()
|
|
||||||
self.error: str | None = None
|
|
||||||
self.token = secrets.token_hex(12)
|
|
||||||
self.transcript: list[str] = []
|
|
||||||
super().__init__(("127.0.0.1", 0), IRCHandler)
|
|
||||||
|
|
||||||
|
|
||||||
class IRCHandler(socketserver.StreamRequestHandler):
|
|
||||||
def handle(self):
|
|
||||||
self.connection.settimeout(60)
|
|
||||||
nick, user, registered, negotiating = "", False, False, False
|
|
||||||
|
|
||||||
def send(line):
|
|
||||||
self.server.transcript.append("S: " + line)
|
|
||||||
self.wfile.write((line + "\r\n").encode("utf-8"))
|
|
||||||
self.wfile.flush()
|
|
||||||
|
|
||||||
try:
|
|
||||||
# A TLS ClientHello is binary and need not contain a newline. Do
|
|
||||||
# not leave the fixture waiting for an IRC line until its timeout.
|
|
||||||
if self.rfile.peek(1).startswith(b"\x16"):
|
|
||||||
self.server.error = "Received TLS on the plaintext loopback IRC fixture; launch with SERVER -insecure"
|
|
||||||
self.server.transcript.append("Error: " + self.server.error)
|
|
||||||
return
|
|
||||||
while raw := self.rfile.readline(8192):
|
|
||||||
line = raw.decode("utf-8", errors="replace").rstrip("\r\n")
|
|
||||||
self.server.transcript.append("C: " + line)
|
|
||||||
parts = line.split()
|
|
||||||
if not parts:
|
|
||||||
continue
|
|
||||||
command = parts[0].upper()
|
|
||||||
if command == "CAP" and len(parts) > 1:
|
|
||||||
if parts[1].upper() == "LS":
|
|
||||||
negotiating = True
|
|
||||||
send(":ci.local CAP * LS :")
|
|
||||||
elif parts[1].upper() == "END":
|
|
||||||
negotiating = False
|
|
||||||
elif parts[1].upper() == "REQ":
|
|
||||||
send(":ci.local CAP * NAK :" + line.partition(" :")[2])
|
|
||||||
elif command == "NICK" and len(parts) > 1:
|
|
||||||
nick = parts[1].lstrip(":")
|
|
||||||
elif command == "USER":
|
|
||||||
user = True
|
|
||||||
elif command == "PING" and len(parts) > 1:
|
|
||||||
send("PONG :" + parts[-1].lstrip(":"))
|
|
||||||
elif command == "PONG" and parts[-1].lstrip(":") == self.server.token:
|
|
||||||
self.server.pong.set()
|
|
||||||
elif command == "JOIN" and registered:
|
|
||||||
channel = parts[1].lstrip(":").split(",")[0]
|
|
||||||
send(f":{nick}!ci@localhost JOIN :{channel}")
|
|
||||||
send(f":ci.local 332 {nick} {channel} :ZoiteChat native macOS visual check")
|
|
||||||
send(f":ci.local 353 {nick} = {channel} :@CiGuide {nick}")
|
|
||||||
send(f":ci.local 366 {nick} {channel} :End of names")
|
|
||||||
for message in (
|
|
||||||
"ZoiteChat is running on macOS " + platform.machine() + ".",
|
|
||||||
"Connected to the temporary IRC server and joined this channel.",
|
|
||||||
"Unicode rendering: café — 日本語 — 👋",
|
|
||||||
"This screenshot is captured from the running application window.",
|
|
||||||
):
|
|
||||||
send(f":CiGuide!ci@localhost PRIVMSG {channel} :{message}")
|
|
||||||
send("PING :" + self.server.token)
|
|
||||||
self.server.joined.set()
|
|
||||||
elif command == "QUIT":
|
|
||||||
return
|
|
||||||
if nick and user and not registered and not negotiating:
|
|
||||||
registered = True
|
|
||||||
send(f":ci.local 001 {nick} :Welcome to the ZoiteChat CI test server")
|
|
||||||
send(f":ci.local 005 {nick} CHANTYPES=# PREFIX=(ov)@+ NETWORK=ZoiteChat-CI :are supported")
|
|
||||||
send(f":ci.local 376 {nick} :End of MOTD")
|
|
||||||
except (OSError, ValueError) as error:
|
|
||||||
self.server.transcript.append("Connection closed: " + str(error))
|
|
||||||
|
|
||||||
|
|
||||||
def stop(process):
|
|
||||||
if process.poll() is None:
|
|
||||||
process.terminate()
|
|
||||||
try:
|
|
||||||
process.wait(timeout=10)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
process.kill()
|
|
||||||
process.wait(timeout=5)
|
|
||||||
|
|
||||||
|
|
||||||
def check(app: Path, output: Path):
|
|
||||||
report = {"status": "failed", "architecture": platform.machine(),
|
|
||||||
"source_commit": os.environ.get("GITHUB_SHA"),
|
|
||||||
"checks": [], "limitations": "Loopback IRC and window capture; no TLS, input, notifications or stability coverage."}
|
|
||||||
with IRCServer() as server, tempfile.TemporaryDirectory(prefix="ZoiteChat visual check ") as temporary:
|
|
||||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
||||||
thread.start()
|
|
||||||
try:
|
|
||||||
directory = Path(temporary)
|
|
||||||
capture = directory / "capture-window"
|
|
||||||
subprocess.run(["xcrun", "swiftc", str(Path(__file__).with_name("capture-window.swift")),
|
|
||||||
"-o", str(capture)], check=True, timeout=120)
|
|
||||||
relocated = directory / "Relocated App/ZoiteChat.app"
|
|
||||||
shutil.copytree(app.resolve(), relocated)
|
|
||||||
subprocess.run(["codesign", "--verify", "--deep", "--strict", str(relocated)], check=True, timeout=30)
|
|
||||||
report["checks"].append("relocated bundle signature verified")
|
|
||||||
config = directory / "config"
|
|
||||||
config.mkdir()
|
|
||||||
(config / "zoitechat.conf").write_text(
|
|
||||||
"gui_slist_skip = 1\ngui_win_width = 1100\ngui_win_height = 700\n"
|
|
||||||
"gui_win_left = 60\ngui_win_top = 60\n"
|
|
||||||
"irc_nick1 = MacTester\nirc_user_name = MacTester\nirc_real_name = Visual check\n"
|
|
||||||
"net_proxy_type = 0\nidentd_server = 0\n", encoding="utf-8")
|
|
||||||
binary = relocated / "Contents/MacOS/zoitechat"
|
|
||||||
with (output / "startup.log").open("w", encoding="utf-8") as log:
|
|
||||||
# SERVER defaults to TLS even for an irc:// URL in this branch.
|
|
||||||
# Explicit plaintext is restricted to our loopback-only fixture.
|
|
||||||
command = f"SERVER -insecure -noproxy irc://127.0.0.1:{server.server_address[1]}/ci-smoke"
|
|
||||||
process = subprocess.Popen([str(binary), "--no-auto", "--cfgdir", str(config),
|
|
||||||
"--command", command],
|
|
||||||
stdout=log, stderr=log)
|
|
||||||
try:
|
|
||||||
deadline = time.monotonic() + 60
|
|
||||||
while not (server.joined.is_set() and server.pong.is_set()):
|
|
||||||
if server.error:
|
|
||||||
raise RuntimeError(server.error)
|
|
||||||
if process.poll() is not None:
|
|
||||||
raise RuntimeError(f"Application exited with code {process.returncode}; see startup.log")
|
|
||||||
if time.monotonic() >= deadline:
|
|
||||||
raise RuntimeError("Timed out waiting for channel join and PING/PONG; see irc.log")
|
|
||||||
time.sleep(0.2)
|
|
||||||
report["checks"] += ["connected to loopback IRC", "joined channel", "answered server PING"]
|
|
||||||
time.sleep(1)
|
|
||||||
window = subprocess.check_output([str(capture), str(process.pid), str(output / "zoitechat.png")],
|
|
||||||
text=True, timeout=40)
|
|
||||||
report["window"] = json.loads(window)
|
|
||||||
if process.poll() is not None:
|
|
||||||
raise RuntimeError("Application exited during capture")
|
|
||||||
report["checks"].append("captured a nonblank application window")
|
|
||||||
report["status"] = "passed"
|
|
||||||
finally:
|
|
||||||
stop(process)
|
|
||||||
except Exception as error:
|
|
||||||
report["error"] = str(error)
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
server.shutdown()
|
|
||||||
thread.join(timeout=5)
|
|
||||||
(output / "irc.log").write_text("\n".join(server.transcript) + "\n", encoding="utf-8")
|
|
||||||
(output / "result.json").write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
|
||||||
parser.add_argument("app", type=Path)
|
|
||||||
parser.add_argument("--output", type=Path, default=Path("dist/visual-check"))
|
|
||||||
args = parser.parse_args()
|
|
||||||
if platform.system() != "Darwin":
|
|
||||||
parser.error("This check must run on macOS with a logged-in desktop")
|
|
||||||
args.output.mkdir(parents=True, exist_ok=False)
|
|
||||||
check(args.app, args.output.resolve())
|
|
||||||
print("Visual check passed; screenshot and evidence saved to " + str(args.output))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
<?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:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
|
|
||||||
xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
|
|
||||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
|
||||||
IgnorableNamespaces="uap uap3 uap10 rescap">
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Identity values are filled by build-msix.ps1. For Microsoft Store
|
|
||||||
submissions they must exactly match Product management > Product identity
|
|
||||||
in Partner Center.
|
|
||||||
-->
|
|
||||||
<Identity
|
|
||||||
Name="@IDENTITY_NAME@"
|
|
||||||
Publisher="@PUBLISHER@"
|
|
||||||
Version="@PACKAGE_VERSION@"
|
|
||||||
ProcessorArchitecture="x64" />
|
|
||||||
|
|
||||||
<Properties>
|
|
||||||
<DisplayName>ZoiteChat</DisplayName>
|
|
||||||
<PublisherDisplayName>@PUBLISHER_DISPLAY_NAME@</PublisherDisplayName>
|
|
||||||
<Description>ZoiteChat IRC client</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" />
|
|
||||||
<PackageDependency
|
|
||||||
Name="Microsoft.VCLibs.140.00.UWPDesktop"
|
|
||||||
Publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
|
|
||||||
MinVersion="14.0.30704.0" />
|
|
||||||
</Dependencies>
|
|
||||||
|
|
||||||
<Capabilities>
|
|
||||||
<rescap:Capability Name="runFullTrust" />
|
|
||||||
</Capabilities>
|
|
||||||
|
|
||||||
<Applications>
|
|
||||||
<Application
|
|
||||||
Id="ZoiteChat"
|
|
||||||
Executable="ZoiteChat\zoitechat.exe"
|
|
||||||
uap10:RuntimeBehavior="packagedClassicApp"
|
|
||||||
uap10:TrustLevel="mediumIL">
|
|
||||||
|
|
||||||
<uap:VisualElements
|
|
||||||
DisplayName="ZoiteChat"
|
|
||||||
Description="ZoiteChat IRC client"
|
|
||||||
BackgroundColor="transparent"
|
|
||||||
Square150x150Logo="Assets\Square150x150Logo.png"
|
|
||||||
Square44x44Logo="Assets\Square44x44Logo.png" />
|
|
||||||
|
|
||||||
<Extensions>
|
|
||||||
<uap3:Extension Category="windows.protocol">
|
|
||||||
<uap3:Protocol
|
|
||||||
Name="irc"
|
|
||||||
Parameters="--url="%1"" />
|
|
||||||
</uap3:Extension>
|
|
||||||
|
|
||||||
<uap3:Extension Category="windows.fileTypeAssociation">
|
|
||||||
<uap3:FileTypeAssociation
|
|
||||||
Name="zoitechattheme"
|
|
||||||
Parameters=""%1"">
|
|
||||||
<uap:DisplayName>ZoiteChat Theme</uap:DisplayName>
|
|
||||||
<uap:SupportedFileTypes>
|
|
||||||
<uap:FileType>.zct</uap:FileType>
|
|
||||||
<uap:FileType>.hct</uap:FileType>
|
|
||||||
</uap:SupportedFileTypes>
|
|
||||||
</uap3:FileTypeAssociation>
|
|
||||||
</uap3:Extension>
|
|
||||||
</Extensions>
|
|
||||||
</Application>
|
|
||||||
</Applications>
|
|
||||||
</Package>
|
|
||||||
@@ -1,392 +0,0 @@
|
|||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string] $SourceDir,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string] $OutputPath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[ValidatePattern('^\d+\.\d+\.\d+\.0$')]
|
|
||||||
[string] $PackageVersion,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string] $IdentityName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string] $Publisher,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string] $PublisherDisplayName,
|
|
||||||
|
|
||||||
[switch] $SelfSign
|
|
||||||
)
|
|
||||||
|
|
||||||
Set-StrictMode -Version Latest
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
|
|
||||||
Add-Type -AssemblyName System.Drawing
|
|
||||||
|
|
||||||
function Find-WindowsSdkTool {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string] $ToolName
|
|
||||||
)
|
|
||||||
|
|
||||||
$command = Get-Command $ToolName -ErrorAction SilentlyContinue
|
|
||||||
if ($command) {
|
|
||||||
return $command.Source
|
|
||||||
}
|
|
||||||
|
|
||||||
$kitsRoot = Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\bin'
|
|
||||||
if (-not (Test-Path $kitsRoot)) {
|
|
||||||
throw "Windows SDK bin directory was not found: $kitsRoot"
|
|
||||||
}
|
|
||||||
|
|
||||||
$tool = Get-ChildItem -Path $kitsRoot -Filter $ToolName -File -Recurse |
|
|
||||||
Where-Object { $_.FullName -match '\\x64\\' } |
|
|
||||||
Sort-Object {
|
|
||||||
$versionDirectory = Split-Path (Split-Path $_.DirectoryName -Parent) -Leaf
|
|
||||||
try {
|
|
||||||
[version] $versionDirectory
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
[version] '0.0'
|
|
||||||
}
|
|
||||||
} -Descending |
|
|
||||||
Select-Object -First 1
|
|
||||||
|
|
||||||
if (-not $tool) {
|
|
||||||
throw "$ToolName was not found in the installed Windows SDK."
|
|
||||||
}
|
|
||||||
|
|
||||||
return $tool.FullName
|
|
||||||
}
|
|
||||||
|
|
||||||
function Escape-Xml {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string] $Value
|
|
||||||
)
|
|
||||||
|
|
||||||
return [System.Security.SecurityElement]::Escape($Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
function New-MsixAsset {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[System.Drawing.Image] $SourceImage,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[int] $Width,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[int] $Height,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string] $Path
|
|
||||||
)
|
|
||||||
|
|
||||||
$bitmap = [System.Drawing.Bitmap]::new($Width, $Height)
|
|
||||||
try {
|
|
||||||
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
|
|
||||||
try {
|
|
||||||
$graphics.Clear([System.Drawing.Color]::Transparent)
|
|
||||||
$graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
|
|
||||||
$graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
|
|
||||||
$graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality
|
|
||||||
|
|
||||||
$scale = [Math]::Min($Width / $SourceImage.Width, $Height / $SourceImage.Height)
|
|
||||||
$drawWidth = [Math]::Max(1, [int][Math]::Round($SourceImage.Width * $scale))
|
|
||||||
$drawHeight = [Math]::Max(1, [int][Math]::Round($SourceImage.Height * $scale))
|
|
||||||
$x = [int](($Width - $drawWidth) / 2)
|
|
||||||
$y = [int](($Height - $drawHeight) / 2)
|
|
||||||
|
|
||||||
$graphics.DrawImage($SourceImage, $x, $y, $drawWidth, $drawHeight)
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
$graphics.Dispose()
|
|
||||||
}
|
|
||||||
|
|
||||||
$bitmap.Save($Path, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
$bitmap.Dispose()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$source = (Resolve-Path $SourceDir).Path
|
|
||||||
if (-not (Test-Path (Join-Path $source 'zoitechat.exe'))) {
|
|
||||||
throw "Staged ZoiteChat executable is missing from $source"
|
|
||||||
}
|
|
||||||
|
|
||||||
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
|
|
||||||
$templatePath = Join-Path $PSScriptRoot 'AppxManifest.xml.in'
|
|
||||||
$iconPath = Join-Path $repoRoot 'data\icons\zoitechat.png'
|
|
||||||
|
|
||||||
if (-not (Test-Path $templatePath)) {
|
|
||||||
throw "MSIX manifest template is missing: $templatePath"
|
|
||||||
}
|
|
||||||
if (-not (Test-Path $iconPath)) {
|
|
||||||
throw "ZoiteChat PNG icon is missing: $iconPath"
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([System.IO.Path]::IsPathRooted($OutputPath)) {
|
|
||||||
$output = [System.IO.Path]::GetFullPath($OutputPath)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$output = [System.IO.Path]::GetFullPath((Join-Path $PWD $OutputPath))
|
|
||||||
}
|
|
||||||
$outputDirectory = Split-Path $output -Parent
|
|
||||||
New-Item -ItemType Directory -Path $outputDirectory -Force | Out-Null
|
|
||||||
|
|
||||||
$stagingRoot = Join-Path $env:RUNNER_TEMP 'zoitechat-msix'
|
|
||||||
$verifyRoot = Join-Path $env:RUNNER_TEMP 'zoitechat-msix-verify'
|
|
||||||
$pfxPath = Join-Path $env:RUNNER_TEMP 'zoitechat-msix-dev.pfx'
|
|
||||||
|
|
||||||
foreach ($path in @($stagingRoot, $verifyRoot)) {
|
|
||||||
if (Test-Path $path) {
|
|
||||||
Remove-Item -Path $path -Recurse -Force
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Test-Path $pfxPath) {
|
|
||||||
Remove-Item -Path $pfxPath -Force
|
|
||||||
}
|
|
||||||
|
|
||||||
New-Item -ItemType Directory -Path $stagingRoot -Force | Out-Null
|
|
||||||
$appRoot = Join-Path $stagingRoot 'ZoiteChat'
|
|
||||||
New-Item -ItemType Directory -Path $appRoot -Force | Out-Null
|
|
||||||
Copy-Item -Path (Join-Path $source '*') -Destination $appRoot -Recurse -Force
|
|
||||||
|
|
||||||
# copy.vcxproj stages everything needed by every Inno component. The Store
|
|
||||||
# package keeps the useful bundled plugins and scripting bridges while letting
|
|
||||||
# the Microsoft Store own application updates. The WinSparkle updater is the
|
|
||||||
# only bundled plugin deliberately removed from the Store package.
|
|
||||||
$excludeFiles = @(
|
|
||||||
'portable-mode',
|
|
||||||
'zoitechat-text.exe',
|
|
||||||
'plugins\hcupd.dll',
|
|
||||||
'WinSparkle.dll',
|
|
||||||
'share\download.png'
|
|
||||||
)
|
|
||||||
|
|
||||||
foreach ($relativePath in $excludeFiles) {
|
|
||||||
$path = Join-Path $appRoot $relativePath
|
|
||||||
if (Test-Path $path) {
|
|
||||||
Remove-Item -Path $path -Force
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($relativePath in @('share\doc\WinSparkle')) {
|
|
||||||
$path = Join-Path $appRoot $relativePath
|
|
||||||
if (Test-Path $path) {
|
|
||||||
Remove-Item -Path $path -Recurse -Force
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($requiredPath in @(
|
|
||||||
'zoitechat.exe',
|
|
||||||
'plugins\hcnotifications-winrt.dll',
|
|
||||||
'plugins\hcchecksum.dll',
|
|
||||||
'plugins\hcexec.dll',
|
|
||||||
'plugins\hcfishlim.dll',
|
|
||||||
'plugins\hcsysinfo.dll',
|
|
||||||
'share\system.png',
|
|
||||||
'share\fonts\zoitechat\NotoColorEmoji.ttf',
|
|
||||||
'share\doc\zoitechat\fonts\OFL.txt',
|
|
||||||
'plugins\hclua.dll',
|
|
||||||
'plugins\hcperl.dll',
|
|
||||||
'plugins\hcpython3.dll',
|
|
||||||
'plugins\hcpython38.dll'
|
|
||||||
)) {
|
|
||||||
if (-not (Test-Path (Join-Path $appRoot $requiredPath))) {
|
|
||||||
throw "MSIX staging is missing required file: $requiredPath"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($requiredPattern in @('_cffi_backend.cp314*.pyd', '_cffi_backend.cp38*.pyd')) {
|
|
||||||
$match = Get-ChildItem -Path $appRoot -Filter $requiredPattern -File -ErrorAction SilentlyContinue |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($null -eq $match) {
|
|
||||||
throw "MSIX staging is missing required Python support file: $requiredPattern"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($requiredPythonFile in @('zoitechat.py', '_zoitechat.py', 'hexchat.py', 'xchat.py')) {
|
|
||||||
$path = Join-Path (Join-Path $appRoot 'python') $requiredPythonFile
|
|
||||||
if (-not (Test-Path $path)) {
|
|
||||||
throw "MSIX staging is missing required Python bridge file: $requiredPythonFile"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$assets = Join-Path $stagingRoot 'Assets'
|
|
||||||
New-Item -ItemType Directory -Path $assets -Force | Out-Null
|
|
||||||
|
|
||||||
$image = [System.Drawing.Image]::FromFile($iconPath)
|
|
||||||
try {
|
|
||||||
New-MsixAsset -SourceImage $image -Width 50 -Height 50 `
|
|
||||||
-Path (Join-Path $assets 'StoreLogo.png')
|
|
||||||
New-MsixAsset -SourceImage $image -Width 44 -Height 44 `
|
|
||||||
-Path (Join-Path $assets 'Square44x44Logo.png')
|
|
||||||
New-MsixAsset -SourceImage $image -Width 150 -Height 150 `
|
|
||||||
-Path (Join-Path $assets 'Square150x150Logo.png')
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
$image.Dispose()
|
|
||||||
}
|
|
||||||
|
|
||||||
$manifest = Get-Content $templatePath -Raw -Encoding UTF8
|
|
||||||
$manifest = $manifest.Replace('@IDENTITY_NAME@', (Escape-Xml $IdentityName))
|
|
||||||
$manifest = $manifest.Replace('@PUBLISHER@', (Escape-Xml $Publisher))
|
|
||||||
$manifest = $manifest.Replace('@PUBLISHER_DISPLAY_NAME@', (Escape-Xml $PublisherDisplayName))
|
|
||||||
$manifest = $manifest.Replace('@PACKAGE_VERSION@', (Escape-Xml $PackageVersion))
|
|
||||||
|
|
||||||
if ($manifest -match '@[A-Z0-9_]+@') {
|
|
||||||
throw "Unexpanded MSIX manifest placeholder: $($Matches[0])"
|
|
||||||
}
|
|
||||||
|
|
||||||
$manifestPath = Join-Path $stagingRoot 'AppxManifest.xml'
|
|
||||||
[System.IO.File]::WriteAllText(
|
|
||||||
$manifestPath,
|
|
||||||
$manifest,
|
|
||||||
[System.Text.UTF8Encoding]::new($false)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Catch malformed XML before MakeAppx so CI reports a useful error.
|
|
||||||
[xml](Get-Content $manifestPath -Raw -Encoding UTF8) | Out-Null
|
|
||||||
|
|
||||||
$makeAppx = Find-WindowsSdkTool -ToolName 'MakeAppx.exe'
|
|
||||||
$signtool = Find-WindowsSdkTool -ToolName 'SignTool.exe'
|
|
||||||
|
|
||||||
if (Test-Path $output) {
|
|
||||||
Remove-Item -Path $output -Force
|
|
||||||
}
|
|
||||||
|
|
||||||
& $makeAppx pack /d $stagingRoot /p $output /o
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw "MakeAppx failed with exit code $LASTEXITCODE"
|
|
||||||
}
|
|
||||||
|
|
||||||
$certificate = $null
|
|
||||||
try {
|
|
||||||
if ($SelfSign) {
|
|
||||||
# Partner Center replaces this development signature with Microsoft's
|
|
||||||
# trusted Store signature. The certificate subject must exactly match
|
|
||||||
# the manifest Publisher for SignTool to accept the package.
|
|
||||||
$certificate = New-SelfSignedCertificate `
|
|
||||||
-Type Custom `
|
|
||||||
-KeyUsage DigitalSignature `
|
|
||||||
-Subject $Publisher `
|
|
||||||
-CertStoreLocation 'Cert:\CurrentUser\My' `
|
|
||||||
-HashAlgorithm SHA256 `
|
|
||||||
-TextExtension @(
|
|
||||||
'2.5.29.37={text}1.3.6.1.5.5.7.3.3',
|
|
||||||
'2.5.29.19={text}'
|
|
||||||
) `
|
|
||||||
-FriendlyName 'ZoiteChat MSIX CI signing certificate'
|
|
||||||
|
|
||||||
if ($certificate.Subject -cne $Publisher) {
|
|
||||||
throw "Generated certificate subject '$($certificate.Subject)' does not exactly match manifest Publisher '$Publisher'."
|
|
||||||
}
|
|
||||||
|
|
||||||
$passwordText = [Guid]::NewGuid().ToString('N')
|
|
||||||
$password = ConvertTo-SecureString -String $passwordText -AsPlainText -Force
|
|
||||||
Export-PfxCertificate -Cert $certificate -FilePath $pfxPath -Password $password | Out-Null
|
|
||||||
|
|
||||||
& $signtool sign /fd SHA256 /f $pfxPath /p $passwordText $output
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw "SignTool failed with exit code $LASTEXITCODE"
|
|
||||||
}
|
|
||||||
|
|
||||||
$signature = Get-AuthenticodeSignature -FilePath $output
|
|
||||||
if (-not $signature.SignerCertificate -or
|
|
||||||
$signature.SignerCertificate.Subject -cne $Publisher) {
|
|
||||||
throw 'MSIX signature does not match the manifest Publisher.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
New-Item -ItemType Directory -Path $verifyRoot -Force | Out-Null
|
|
||||||
& $makeAppx unpack /p $output /d $verifyRoot /o
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw "MakeAppx validation unpack failed with exit code $LASTEXITCODE"
|
|
||||||
}
|
|
||||||
|
|
||||||
[xml] $verifiedManifest = Get-Content (Join-Path $verifyRoot 'AppxManifest.xml') -Raw -Encoding UTF8
|
|
||||||
$ns = New-Object System.Xml.XmlNamespaceManager($verifiedManifest.NameTable)
|
|
||||||
$ns.AddNamespace('f', 'http://schemas.microsoft.com/appx/manifest/foundation/windows10')
|
|
||||||
$identity = $verifiedManifest.SelectSingleNode('/f:Package/f:Identity', $ns)
|
|
||||||
if (-not $identity) {
|
|
||||||
throw 'Validated MSIX does not contain a package Identity.'
|
|
||||||
}
|
|
||||||
if ($identity.Name -cne $IdentityName -or
|
|
||||||
$identity.Publisher -cne $Publisher -or
|
|
||||||
$identity.Version -cne $PackageVersion) {
|
|
||||||
throw 'Validated MSIX identity does not match the requested Store identity.'
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($requiredPath in @(
|
|
||||||
'ZoiteChat\zoitechat.exe',
|
|
||||||
'ZoiteChat\plugins\hcnotifications-winrt.dll',
|
|
||||||
'ZoiteChat\plugins\hcchecksum.dll',
|
|
||||||
'ZoiteChat\plugins\hcexec.dll',
|
|
||||||
'ZoiteChat\plugins\hcfishlim.dll',
|
|
||||||
'ZoiteChat\plugins\hcsysinfo.dll',
|
|
||||||
'ZoiteChat\share\system.png',
|
|
||||||
'ZoiteChat\share\fonts\zoitechat\NotoColorEmoji.ttf',
|
|
||||||
'ZoiteChat\share\doc\zoitechat\fonts\OFL.txt',
|
|
||||||
'ZoiteChat\plugins\hclua.dll',
|
|
||||||
'ZoiteChat\plugins\hcperl.dll',
|
|
||||||
'ZoiteChat\plugins\hcpython3.dll',
|
|
||||||
'ZoiteChat\plugins\hcpython38.dll'
|
|
||||||
)) {
|
|
||||||
if (-not (Test-Path (Join-Path $verifyRoot $requiredPath))) {
|
|
||||||
throw "Validated MSIX is missing required file: $requiredPath"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($requiredPattern in @('_cffi_backend.cp314*.pyd', '_cffi_backend.cp38*.pyd')) {
|
|
||||||
$match = Get-ChildItem -Path (Join-Path $verifyRoot 'ZoiteChat') -Filter $requiredPattern -File -ErrorAction SilentlyContinue |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($null -eq $match) {
|
|
||||||
throw "Validated MSIX is missing required Python support file: $requiredPattern"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($requiredPythonFile in @('zoitechat.py', '_zoitechat.py', 'hexchat.py', 'xchat.py')) {
|
|
||||||
$path = Join-Path (Join-Path $verifyRoot 'ZoiteChat\python') $requiredPythonFile
|
|
||||||
if (-not (Test-Path $path)) {
|
|
||||||
throw "Validated MSIX is missing required Python bridge file: $requiredPythonFile"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($excludedPath in @(
|
|
||||||
'ZoiteChat\portable-mode',
|
|
||||||
'ZoiteChat\zoitechat-text.exe',
|
|
||||||
'ZoiteChat\plugins\hcupd.dll',
|
|
||||||
'ZoiteChat\WinSparkle.dll',
|
|
||||||
'ZoiteChat\share\download.png'
|
|
||||||
)) {
|
|
||||||
if (Test-Path (Join-Path $verifyRoot $excludedPath)) {
|
|
||||||
throw "MSIX incorrectly contains excluded file/directory: $excludedPath"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
if ($certificate) {
|
|
||||||
Remove-Item -Path "Cert:\CurrentUser\My\$($certificate.Thumbprint)" -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
if (Test-Path $pfxPath) {
|
|
||||||
Remove-Item -Path $pfxPath -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
foreach ($path in @($stagingRoot, $verifyRoot)) {
|
|
||||||
if (Test-Path $path) {
|
|
||||||
Remove-Item -Path $path -Recurse -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $output)) {
|
|
||||||
throw "Expected MSIX was not created: $output"
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "Created MSIX: $output"
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
|
||||||
<!--
|
|
||||||
This MSI intentionally mirrors the staged portion of the normal Inno Setup
|
|
||||||
payload. Optional/custom-only files are excluded below. In particular,
|
|
||||||
portable-mode must never be harvested because copy.vcxproj always stages
|
|
||||||
that marker. Inno-only download-at-install components remain outside the MSI.
|
|
||||||
-->
|
|
||||||
<Package
|
|
||||||
Name="ZoiteChat"
|
|
||||||
Manufacturer="ZoiteChat"
|
|
||||||
Version="$(ProductVersion)"
|
|
||||||
UpgradeCode="{E9F09DF3-12FA-4BDF-A8AA-A6D62A327ED5}"
|
|
||||||
Scope="perMachine"
|
|
||||||
InstallerVersion="500"
|
|
||||||
Compressed="yes">
|
|
||||||
|
|
||||||
<MajorUpgrade
|
|
||||||
AllowSameVersionUpgrades="yes"
|
|
||||||
DowngradeErrorMessage="A newer version of ZoiteChat is already installed." />
|
|
||||||
|
|
||||||
<MediaTemplate EmbedCab="yes" CompressionLevel="high" />
|
|
||||||
|
|
||||||
<Icon Id="ZoiteChat.ico" SourceFile="$(sys.SOURCEFILEDIR)..\..\data\icons\zoitechat.ico" />
|
|
||||||
<Property Id="ARPPRODUCTICON" Value="ZoiteChat.ico" />
|
|
||||||
<Property Id="ARPHELPLINK" Value="https://github.com/ZoiteChat/zoitechat/issues" />
|
|
||||||
<Property Id="ARPURLINFOABOUT" Value="https://zoitechat.org/" />
|
|
||||||
<Property Id="ARPURLUPDATEINFO" Value="https://zoitechat.org/downloads.html" />
|
|
||||||
<Property Id="ARPNOMODIFY" Value="1" />
|
|
||||||
|
|
||||||
<!--
|
|
||||||
The Inno installer can bootstrap the VC++ runtime. A plain MSI should not
|
|
||||||
run an EXE prerequisite as an MSI custom action, so require the runtime.
|
|
||||||
Administrative extraction is allowed regardless of prerequisite state.
|
|
||||||
-->
|
|
||||||
<Property Id="VCREDIST_X64_INSTALLED">
|
|
||||||
<RegistrySearch
|
|
||||||
Id="FindVCRedistX64"
|
|
||||||
Root="HKLM"
|
|
||||||
Key="SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64"
|
|
||||||
Name="Installed"
|
|
||||||
Type="raw"
|
|
||||||
Bitness="always64" />
|
|
||||||
</Property>
|
|
||||||
<Launch
|
|
||||||
Condition="Installed OR ACTION = "ADMIN" OR VCREDIST_X64_INSTALLED = "#1""
|
|
||||||
Message="Microsoft Visual C++ 2015-2022 x64 Redistributable is required before installing ZoiteChat." />
|
|
||||||
<Launch
|
|
||||||
Condition="Installed OR ACTION = "ADMIN" OR VersionNT64 > 601 OR (VersionNT64 = 601 AND ServicePackLevel >= 1)"
|
|
||||||
Message="ZoiteChat requires 64-bit Windows 7 SP1 or later." />
|
|
||||||
|
|
||||||
<StandardDirectory Id="ProgramFiles6432Folder">
|
|
||||||
<Directory Id="INSTALLFOLDER" Name="ZoiteChat" />
|
|
||||||
</StandardDirectory>
|
|
||||||
|
|
||||||
<StandardDirectory Id="ProgramMenuFolder">
|
|
||||||
<Directory Id="ApplicationProgramsFolder" Name="ZoiteChat" />
|
|
||||||
</StandardDirectory>
|
|
||||||
|
|
||||||
<Feature Id="MainFeature" Title="ZoiteChat" Level="1">
|
|
||||||
<ComponentGroupRef Id="ApplicationFiles" />
|
|
||||||
<ComponentRef Id="StartMenuShortcuts" />
|
|
||||||
<ComponentRef Id="ProtocolAndThemeAssociations" />
|
|
||||||
</Feature>
|
|
||||||
</Package>
|
|
||||||
|
|
||||||
<Fragment>
|
|
||||||
<ComponentGroup Id="ApplicationFiles">
|
|
||||||
<Files Directory="INSTALLFOLDER" Include="$(SourceDir)\**">
|
|
||||||
<!-- Created by copy.vcxproj for Inno's optional portable task only. -->
|
|
||||||
<Exclude Files="$(SourceDir)\portable-mode" />
|
|
||||||
<!-- Inno custom-only application component. -->
|
|
||||||
<Exclude Files="$(SourceDir)\zoitechat-text.exe" />
|
|
||||||
<!--
|
|
||||||
Keep the bundled utility and scripting plugins, including Python
|
|
||||||
3.14 and 3.8 support, available to non-Inno installations.
|
|
||||||
-->
|
|
||||||
</Files>
|
|
||||||
</ComponentGroup>
|
|
||||||
</Fragment>
|
|
||||||
|
|
||||||
<Fragment>
|
|
||||||
<Component Id="StartMenuShortcuts" Directory="ApplicationProgramsFolder" Guid="*">
|
|
||||||
<Shortcut
|
|
||||||
Id="ZoiteChatShortcut"
|
|
||||||
Name="ZoiteChat"
|
|
||||||
Description="ZoiteChat IRC client"
|
|
||||||
Target="[INSTALLFOLDER]zoitechat.exe"
|
|
||||||
WorkingDirectory="INSTALLFOLDER"
|
|
||||||
Icon="ZoiteChat.ico">
|
|
||||||
<ShortcutProperty Key="System.AppUserModel.ID" Value="ZoiteChat.Desktop.Notify" />
|
|
||||||
</Shortcut>
|
|
||||||
|
|
||||||
<Shortcut
|
|
||||||
Id="ZoiteChatSafeModeShortcut"
|
|
||||||
Name="ZoiteChat Safe Mode"
|
|
||||||
Description="Start ZoiteChat without auto-connect or plugins"
|
|
||||||
Target="[INSTALLFOLDER]zoitechat.exe"
|
|
||||||
Arguments="--no-auto --no-plugins"
|
|
||||||
WorkingDirectory="INSTALLFOLDER"
|
|
||||||
Icon="ZoiteChat.ico" />
|
|
||||||
|
|
||||||
<Shortcut
|
|
||||||
Id="ZoiteChatChangelogShortcut"
|
|
||||||
Name="ZoiteChat ChangeLog"
|
|
||||||
Target="[INSTALLFOLDER]changelog.url" />
|
|
||||||
|
|
||||||
<Shortcut
|
|
||||||
Id="ZoiteChatReadmeShortcut"
|
|
||||||
Name="ZoiteChat ReadMe"
|
|
||||||
Target="[INSTALLFOLDER]readme.url" />
|
|
||||||
|
|
||||||
<Shortcut
|
|
||||||
Id="ZoiteChatUninstallShortcut"
|
|
||||||
Name="Uninstall ZoiteChat"
|
|
||||||
Target="[System64Folder]msiexec.exe"
|
|
||||||
Arguments="/x [ProductCode]" />
|
|
||||||
|
|
||||||
<RemoveFolder Id="RemoveApplicationProgramsFolder" On="uninstall" />
|
|
||||||
<RegistryValue
|
|
||||||
Root="HKLM"
|
|
||||||
Key="Software\ZoiteChat\Installer"
|
|
||||||
Name="StartMenuShortcuts"
|
|
||||||
Type="integer"
|
|
||||||
Value="1"
|
|
||||||
KeyPath="yes" />
|
|
||||||
</Component>
|
|
||||||
</Fragment>
|
|
||||||
|
|
||||||
<Fragment>
|
|
||||||
<Component Id="ProtocolAndThemeAssociations" Directory="INSTALLFOLDER" Guid="*">
|
|
||||||
<RegistryValue
|
|
||||||
Root="HKCR"
|
|
||||||
Key="irc"
|
|
||||||
Type="string"
|
|
||||||
Value="URL:IRC Protocol"
|
|
||||||
KeyPath="yes" />
|
|
||||||
<RegistryValue Root="HKCR" Key="irc" Name="URL Protocol" Type="string" Value="" />
|
|
||||||
<RegistryValue Root="HKCR" Key="irc\DefaultIcon" Type="string" Value="[INSTALLFOLDER]zoitechat.exe,0" />
|
|
||||||
<RegistryValue Root="HKCR" Key="irc\shell" Type="string" Value="open" />
|
|
||||||
<RegistryValue
|
|
||||||
Root="HKCR"
|
|
||||||
Key="irc\shell\open\command"
|
|
||||||
Type="string"
|
|
||||||
Value=""[INSTALLFOLDER]zoitechat.exe" --url="%1"" />
|
|
||||||
|
|
||||||
<RegistryValue Root="HKCR" Key=".zct" Type="string" Value="ZoiteChat.Theme" />
|
|
||||||
<RegistryValue Root="HKCR" Key=".hct" Type="string" Value="ZoiteChat.Theme" />
|
|
||||||
<RegistryValue Root="HKCR" Key="ZoiteChat.Theme" Type="string" Value="ZoiteChat Theme" />
|
|
||||||
<RegistryValue
|
|
||||||
Root="HKCR"
|
|
||||||
Key="ZoiteChat.Theme\DefaultIcon"
|
|
||||||
Type="string"
|
|
||||||
Value="[INSTALLFOLDER]zoitechat.exe,0" />
|
|
||||||
<RegistryValue
|
|
||||||
Root="HKCR"
|
|
||||||
Key="ZoiteChat.Theme\shell\open\command"
|
|
||||||
Type="string"
|
|
||||||
Value=""[INSTALLFOLDER]zoitechat.exe" "%1"" />
|
|
||||||
</Component>
|
|
||||||
</Fragment>
|
|
||||||
</Wix>
|
|
||||||
Reference in new issue
Block a user