Compare commits

..
Author SHA1 Message Date
deepend 88bd1da5c4 track Python 3.14 updates and latest stable Perl 2026-10-19 21:44:29 -06:00
8 changed files with 317 additions and 17 deletions
+16 -5
View File
@@ -30,9 +30,13 @@ jobs:
- uses: actions/setup-python@v7
with:
python-version: '3.14.3'
python-version: '3.14'
check-latest: true
architecture: ${{ matrix.arch }}
- name: Test Windows runtime selection
run: python -m unittest discover -s tools/tests -p test_windows_runtimes.py
- name: Install Dependencies
run: |
New-Item -Name "deps" -ItemType "Directory" -Force | Out-Null
@@ -125,13 +129,13 @@ jobs:
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 }}
python tools/prepare-windows-runtimes.py
if ($LASTEXITCODE -ne 0) { throw 'Could not prepare Windows scripting runtimes' }
$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")) {
foreach ($pyDir in @("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 }
@@ -162,7 +166,7 @@ jobs:
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 }}"
set "PYTHON_DIR=C:\gtk-build\python-3.14\${{ matrix.platform }}"
if not exist "%PYTHON_DIR%\libs\python314.lib" (
echo Missing %PYTHON_DIR%\libs\python314.lib
dir "%PYTHON_DIR%\libs"
@@ -188,6 +192,13 @@ jobs:
msbuild win32\zoitechat.sln /m /verbosity:minimal /p:Configuration=Release /p:Platform=${{ matrix.platform }}
if errorlevel 1 exit /b 1
for %%P in (hcpython3 hcperl) do (
if not exist "..\zoitechat-build\${{ matrix.platform }}\rel\plugins\%%P.dll" (
echo Required scripting plugin %%P.dll was not built
exit /b 1
)
)
if not exist "..\zoitechat-build\${{ matrix.platform }}\rel\plugins\hcpython38.dll" (
echo hcpython38.dll was not built
exit /b 1
+16 -5
View File
@@ -31,9 +31,13 @@ jobs:
- uses: actions/setup-python@v7
with:
python-version: '3.14.3'
python-version: '3.14'
check-latest: true
architecture: ${{ matrix.arch }}
- name: Test Windows runtime selection
run: python -m unittest discover -s tools/tests -p test_windows_runtimes.py
- name: Install Dependencies
shell: pwsh
run: |
@@ -133,13 +137,13 @@ jobs:
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 }}
python tools/prepare-windows-runtimes.py
if ($LASTEXITCODE -ne 0) { throw 'Could not prepare Windows scripting runtimes' }
$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")) {
foreach ($pyDir in @("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 }
@@ -172,7 +176,7 @@ jobs:
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 }}"
set "PYTHON_DIR=C:\gtk-build\python-3.14\${{ matrix.platform }}"
if not exist "%PYTHON_DIR%\libs\python314.lib" (
echo Missing %PYTHON_DIR%\libs\python314.lib
dir "%PYTHON_DIR%\libs"
@@ -198,6 +202,13 @@ jobs:
msbuild win32\zoitechat.sln /m /verbosity:minimal /p:Configuration=Release /p:Platform=${{ matrix.platform }}
if errorlevel 1 exit /b 1
for %%P in (hcpython3 hcperl) do (
if not exist "..\zoitechat-build\${{ matrix.platform }}\rel\plugins\%%P.dll" (
echo Required scripting plugin %%P.dll was not built
exit /b 1
)
)
if not exist "..\zoitechat-build\${{ matrix.platform }}\rel\plugins\hcpython38.dll" (
echo hcpython38.dll was not built
exit /b 1
+4
View File
@@ -5,6 +5,10 @@ html/
*.patch
tags
# Win32 generated files
win32/runtimes.generated.props
win32/installer/runtimes.generated.iss
/tools/__pycache__/
/tools/tests/__pycache__/
plugins/wmpa/wmpa_h.h
plugins/wmpa/wmpa_i.c
src/htm/obj/*
+31
View File
@@ -53,6 +53,37 @@ Python path if your install is elsewhere::
cmd /c mklink /J C:\gtk-build\python-3.14\x64 C:\Python314
C:\gtk-build\python-3.14\x64\python.exe -m pip install --upgrade pip cffi
Windows runtime updates
~~~~~~~~~~~~~~~~~~~~~~~
Both Windows workflows select the latest stable Python **3.14.x** using
``actions/setup-python`` with ``check-latest: true``. Python 3.15 and prereleases
are excluded. The separate Python 3.8.10 build for older Windows is retained.
``tools/prepare-windows-runtimes.py`` selects the highest stable x64 Strawberry
Perl version from its official release feed, verifies the portable ZIP against
the published SHA-256, and checks the extracted interpreter's version and
architecture before building. Development versions (odd Perl minor numbers)
are excluded. The generated MSBuild properties select its headers and DLL;
the generated Inno include selects the matching MSI, Python installer URL and
component descriptions. Build logs record the exact selected versions.
For a manual build, first install the latest Python 3.14 point release (at least
3.14.8), prepare its junction and CFFI as above, then run from the checkout::
C:\gtk-build\python-3.14\x64\python.exe tools\prepare-windows-runtimes.py
The script downloads the complete Strawberry portable distribution under
``C:\gtk-build``. Generated configuration files are ignored by Git. Running
it again refreshes that dependency; builds without generated files retain the
original manually configured Perl bundle.
Updates happen when a new ZoiteChat build runs. An existing installation keeps
using its installed compatible Python or Perl; installing/updating ZoiteChat
does not force an upgrade of an already detected system runtime. MSI/MSIX
continue to package the scripting plugins; the Inno installer offers runtime
downloads when the corresponding runtime DLL is missing.
If you use different locations, set these environment variables before building
instead of editing ``win32\zoitechat.props``:
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""Prepare current Windows scripting dependencies and matching installer metadata."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
import tempfile
import time
import urllib.request
from urllib.parse import urlsplit
import xml.etree.ElementTree as ET
import zipfile
RELEASES_URL = "https://strawberryperl.com/releases.json"
RELEASE_ROOT = "https://github.com/StrawberryPerl/Perl-Dist-Strawberry/releases/download/"
MSBUILD_NS = "http://schemas.microsoft.com/developer/msbuild/2003"
def select_perl(releases):
"""Compare versions, not publication dates: older branches get newer releases too."""
candidates = []
for release in releases:
version = release.get("version", "")
if not re.fullmatch(r"5\.\d+\.\d+\.\d+", version):
continue
parts = tuple(map(int, version.split(".")))
if parts[1] % 2 or release.get("archname") != "MSWin32-x64-multi-thread":
continue
if not all(kind in release.get("edition", {}) for kind in ("portable", "msi")):
continue
candidates.append((parts, release))
if not candidates:
raise ValueError("No stable x64 Strawberry Perl release with portable ZIP and MSI")
release = max(candidates, key=lambda item: item[0])[1]
for kind, suffix in (("portable", "portable.zip"), ("msi", "msi")):
package = release["edition"][kind]
expected_name = f"strawberry-perl-{release['version']}-64bit-{suffix}"
if kind == "msi":
expected_name = f"strawberry-perl-{release['version']}-64bit.msi"
url = package.get("url", "")
if not url.startswith(RELEASE_ROOT) or urlsplit(url).path.rsplit("/", 1)[-1] != expected_name:
raise ValueError(f"Unexpected Strawberry Perl {kind} URL: {url}")
if not re.fullmatch(r"[0-9a-fA-F]{64}", package.get("sha256", "")):
raise ValueError(f"Missing SHA-256 for Strawberry Perl {kind}")
return release
def download(url, destination):
for attempt in range(4):
try:
request = urllib.request.Request(url, headers={"User-Agent": "ZoiteChat-build"})
with urllib.request.urlopen(request, timeout=120) as response, destination.open("wb") as output:
while chunk := response.read(1024 * 1024):
output.write(chunk)
return
except (OSError, TimeoutError):
if attempt == 3:
raise
time.sleep(2 ** attempt)
def write_metadata(repo, perl_root, release, python_version):
parts = release["version"].split(".")
perl_lib = f"perl{parts[0]}{parts[1]}"
ET.register_namespace("", MSBUILD_NS)
project = ET.Element(f"{{{MSBUILD_NS}}}Project")
group = ET.SubElement(project, "PropertyGroup")
ET.SubElement(group, "PerlPath").text = str(perl_root)
ET.SubElement(group, "PerlLib").text = perl_lib
ET.ElementTree(project).write(repo / "win32/runtimes.generated.props", encoding="utf-8", xml_declaration=True)
# Only numeric versions and validated upstream URLs enter the Inno source.
definitions = {
"PythonRuntimeVersion": python_version,
"PythonRuntimeURL": f"https://www.python.org/ftp/python/{python_version}/python-{python_version}-amd64.exe",
"PerlRuntimeVersion": release["version"],
"PerlRuntimeURL": release["edition"]["msi"]["url"],
"PerlRuntimeDLL": f"{perl_lib}.dll",
}
(repo / "win32/installer/runtimes.generated.iss").write_text(
"// Generated by tools/prepare-windows-runtimes.py\n"
+ "".join(f'#define {key} "{value}"\n' for key, value in definitions.items()),
encoding="utf-8",
)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--deps-root", type=Path, default=Path("C:/gtk-build"))
args = parser.parse_args()
if sys.platform != "win32" or sys.version_info[:2] != (3, 14) or sys.version_info[:3] < (3, 14, 8) or sys.version_info.releaselevel != "final":
parser.error("Run with stable x64 Python 3.14.8 or newer within the 3.14 branch")
if sys.maxsize <= 2 ** 32:
parser.error("The Windows build requires x64 Python")
repo = Path(__file__).resolve().parents[1]
# Do not let an interrupted refresh leave metadata from an older dependency.
for relative in ("win32/runtimes.generated.props", "win32/installer/runtimes.generated.iss"):
(repo / relative).unlink(missing_ok=True)
args.deps_root.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(dir=args.deps_root) as temporary:
temporary = Path(temporary)
metadata = temporary / "releases.json"
download(RELEASES_URL, metadata)
release = select_perl(json.loads(metadata.read_text(encoding="utf-8")))
archive = temporary / "perl.zip"
download(release["edition"]["portable"]["url"], archive)
with archive.open("rb") as source:
digest = hashlib.file_digest(source, "sha256").hexdigest()
if digest.lower() != release["edition"]["portable"]["sha256"].lower():
raise ValueError("Strawberry Perl portable ZIP SHA-256 mismatch")
# Keep the portable distribution intact, including its companion C DLLs.
target = args.deps_root / f"strawberry-perl-{release['version']}" / "x64"
extracted = temporary / "extracted"
with zipfile.ZipFile(archive) as package:
package.extractall(extracted)
target.parent.mkdir(parents=True, exist_ok=True)
if target.exists():
shutil.rmtree(target)
shutil.move(extracted, target)
perl_root = target / "perl"
environment = os.environ.copy()
paths = [str(perl_root / "bin"), str(target / "c/bin")]
environment["PATH"] = os.pathsep.join(paths + [environment.get("PATH", "")])
perl_lib = "perl" + "".join(release["version"].split(".")[:2])
for required in (perl_root / "bin/perl.exe", perl_root / f"bin/{perl_lib}.dll", perl_root / "lib/CORE/perl.h"):
if not required.is_file():
raise FileNotFoundError(f"Strawberry Perl is missing {required}")
actual = subprocess.check_output(
[str(perl_root / "bin/perl.exe"), "-MConfig", "-e", 'print "$Config{version}|$Config{archname}"'],
env=environment, text=True,
).strip()
expected = ".".join(release["version"].split(".")[:3]) + "|MSWin32-x64-multi-thread"
if actual != expected:
raise ValueError(f"Unexpected Perl runtime: {actual}; expected {expected}")
python_version = ".".join(map(str, sys.version_info[:3]))
write_metadata(repo, perl_root, release, python_version)
if os.environ.get("GITHUB_PATH"):
with open(os.environ["GITHUB_PATH"], "a", encoding="utf-8") as output:
output.write("\n".join(paths) + "\n")
print(f"Windows runtimes: Python {python_version}; Strawberry Perl {release['version']}")
if __name__ == "__main__":
main()
+81
View File
@@ -0,0 +1,81 @@
"""Regression coverage for stable release selection and build/installer alignment."""
import copy
import importlib.util
from pathlib import Path
import tempfile
import unittest
import xml.etree.ElementTree as ET
spec = importlib.util.spec_from_file_location(
"windows_runtimes", Path(__file__).resolve().parents[1] / "prepare-windows-runtimes.py"
)
runtimes = importlib.util.module_from_spec(spec)
spec.loader.exec_module(runtimes)
def release(version, arch="MSWin32-x64-multi-thread"):
prefix = runtimes.RELEASE_ROOT + "SP_" + version.replace(".", "") + "_64bit/"
stem = f"strawberry-perl-{version}-64bit"
return {
"version": version,
"archname": arch,
"edition": {
"portable": {"url": prefix + stem + "-portable.zip", "sha256": "a" * 64},
"msi": {"url": prefix + stem + ".msi", "sha256": "b" * 64},
},
}
class RuntimeTests(unittest.TestCase):
def test_compares_versions_instead_of_feed_order_or_dates(self):
releases = [release("5.40.5.1"), release("5.42.3.1"), release("5.42.0.1")]
self.assertEqual(runtimes.select_perl(releases)["version"], "5.42.3.1")
def test_numeric_patch_order(self):
self.assertEqual(runtimes.select_perl([release("5.42.9.1"), release("5.42.10.1")])["version"], "5.42.10.1")
def test_filters_development_prerelease_and_wrong_architecture(self):
releases = [release("5.43.1.1"), release("5.44.0.1-RC1"), release("5.44.0.1", "MSWin32-x86-multi-thread"), release("5.42.3.1")]
self.assertEqual(runtimes.select_perl(releases)["version"], "5.42.3.1")
def test_accepts_next_stable_branch(self):
self.assertEqual(runtimes.select_perl([release("5.42.3.1"), release("5.44.0.1")])["version"], "5.44.0.1")
def test_requires_both_build_and_installer_packages(self):
newer = release("5.44.0.1")
del newer["edition"]["msi"]
self.assertEqual(runtimes.select_perl([newer, release("5.42.3.1")])["version"], "5.42.3.1")
def test_fails_if_no_suitable_release_exists(self):
with self.assertRaises(ValueError):
runtimes.select_perl([release("5.43.1.1")])
def test_rejects_bad_hashes_untrusted_hosts_and_wrong_packages(self):
for kind in ("portable", "msi"):
for field, value in (("sha256", ""), ("url", "https://example.org/perl.zip"), ("url", runtimes.RELEASE_ROOT + "SP/strawberry-perl-5.40.5.1-64bit.msi")):
with self.subTest(kind=kind, field=field, value=value):
invalid = copy.deepcopy(release("5.42.3.1"))
invalid["edition"][kind][field] = value
with self.assertRaises(ValueError):
runtimes.select_perl([invalid])
def test_generated_dll_and_installer_follow_selected_release(self):
with tempfile.TemporaryDirectory() as directory:
repo = Path(directory)
(repo / "win32/installer").mkdir(parents=True)
selected = release("5.44.1.1")
perl_root = Path("C:/gtk-build/Perl & tools/perl")
runtimes.write_metadata(repo, perl_root, selected, "3.14.8")
props = ET.parse(repo / "win32/runtimes.generated.props")
ns = {"m": runtimes.MSBUILD_NS}
self.assertEqual(props.find("m:PropertyGroup/m:PerlLib", ns).text, "perl544")
self.assertEqual(props.find("m:PropertyGroup/m:PerlPath", ns).text, str(perl_root))
installer = (repo / "win32/installer/runtimes.generated.iss").read_text()
self.assertIn('#define PerlRuntimeDLL "perl544.dll"', installer)
self.assertIn(selected["edition"]["msi"]["url"], installer)
self.assertIn("/3.14.8/python-3.14.8-amd64.exe", installer)
if __name__ == "__main__":
unittest.main()
+17 -5
View File
@@ -4,6 +4,18 @@
;#define APPARCH "x64"
;#define PROJECTDIR "C:\...\zoitechat\win32\installer\"
; CI generates these from the runtimes actually used to build the plugins.
#if FileExists(PROJECTDIR + "runtimes.generated.iss")
#include PROJECTDIR + "runtimes.generated.iss"
#else
; Manual builds without the preparation script retain the original Perl bundle.
#define PythonRuntimeVersion "3.14.8"
#define PythonRuntimeURL "https://www.python.org/ftp/python/3.14.8/python-3.14.8-amd64.exe"
#define PerlRuntimeVersion "5.42.0.1"
#define PerlRuntimeURL "https://github.com/StrawberryPerl/Perl-Dist-Strawberry/releases/download/SP_54201_64bit/strawberry-perl-5.42.0.1-64bit.msi"
#define PerlRuntimeDLL "perl542.dll"
#endif
[Setup]
AppName=ZoiteChat
AppVersion={#APPVER}
@@ -58,8 +70,8 @@ Name: "plugins\sysinfo"; Description: "SysInfo"; Types: custom; Flags: disableno
Name: "plugins\upd"; Description: "Update Checker"; Types: normal custom; Flags: disablenouninstallwarning
Name: "langs"; Description: "Language Interfaces"; Types: custom; Flags: disablenouninstallwarning
Name: "langs\lua"; Description: "Lua (LuaJIT 2.1)"; Types: normal custom; Flags: disablenouninstallwarning
Name: "langs\perl"; Description: "Perl (Strawberry Perl 5.42.0.1)"; Types: custom; Flags: disablenouninstallwarning
Name: "langs\python"; Description: "Python (Python 3.14.3, or 3.8.10 before Windows 10)"; Types: custom; Flags: disablenouninstallwarning
Name: "langs\perl"; Description: "Perl (Strawberry Perl {#PerlRuntimeVersion})"; Types: custom; Flags: disablenouninstallwarning
Name: "langs\python"; Description: "Python (Python {#PythonRuntimeVersion}, or 3.8.10 before Windows 10)"; Types: custom; Flags: disablenouninstallwarning
Name: "themes"; Description: "GTK3 Themes"; Types: normal minimal custom; Flags: disablenouninstallwarning
Name: "themes\windows10"; Description: "Windows 10"; Types: normal minimal custom; Flags: disablenouninstallwarning
Name: "themes\windows10dark"; Description: "Windows 10 Dark"; Types: normal minimal custom; Flags: disablenouninstallwarning
@@ -349,7 +361,7 @@ begin
if NeedsLegacyPython() then
Result := 'https://www.python.org/ftp/python/3.8.10/python-3.8.10-amd64.exe'
else
Result := 'https://www.python.org/ftp/python/3.14.3/python-3.14.3-amd64.exe';
Result := '{#PythonRuntimeURL}';
end;
function PythonDLL(): String;
@@ -387,7 +399,7 @@ begin
begin
REDIST := 'https://aka.ms/vs/17/release/vc_redist.x64.exe';
PERL := 'https://github.com/StrawberryPerl/Perl-Dist-Strawberry/releases/download/SP_54201_64bit/strawberry-perl-5.42.0.1-64bit.msi';
PERL := '{#PerlRuntimeURL}';
PY3 := PythonURL();
SPELL := 'https://github.com/hexchat/gvsbuild/releases/download/hexchat-2.16.2/HexChat.Spelling.Dictionaries.r2.exe';
@@ -399,7 +411,7 @@ begin
if not WizardSilent() then
begin
if WizardIsComponentSelected('langs\perl') and not CheckDLL('perl542.dll') then
if WizardIsComponentSelected('langs\perl') and not CheckDLL('{#PerlRuntimeDLL}') then
begin
idpAddFile(PERL, ExpandConstant('{tmp}\perl.msi'))
end;
+3 -2
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildThisFileDirectory)runtimes.generated.props" Condition="Exists('$(MSBuildThisFileDirectory)runtimes.generated.props')" />
<PropertyGroup Label="UserMacros">
<!-- SPECIFY YOUR DEPENDENCY DIRECTORIES HERE -->
@@ -24,8 +25,8 @@
<DepsRoot>$(YourDepsPath)\$(ZoiteChatPlatform)\release</DepsRoot>
<GendefPath>$(YourGendefPath)</GendefPath>
<WinSparklePath>$(YourWinSparklePath)\$(ZoiteChatPlatform)</WinSparklePath>
<PerlPath>$(YourPerlPath)\$(ZoiteChatPlatform)</PerlPath>
<PerlLib>perl542</PerlLib>
<PerlPath Condition="'$(PerlPath)'==''">$(YourPerlPath)\$(ZoiteChatPlatform)</PerlPath>
<PerlLib Condition="'$(PerlLib)'==''">perl542</PerlLib>
<Python3Path>$(YourPython3Path)\$(ZoiteChatPlatform)</Python3Path>
<Python3Lib>python314</Python3Lib>