Compare commits

..
Author SHA1 Message Date
deepend 21419b0c76 add MSI and MSIX packaging 2026-09-26 09:39:27 -06:00
deepend 58208b81f2 Fix away tracking
(cherry picked from commit 87410aea50)
2026-09-15 13:04:10 -06:00
deepend e7631701ce release: prepare 2.19.1 2026-09-15 09:28:29 -06:00
deepend b5c3be8ff1 defer channel tree scrolling until realized
(cherry picked from commit e94ffd9d79)
2026-09-15 09:12:34 -06:00
deepend 2b9a39783b fix theme service tests
(cherry picked from commit ff4217c3d1)
2026-09-15 09:12:30 -06:00
Christopher Byrne ecdb462ff8 meson.build: Reflect usage of 0.59 Meson features
src/fe-gtk/meson.build calls feature_opt.allowed() which wasn't
added until 0.59.

Signed-off-by: Christopher Byrne <[email protected]>
(cherry picked from commit 5018a3a848)
2026-09-15 09:12:25 -06:00
Christopher Byrne 78eb1882cb data/misc/meson.build: Fix referencing possibly undefined variable "appstreamcli"
If meson is passed -Dinstall-plugin-metainfo=false, the "appstreamcli"
will not be defined, but it is later referenced in global scope, which
causes and "undefined variable" error in Meson

Signed-off-by: Christopher Byrne <[email protected]>
(cherry picked from commit df1c74a91d)
2026-09-15 09:12:21 -06:00
deepend 6c95db8539 Fix channel ACTION routing after self-message handling
(cherry picked from commit bb3893d36c)
2026-09-15 09:12:16 -06:00
ThePeGaSuS 75286642df Fix CTCP ACTION not rerouting self-sent /me to correct PM window
Mirrors the PRIVMSG fix: replayed /me messages sent by our own nick
via WeeChat relay backlog were always passed with fromme=FALSE,
so they never got rerouted to the actual PM query window.

(cherry picked from commit c725adaa09)
2026-09-15 09:12:13 -06:00
ThePeGaSuS 8044b8562d Fix: private messages split into two windows over WeeChat's IRC relay
When you send a private message, ZoiteChat sometimes gets the same
message echoed back by the server so it can show it in the right
window. Whether it recognizes that echo depended on the server first
confirming a feature called "echo-message" during connection setup.

WeeChat's relay doesn't seem to confirm that feature the way other
servers (or ZNC) do, so ZoiteChat didn't recognize its own echoed
messages and treated them as brand new incoming messages from
yourself, opening a second, separate conversation window instead of
using the one you were already typing in.

This makes ZoiteChat recognize its own message by checking who sent
it, instead of only trusting that feature confirmation. That way,
sent and received messages always end up in the same conversation
window, regardless of how well the server/relay announces its
capabilities.

(cherry picked from commit 0ba523d071)
2026-09-15 09:12:09 -06:00
deepend de6e16cbdf fix startup error on flatpak
(cherry picked from commit 36f0d7450c)
2026-09-15 09:12:02 -06:00
deepend 909309cc2e WIN32: fix installer theme download URL. 2026-08-15 12:49:58 -06:00
deepend-tildeclub 73d958911f Merge pull request #364 from ZoiteChat/master
run Ci for release branches
2026-08-15 12:10:29 -06:00
deepend 6b4a2b5406 fix unmark away option 2026-08-15 12:07:41 -06:00
18 changed files with 1234 additions and 38 deletions
+382
View File
@@ -0,0 +1,382 @@
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
Download-WithRetry -Url https://github.com/ZoiteChat/gvsbuild/releases/download/zoitechat-2.18.1/GTK3_Gvsbuild_zoitechat-2.18.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
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
)
- 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"
}
# Windows Installer ProductVersion is numeric. Keep the full project
# version in the artifact filename while using major.minor.patch in MSI.
$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'
)) {
$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 version field and requires it to
# be zero in submitted Windows 10/11 app packages.
$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"
# Microsoft Store identity reserved for ZoiteChat in Partner Center.
# These three values are case-sensitive and must exactly match the
# Product identity page for the Store submission.
$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 }}
+1 -1
View File
@@ -1 +1 @@
2.19.0 2.19.1
+1 -1
View File
@@ -2,6 +2,7 @@ appdir = join_paths(get_option('datadir'), 'applications')
metainfodir = join_paths(get_option('datadir'), 'metainfo') metainfodir = join_paths(get_option('datadir'), 'metainfo')
mimedir = join_paths(get_option('datadir'), 'mime', 'packages') mimedir = join_paths(get_option('datadir'), 'mime', 'packages')
desktop_utils = find_program('desktop-file-validate', required: false) desktop_utils = find_program('desktop-file-validate', required: false)
appstreamcli = find_program('appstreamcli', required: false)
if get_option('gtk-frontend') if get_option('gtk-frontend')
if get_option('install-appdata') if get_option('install-appdata')
@@ -13,7 +14,6 @@ if get_option('gtk-frontend')
install_dir: metainfodir install_dir: metainfodir
) )
appstreamcli = find_program('appstreamcli', required: false)
if appstreamcli.found() if appstreamcli.found()
test('Validate net.zoite.Zoitechat.appdata.xml', appstreamcli, test('Validate net.zoite.Zoitechat.appdata.xml', appstreamcli,
args: ['validate', zoitechat_appdata] args: ['validate', zoitechat_appdata]
+1 -1
View File
@@ -1,6 +1,6 @@
project('zoitechat', 'c', project('zoitechat', 'c',
version: run_command('cat', 'VERSION', check: true).stdout().strip(), version: run_command('cat', 'VERSION', check: true).stdout().strip(),
meson_version: '>= 0.55.0', meson_version: '>= 0.59.0',
default_options: [ default_options: [
'c_std=c17', 'c_std=c17',
'buildtype=debugoptimized', 'buildtype=debugoptimized',
+15 -1
View File
@@ -126,7 +126,21 @@ ctcp_handle (session *sess, char *to, char *nick, char *ip,
if (ctcp_check (sess, nick, word, word_eol, word[4] + ctcp_offset)) if (ctcp_check (sess, nick, word, word_eol, word[4] + ctcp_offset))
goto generic; goto generic;
inbound_action (sess, to, nick, ip, msg + 7, FALSE, tags_data->identified, tags_data); {
gboolean private_fromme = !serv->p_cmp (nick, serv->nick) && !is_channel (serv, to);
if (private_fromme)
{
session *target = find_dialog (serv, to);
if (target)
sess = target;
else if (serv->front_session)
sess = serv->front_session;
}
inbound_action (sess, to, nick, ip, msg + 7, private_fromme, tags_data->identified, tags_data);
}
return; return;
} }
+3 -3
View File
@@ -13,9 +13,9 @@ dbus_includes = [
dbus_service_dir = join_paths(get_option('datadir'), 'dbus-1/services') dbus_service_dir = join_paths(get_option('datadir'), 'dbus-1/services')
if get_option('dbus-service-use-appid') if get_option('dbus-service-use-appid')
dbus_service_name = 'io.github.Hexchat' dbus_service_name = 'net.zoite.Zoitechat'
dbus_output_file = 'io.github.Hexchat.service' dbus_output_file = 'net.zoite.Zoitechat.service'
dbus_cargs = '-DDBUS_SERVICE="io.github.Hexchat"' dbus_cargs = '-DDBUS_SERVICE="net.zoite.Zoitechat"'
else else
dbus_service_name = 'org.zoitechat.service' dbus_service_name = 'org.zoitechat.service'
dbus_output_file = 'org.zoitechat.service.service' dbus_output_file = 'org.zoitechat.service.service'
+4 -2
View File
@@ -858,8 +858,10 @@ path_depth_from_root (const char *base, const char *path)
static gint static gint
theme_root_candidate_compare (gconstpointer a, gconstpointer b) theme_root_candidate_compare (gconstpointer a, gconstpointer b)
{ {
const ThemeRootCandidate *ca = a; /* g_ptr_array_sort() passes pointers to the array slots, not the values
const ThemeRootCandidate *cb = b; * stored in those slots. Dereference once before comparing candidates. */
const ThemeRootCandidate *ca = *(ThemeRootCandidate * const *) a;
const ThemeRootCandidate *cb = *(ThemeRootCandidate * const *) b;
if (ca->has_index_theme != cb->has_index_theme) if (ca->has_index_theme != cb->has_index_theme)
return ca->has_index_theme ? -1 : 1; return ca->has_index_theme ? -1 : 1;
if (ca->depth != cb->depth) if (ca->depth != cb->depth)
+2 -1
View File
@@ -675,7 +675,8 @@ inbound_chanmsg (server *serv, session *sess, char *chan, char *from,
if (fromme) if (fromme)
{ {
if (prefs.hex_away_auto_unmark && serv->is_away && !tags_data->timestamp) if (prefs.hex_away_auto_unmark && serv->is_away &&
(!tags_data->timestamp || serv->have_echo_message))
sess->server->p_set_back (sess->server); sess->server->p_set_back (sess->server);
reply_context_print (sess, tags_data); reply_context_print (sess, tags_data);
EMIT_SIGNAL_TIMESTAMP (XP_TE_UCHANMSG, sess, from, text, nickchar, NULL, EMIT_SIGNAL_TIMESTAMP (XP_TE_UCHANMSG, sess, from, text, nickchar, NULL,
+92 -3
View File
@@ -86,6 +86,88 @@ plugin_windows_needs_python38 (void)
return version.dwMajorVersion < 10; return version.dwMajorVersion < 10;
} }
/*
* The Windows scripting plugins are linked against exact runtime DLL names.
* Keep the bridge DLLs installed and resolve the matching system runtimes at
* startup so installing Python/Perl later only requires restarting ZoiteChat.
*/
static gboolean
plugin_windows_dll_available (const wchar_t *dll_name)
{
return SearchPathW (NULL, dll_name, NULL, 0, NULL, NULL) != 0;
}
/*
* MSIX packaged processes do not search the normal user PATH for dependent
* DLLs. Official CPython installs register an InstallPath using PEP 514, so
* explicitly load pythonXY.dll from that absolute path before loading the
* ZoiteChat Python bridge. The loaded-module list will then satisfy the
* hcpython*.dll import of the same Python runtime DLL.
*/
static HMODULE
plugin_windows_load_python_runtime_from_registry (const wchar_t *version,
const wchar_t *dll_name)
{
static const HKEY roots[] = { HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE };
wchar_t key_name[128];
wchar_t install_path[32768];
wchar_t dll_path[32768];
DWORD install_size;
LONG status;
HMODULE module;
int install_len;
const wchar_t *separator;
guint i;
if (_snwprintf_s (key_name, G_N_ELEMENTS (key_name), _TRUNCATE,
L"Software\\Python\\PythonCore\\%ls\\InstallPath",
version) < 0)
return NULL;
for (i = 0; i < G_N_ELEMENTS (roots); i++)
{
install_size = sizeof (install_path);
status = RegGetValueW (roots[i], key_name, NULL,
RRF_RT_REG_SZ | RRF_RT_REG_EXPAND_SZ,
NULL, install_path, &install_size);
if (status != ERROR_SUCCESS)
continue;
install_path[G_N_ELEMENTS (install_path) - 1] = L'\0';
install_len = lstrlenW (install_path);
if (install_len == 0)
continue;
separator = (install_path[install_len - 1] == L'\\' ||
install_path[install_len - 1] == L'/') ? L"" : L"\\";
if (_snwprintf_s (dll_path, G_N_ELEMENTS (dll_path), _TRUNCATE,
L"%ls%ls%ls", install_path, separator, dll_name) < 0)
continue;
module = LoadLibraryW (dll_path);
if (module != NULL)
return module;
}
return NULL;
}
static gboolean
plugin_windows_prepare_python_runtime (const wchar_t *version,
const wchar_t *dll_name)
{
HMODULE module;
module = GetModuleHandleW (dll_name);
if (module == NULL)
module = LoadLibraryW (dll_name);
if (module == NULL)
module = plugin_windows_load_python_runtime_from_registry (version, dll_name);
return module != NULL;
}
#endif #endif
/* crafted to be an even 32 bytes */ /* crafted to be an even 32 bytes */
@@ -521,11 +603,18 @@ plugin_auto_load (session *sess)
for_files (lib_dir, "hcexec.dll", plugin_auto_load_cb); for_files (lib_dir, "hcexec.dll", plugin_auto_load_cb);
for_files (lib_dir, "hcfishlim.dll", plugin_auto_load_cb); for_files (lib_dir, "hcfishlim.dll", plugin_auto_load_cb);
for_files(lib_dir, "hclua.dll", plugin_auto_load_cb); for_files(lib_dir, "hclua.dll", plugin_auto_load_cb);
for_files (lib_dir, "hcperl.dll", plugin_auto_load_cb); if (plugin_windows_dll_available (L"perl542.dll"))
for_files (lib_dir, "hcperl.dll", plugin_auto_load_cb);
if (plugin_windows_needs_python38 ()) if (plugin_windows_needs_python38 ())
for_files (lib_dir, "hcpython38.dll", plugin_auto_load_cb); {
if (plugin_windows_prepare_python_runtime (L"3.8", L"python38.dll"))
for_files (lib_dir, "hcpython38.dll", plugin_auto_load_cb);
}
else else
for_files (lib_dir, "hcpython3.dll", plugin_auto_load_cb); {
if (plugin_windows_prepare_python_runtime (L"3.14", L"python314.dll"))
for_files (lib_dir, "hcpython3.dll", plugin_auto_load_cb);
}
for_files (lib_dir, "hcupd.dll", plugin_auto_load_cb); for_files (lib_dir, "hcupd.dll", plugin_auto_load_cb);
for_files (lib_dir, "hcsysinfo.dll", plugin_auto_load_cb); for_files (lib_dir, "hcsysinfo.dll", plugin_auto_load_cb);
#else #else
+1 -1
View File
@@ -1368,7 +1368,7 @@ process_named_msg (session *sess, char *type, char *word[], char *word_eol[],
{ {
if (ignore_check (word[1], IG_PRIV)) if (ignore_check (word[1], IG_PRIV))
return; return;
if (serv->have_echo_message && !serv->p_cmp (nick, serv->nick)) if (!serv->p_cmp (nick, serv->nick))
{ {
session *target_sess = find_dialog (serv, to); session *target_sess = find_dialog (serv, to);
+1 -2
View File
@@ -463,8 +463,7 @@ doover:
while (list) while (list)
{ {
sess = list->data; sess = list->data;
if (!sess->server->have_awaynotify) sess->done_away_check = FALSE;
sess->done_away_check = FALSE;
list = list->next; list = list->next;
} }
loop++; loop++;
+28 -19
View File
@@ -301,26 +301,35 @@ cv_tree_focus (chan *ch)
path = gtk_tree_model_get_path (model, &ch->iter); path = gtk_tree_model_get_path (model, &ch->iter);
if (path) if (path)
{ {
/* This full section does what if (!gtk_widget_get_realized (GTK_WIDGET (tree)))
* gtk_tree_view_scroll_to_cell (tree, path, NULL, TRUE, 0.5, 0.5);
* does, except it only scrolls the window if the provided cell is
* not visible. Basic algorithm taken from gtktreeview.c */
/* obtain information to see if the cell is visible */
gtk_tree_view_get_background_area (tree, path, NULL, &cell_rect);
gtk_tree_view_get_visible_rect (tree, &vis_rect);
/* The cordinates aren't offset correctly */
gtk_tree_view_convert_widget_to_bin_window_coords ( tree, cell_rect.x, cell_rect.y, NULL, &cell_rect.y );
/* only need to scroll if out of bounds */
if (cell_rect.y < vis_rect.y ||
cell_rect.y + cell_rect.height > vis_rect.y + vis_rect.height)
{ {
dest_y = cell_rect.y - ((vis_rect.height - cell_rect.height) * 0.5); /* Layout changes can restore focus before the new tree is realized.
if (dest_y < 0) * Let GTK track the row and scroll once its geometry is available. */
dest_y = 0; gtk_tree_view_scroll_to_cell (tree, path, NULL, TRUE, 0.5, 0.5);
gtk_tree_view_scroll_to_point (tree, -1, dest_y); }
else
{
/* This full section does what
* gtk_tree_view_scroll_to_cell (tree, path, NULL, TRUE, 0.5, 0.5);
* does, except it only scrolls the window if the provided cell is
* not visible. Basic algorithm taken from gtktreeview.c */
/* obtain information to see if the cell is visible */
gtk_tree_view_get_background_area (tree, path, NULL, &cell_rect);
gtk_tree_view_get_visible_rect (tree, &vis_rect);
/* The cordinates aren't offset correctly */
gtk_tree_view_convert_widget_to_bin_window_coords ( tree, cell_rect.x, cell_rect.y, NULL, &cell_rect.y );
/* only need to scroll if out of bounds */
if (cell_rect.y < vis_rect.y ||
cell_rect.y + cell_rect.height > vis_rect.y + vis_rect.height)
{
dest_y = cell_rect.y - ((vis_rect.height - cell_rect.height) * 0.5);
if (dest_y < 0)
dest_y = 0;
gtk_tree_view_scroll_to_point (tree, -1, dest_y);
}
} }
/* theft done, now make it focused like */ /* theft done, now make it focused like */
gtk_tree_view_set_cursor (tree, path, NULL, FALSE); gtk_tree_view_set_cursor (tree, path, NULL, FALSE);
+26
View File
@@ -124,12 +124,38 @@ create_msg_dialog (gchar *title, gchar *message)
static char *win32_argv0_dir; static char *win32_argv0_dir;
static gboolean
win32_has_package_identity (void)
{
HMODULE kernel32;
LONG (WINAPI *get_current_aumid) (UINT32 *, PWSTR);
UINT32 length = 0;
kernel32 = GetModuleHandleW (L"kernel32.dll");
if (!kernel32)
return FALSE;
get_current_aumid = (LONG (WINAPI *) (UINT32 *, PWSTR))
GetProcAddress (kernel32, "GetCurrentApplicationUserModelId");
if (!get_current_aumid)
return FALSE;
return get_current_aumid (&length, NULL) == ERROR_INSUFFICIENT_BUFFER;
}
static void static void
win32_set_appusermodelid (void) win32_set_appusermodelid (void)
{ {
HMODULE shell32; HMODULE shell32;
HRESULT (WINAPI *set_appid) (PCWSTR); HRESULT (WINAPI *set_appid) (PCWSTR);
/*
* Packaged desktop apps must use the AUMID assigned by the Windows app
* package. Keep the existing explicit AUMID only for Inno/MSI installs.
*/
if (win32_has_package_identity ())
return;
shell32 = GetModuleHandleW (L"shell32.dll"); shell32 = GetModuleHandleW (L"shell32.dll");
if (!shell32) if (!shell32)
shell32 = LoadLibraryW (L"shell32.dll"); shell32 = LoadLibraryW (L"shell32.dll");
@@ -22,6 +22,8 @@
#include <string> #include <string>
#include <codecvt> #include <codecvt>
#include <vector>
#include <windows.h>
#include <strsafe.h> #include <strsafe.h>
#include <roapi.h> #include <roapi.h>
@@ -32,6 +34,30 @@ using namespace Windows::Data::Xml::Dom;
static ToastNotifier ^ notifier = nullptr; static ToastNotifier ^ notifier = nullptr;
static Platform::String ^
current_package_aumid (void)
{
HMODULE kernel32;
LONG (WINAPI *get_current_aumid) (UINT32 *, PWSTR);
UINT32 length = 0;
kernel32 = GetModuleHandleW (L"kernel32.dll");
if (!kernel32)
return nullptr;
get_current_aumid = (LONG (WINAPI *) (UINT32 *, PWSTR))
GetProcAddress (kernel32, "GetCurrentApplicationUserModelId");
if (!get_current_aumid ||
get_current_aumid (&length, nullptr) != ERROR_INSUFFICIENT_BUFFER)
return nullptr;
std::vector<wchar_t> app_id (length);
if (get_current_aumid (&length, app_id.data ()) != ERROR_SUCCESS)
return nullptr;
return ref new Platform::String (app_id.data ());
}
static std::wstring static std::wstring
widen(const std::string & to_widen) widen(const std::string & to_widen)
{ {
@@ -87,7 +113,14 @@ extern "C"
try try
{ {
if (!notifier) if (!notifier)
notifier = ToastNotificationManager::CreateToastNotifier (L"ZoiteChat.Desktop.Notify"); {
auto package_aumid = current_package_aumid ();
if (package_aumid != nullptr)
notifier = ToastNotificationManager::CreateToastNotifier (package_aumid);
else
notifier = ToastNotificationManager::CreateToastNotifier (L"ZoiteChat.Desktop.Notify");
}
} }
catch (Platform::Exception ^ ex) catch (Platform::Exception ^ ex)
{ {
+2 -2
View File
@@ -373,10 +373,10 @@ begin
idpClearFiles; idpClearFiles;
if WizardIsComponentSelected('themes\windows10') then if WizardIsComponentSelected('themes\windows10') then
idpAddFile('https://dl.zoitechat.zoite.net/themes/GTK3Themes/Windows-10-3.2.1.zip', ExpandConstant('{tmp}\Windows-10-3.2.1.zip')); idpAddFile('https://dl.zoitechat.org/themes/GTK3Themes/Windows-10-3.2.1.zip', ExpandConstant('{tmp}\Windows-10-3.2.1.zip'));
if WizardIsComponentSelected('themes\windows10dark') then if WizardIsComponentSelected('themes\windows10dark') then
idpAddFile('https://dl.zoitechat.zoite.net/themes/GTK3Themes/Windows-10-Dark-3.2.1-dark.zip', ExpandConstant('{tmp}\Windows-10-Dark-3.2.1-dark.zip')); idpAddFile('https://dl.zoitechat.org/themes/GTK3Themes/Windows-10-Dark-3.2.1-dark.zip', ExpandConstant('{tmp}\Windows-10-Dark-3.2.1-dark.zip'));
if not WizardIsTaskSelected('portable') then if not WizardIsTaskSelected('portable') then
begin begin
+82
View File
@@ -0,0 +1,82 @@
<?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=&quot;%1&quot;" />
</uap3:Extension>
<uap3:Extension Category="windows.fileTypeAssociation">
<uap3:FileTypeAssociation
Name="zoitechattheme"
Parameters="&quot;%1&quot;">
<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>
+388
View File
@@ -0,0 +1,388 @@
[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',
'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\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"
+171
View File
@@ -0,0 +1,171 @@
<?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 = &quot;ADMIN&quot; OR VCREDIST_X64_INSTALLED = &quot;#1&quot;"
Message="Microsoft Visual C++ 2015-2022 x64 Redistributable is required before installing ZoiteChat." />
<Launch
Condition="Installed OR ACTION = &quot;ADMIN&quot; OR VersionNT64 &gt; 601 OR (VersionNT64 = 601 AND ServicePackLevel &gt;= 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" />
<!--
The MSI has no Inno-style component selection UI, so keep the
bundled utility plugins (Checksum, Exec, FiSHLiM, SysInfo) available.
-->
<!--
Keep the Python and Perl bridge plugins in the MSI. They use the
matching system runtimes when present: Python 3.14 on Windows 10+
(Python 3.8 on legacy Windows) and Perl 5.42. ZoiteChat checks for
those runtime DLLs before auto-loading the bridge plugins.
-->
</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="&quot;[INSTALLFOLDER]zoitechat.exe&quot; --url=&quot;%1&quot;" />
<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="&quot;[INSTALLFOLDER]zoitechat.exe&quot; &quot;%1&quot;" />
</Component>
</Fragment>
</Wix>