From 88bd1da5c42376652939516eada03fe1675c64fe Mon Sep 17 00:00:00 2001 From: deepend Date: Mon, 19 Oct 2026 21:44:29 -0600 Subject: [PATCH] track Python 3.14 updates and latest stable Perl --- .github/workflows/windows-build.yml | 21 +++- .github/workflows/windows-msi-build.yml | 21 +++- .gitignore | 4 + docs/building.rst | 31 +++++ tools/prepare-windows-runtimes.py | 149 ++++++++++++++++++++++++ tools/tests/test_windows_runtimes.py | 81 +++++++++++++ win32/installer/zoitechat.iss.tt | 22 +++- win32/zoitechat.props | 5 +- 8 files changed, 317 insertions(+), 17 deletions(-) create mode 100644 tools/prepare-windows-runtimes.py create mode 100644 tools/tests/test_windows_runtimes.py diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index f72bd69b..ba95b6ba 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -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 diff --git a/.github/workflows/windows-msi-build.yml b/.github/workflows/windows-msi-build.yml index bdea47fa..02e5f000 100644 --- a/.github/workflows/windows-msi-build.yml +++ b/.github/workflows/windows-msi-build.yml @@ -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 diff --git a/.gitignore b/.gitignore index cba77ef4..2b804073 100644 --- a/.gitignore +++ b/.gitignore @@ -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/* diff --git a/docs/building.rst b/docs/building.rst index 93bc3f12..c16bd768 100644 --- a/docs/building.rst +++ b/docs/building.rst @@ -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``: diff --git a/tools/prepare-windows-runtimes.py b/tools/prepare-windows-runtimes.py new file mode 100644 index 00000000..8012926c --- /dev/null +++ b/tools/prepare-windows-runtimes.py @@ -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() diff --git a/tools/tests/test_windows_runtimes.py b/tools/tests/test_windows_runtimes.py new file mode 100644 index 00000000..8eba708d --- /dev/null +++ b/tools/tests/test_windows_runtimes.py @@ -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() diff --git a/win32/installer/zoitechat.iss.tt b/win32/installer/zoitechat.iss.tt index 09339a6d..a5c1543c 100644 --- a/win32/installer/zoitechat.iss.tt +++ b/win32/installer/zoitechat.iss.tt @@ -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; diff --git a/win32/zoitechat.props b/win32/zoitechat.props index f393022d..adc851f6 100644 --- a/win32/zoitechat.props +++ b/win32/zoitechat.props @@ -1,5 +1,6 @@ + @@ -24,8 +25,8 @@ $(YourDepsPath)\$(ZoiteChatPlatform)\release $(YourGendefPath) $(YourWinSparklePath)\$(ZoiteChatPlatform) - $(YourPerlPath)\$(ZoiteChatPlatform) - perl542 + $(YourPerlPath)\$(ZoiteChatPlatform) + perl542 $(YourPython3Path)\$(ZoiteChatPlatform) python314