From 5c9ac0ead6c62c2fbcba513487f1182fd8ad2b69 Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Fri, 22 May 2026 15:31:25 -0400 Subject: [PATCH 1/8] Replace execution strings with invocation lists --- gvsbuild/projects/cyrus_sasl.py | 39 +++--- gvsbuild/projects/dev_shell.py | 7 +- gvsbuild/projects/enchant.py | 22 ++-- gvsbuild/projects/ffmpeg.py | 22 ++-- gvsbuild/projects/gdk_pixbuf.py | 5 +- gvsbuild/projects/gettext.py | 18 ++- gvsbuild/projects/gstreamer.py | 2 +- gvsbuild/projects/gtk.py | 6 +- gvsbuild/projects/icu.py | 2 +- gvsbuild/projects/libfido2.py | 9 +- gvsbuild/projects/librsvg.py | 7 +- gvsbuild/projects/libvpx.py | 17 ++- gvsbuild/projects/luajit.py | 7 +- gvsbuild/projects/mit_kerberos.py | 19 ++- gvsbuild/projects/nuspell.py | 4 +- gvsbuild/projects/openssl.py | 64 +++++++--- gvsbuild/projects/pkgconf.py | 8 +- gvsbuild/projects/pycairo.py | 5 +- gvsbuild/projects/pygobject.py | 7 +- gvsbuild/projects/sqlite.py | 11 +- gvsbuild/projects/x264.py | 12 +- gvsbuild/projects/zlib.py | 17 +-- gvsbuild/tools.py | 8 +- gvsbuild/utils/base_builders.py | 99 +++++++++------ gvsbuild/utils/base_expanders.py | 26 ++-- gvsbuild/utils/base_project.py | 21 ++-- gvsbuild/utils/builder.py | 99 +++++---------- tests/utils/test_execute.py | 202 ++++++++++++++++++++++++++++++ 28 files changed, 531 insertions(+), 234 deletions(-) create mode 100644 tests/utils/test_execute.py diff --git a/gvsbuild/projects/cyrus_sasl.py b/gvsbuild/projects/cyrus_sasl.py index 0d6af20a6..cf51189bc 100644 --- a/gvsbuild/projects/cyrus_sasl.py +++ b/gvsbuild/projects/cyrus_sasl.py @@ -13,6 +13,8 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see . +from pathlib import Path + from gvsbuild.utils.base_expanders import Tarball from gvsbuild.utils.base_project import Project, project_add @@ -41,22 +43,27 @@ def build(self): "Debug" if self.builder.opts.configuration == "debug" else "Release" ) gssapilib = "gssapi32.lib" if self.builder.x86 else "gssapi64.lib" - self.exec_vs( - r'nmake /nologo /f NTMakefile SASLDB="LMDB" LMDB_INCLUDE="%(gtk_dir)s\include" LMDB_LIBPATH="%(gtk_dir)s\lib" ' - + r'GSSAPI="MITKerberos" GSSAPILIB="' - + gssapilib - + r'" GSSAPI_INCLUDE="%(gtk_dir)s\include" GSSAPI_LIBPATH="%(gtk_dir)s\lib" ' - + r'OPENSSL_INCLUDE="%(gtk_dir)s\include" OPENSSL_LIBPATH="%(gtk_dir)s\lib" prefix="%(pkg_dir)s" CFG=' - + configuration - ) - self.exec_vs( - r'nmake /nologo /f NTMakefile install SASLDB="LMDB" LMDB_INCLUDE="%(gtk_dir)s\include" ' - + r'GSSAPI="MITKerberos" GSSAPILIB="' - + gssapilib - + r'" GSSAPI_INCLUDE="%(gtk_dir)s\include" GSSAPI_LIBPATH="%(gtk_dir)s\lib" ' - + r'LMDB_LIBPATH="%(gtk_dir)s\lib" OPENSSL_INCLUDE="%(gtk_dir)s\include" OPENSSL_LIBPATH="%(gtk_dir)s\lib" prefix="%(pkg_dir)s" CFG=' - + configuration - ) + gtk = Path(self.builder.gtk_dir) + inc = gtk / "include" + lib = gtk / "lib" + common_params = [ + "/nologo", + "/f", + "NTMakefile", + "SASLDB=LMDB", + f"LMDB_INCLUDE={inc}", + f"LMDB_LIBPATH={lib}", + "GSSAPI=MITKerberos", + f"GSSAPILIB={gssapilib}", + f"GSSAPI_INCLUDE={inc}", + f"GSSAPI_LIBPATH={lib}", + f"OPENSSL_INCLUDE={inc}", + f"OPENSSL_LIBPATH={lib}", + f"prefix={self.pkg_dir}", + f"CFG={configuration}", + ] + self.exec_vs(["nmake"] + common_params) + self.exec_vs(["nmake", "install"] + common_params) self.install(r".\COPYING share\doc\cyrus-sasl") self.install_pc_files() diff --git a/gvsbuild/projects/dev_shell.py b/gvsbuild/projects/dev_shell.py index d2e255070..52e9c9ecf 100644 --- a/gvsbuild/projects/dev_shell.py +++ b/gvsbuild/projects/dev_shell.py @@ -13,6 +13,8 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see . +import os + from gvsbuild.utils.base_project import Project, ProjectType, project_add from gvsbuild.utils.simple_ui import log @@ -62,7 +64,8 @@ def build(self): if self.meson: # Add a _meson env to use it directly meson_path = Project.get_tool_path("meson") - self.builder.mod_env("_MESON", f"python {meson_path}\\meson.py") + meson_py = os.path.join(meson_path, "meson.py") + self.builder.mod_env("_MESON", f'python "{meson_py}"') print("If you need to use meson you can use the _MESON environment, e.g.") print("%_MESON% configure") print("") @@ -70,4 +73,4 @@ def build(self): # If you need to use it as a --prefix in some build test ... self.builder.mod_env("GTK_BASE_DIR", self.builder.gtk_dir) self.builder.mod_env("PROMPT", "[ gvsbuild shell ] $P $G", subst=True) - self.builder.exec_vs("cmd", working_dir=self.builder.working_dir) + self.builder.exec_vs(["cmd"], working_dir=self.builder.working_dir) diff --git a/gvsbuild/projects/enchant.py b/gvsbuild/projects/enchant.py index 9b6464a5e..7f5176a4b 100644 --- a/gvsbuild/projects/enchant.py +++ b/gvsbuild/projects/enchant.py @@ -13,6 +13,8 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see . +from pathlib import Path + from gvsbuild.utils.base_expanders import Tarball from gvsbuild.utils.base_project import Project, project_add @@ -31,15 +33,19 @@ def __init__(self): ) def build(self): - x64_param = "X64=1" if self.builder.x64 else "" self.push_location(r".\src") - - # Exec nmake /nologo -f makefile.mak clean - self.exec_vs( - r"nmake /nologo -f makefile.mak DLL=1 " - + x64_param - + r" MFLAGS=-MD GLIBDIR=%(gtk_dir)s\include\glib-2.0" - ) + cmd = [ + "nmake", + "/nologo", + "-f", + "makefile.mak", + "DLL=1", + "MFLAGS=-MD", + f"GLIBDIR={Path(self.builder.gtk_dir) / 'include' / 'glib-2.0'}", + ] + if self.builder.x64: + cmd.append("X64=1") + self.exec_vs(cmd) self.pop_location() diff --git a/gvsbuild/projects/ffmpeg.py b/gvsbuild/projects/ffmpeg.py index 039506c6e..5a03843ec 100644 --- a/gvsbuild/projects/ffmpeg.py +++ b/gvsbuild/projects/ffmpeg.py @@ -13,7 +13,7 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see . -import os +from pathlib import Path from gvsbuild.utils.base_expanders import Tarball from gvsbuild.utils.base_project import Project, project_add @@ -49,14 +49,17 @@ def build(self): else self.opts.configuration ) msys_path = Project.get_tool_path("msys2") + bash = str(Path(msys_path) / "bash") + gpl_flag = "enable_gpl" if self.opts.ffmpeg_enable_gpl else "disable_gpl" self.exec_vs( - r"{}\bash build\build.sh {} {} {} {}".format( - msys_path, + [ + bash, + r"build\build.sh", convert_to_msys(self.pkg_dir), convert_to_msys(self.builder.gtk_dir), configuration, - "enable_gpl" if self.opts.ffmpeg_enable_gpl else "disable_gpl", - ), + gpl_flag, + ], add_path=msys_path, ) @@ -82,7 +85,7 @@ def post_install(self): ]: self.builder.exec_msys( ["mv", lib, "../lib/"], - working_dir=os.path.join(self.builder.gtk_dir, "bin"), + working_dir=Path(self.builder.gtk_dir) / "bin", ) @@ -99,6 +102,9 @@ def __init__(self): ) def build(self): - add_path = os.path.join(self.builder.opts.msys_dir, "usr", "bin") + add_path = Path(self.builder.opts.msys_dir) / "usr" / "bin" - self.exec_vs(r'make install PREFIX="%(gtk_dir)s"', add_path=add_path) + self.exec_vs( + ["make", "install", f"PREFIX={self.builder.gtk_dir}"], + add_path=add_path, + ) diff --git a/gvsbuild/projects/gdk_pixbuf.py b/gvsbuild/projects/gdk_pixbuf.py index 4f09e4c32..b410162ce 100644 --- a/gvsbuild/projects/gdk_pixbuf.py +++ b/gvsbuild/projects/gdk_pixbuf.py @@ -13,6 +13,8 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see . +from pathlib import Path + from gvsbuild.utils.base_builders import Meson from gvsbuild.utils.base_expanders import Tarball from gvsbuild.utils.base_project import Project, project_add @@ -53,4 +55,5 @@ def build(self): self.install(r".\COPYING share\doc\gdk-pixbuf") def post_install(self): - self.exec_cmd(r"%(gtk_dir)s\bin\gdk-pixbuf-query-loaders.exe --update-cache") + exe = Path(self.builder.gtk_dir) / "bin" / "gdk-pixbuf-query-loaders.exe" + self.exec_cmd([exe, "--update-cache"]) diff --git a/gvsbuild/projects/gettext.py b/gvsbuild/projects/gettext.py index c8fdf9f6f..fb88b3fd3 100644 --- a/gvsbuild/projects/gettext.py +++ b/gvsbuild/projects/gettext.py @@ -13,7 +13,8 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see . -import os +import sys +from pathlib import Path from gvsbuild.utils.base_expanders import Tarball from gvsbuild.utils.base_project import Project, project_add @@ -39,10 +40,19 @@ def __init__(self): ) def build(self): + python_exe = Path(sys.executable).parent / "python.exe" self.push_location(r".\nmake") self.exec_vs( - r'nmake /nologo /f Makefile.vc CFG=%(configuration)s PYTHON="%(python_dir)s\python.exe" PREFIX="%(gtk_dir)s"', - add_path=os.path.join(self.builder.opts.msys_dir, "usr", "bin"), + [ + "nmake", + "/nologo", + "/f", + "Makefile.vc", + f"CFG={self.builder.opts.configuration}", + f"PYTHON={python_exe}", + f"PREFIX={self.builder.gtk_dir}", + ], + add_path=Path(self.builder.opts.msys_dir) / "usr" / "bin", ) self.pop_location() @@ -92,5 +102,5 @@ def build(self): def post_install(self): self.builder.exec_msys( ["mv", "libgnuintl.h", "libintl.h"], - working_dir=os.path.join(self.builder.gtk_dir, "include"), + working_dir=Path(self.builder.gtk_dir) / "include", ) diff --git a/gvsbuild/projects/gstreamer.py b/gvsbuild/projects/gstreamer.py index b9a2424ba..ed271fed6 100644 --- a/gvsbuild/projects/gstreamer.py +++ b/gvsbuild/projects/gstreamer.py @@ -319,6 +319,6 @@ def __init__(self): self.add_param("--auto-features=disabled") def build(self): - self.builder.exec_cargo("install cargo-c --locked") + self.builder.exec_cargo(["install", "cargo-c", "--locked"]) Meson.build(self) self.install(r".\LICENSE-MPL-2.0 share\doc\gst-plugin-gtk4") diff --git a/gvsbuild/projects/gtk.py b/gvsbuild/projects/gtk.py index 3b00c0613..21ba13afc 100644 --- a/gvsbuild/projects/gtk.py +++ b/gvsbuild/projects/gtk.py @@ -35,8 +35,10 @@ def make_all_mo(self): f = os.path.basename(fp) lcmsgdir = os.path.join(localedir, f[:-3], "LC_MESSAGES") self.builder.make_dir(lcmsgdir) - cmd = " ".join(["msgfmt", "-co", os.path.join(lcmsgdir, mo), f]) - self.builder.exec_cmd(cmd, working_dir=self._get_working_dir()) + self.builder.exec_cmd( + ["msgfmt", "-co", os.path.join(lcmsgdir, mo), f], + working_dir=self._get_working_dir(), + ) self.pop_location() self.install(rf".\COPYING share\doc\{self.name}") diff --git a/gvsbuild/projects/icu.py b/gvsbuild/projects/icu.py index 696e892d3..8ea897cba 100644 --- a/gvsbuild/projects/icu.py +++ b/gvsbuild/projects/icu.py @@ -48,7 +48,7 @@ def build(self): replace, ) - self.exec_msbuild(r"source\allinone\allinone.sln /p:SkipUWP=true") + self.exec_msbuild([r"source\allinone\allinone.sln", "/p:SkipUWP=true"]) if self.builder.opts.configuration == "debug": self.install_pc_files("pc-files-debug") diff --git a/gvsbuild/projects/libfido2.py b/gvsbuild/projects/libfido2.py index 95cc75bd5..9591497cf 100644 --- a/gvsbuild/projects/libfido2.py +++ b/gvsbuild/projects/libfido2.py @@ -53,7 +53,14 @@ def build(self): bin_dirs = lib_dirs = os.path.join(self.builder.gtk_dir, "bin") build_params = "-DBUILD_EXAMPLES=OFF -DBUILD_MANPAGES=OFF -DBUILD_TESTS=OFF -DBUILD_TOOLS=OFF -DBUILD_STATIC_LIBS=OFF" - cmake_params = f"-DWITH_ZLIB=ON -DCBOR_INCLUDE_DIRS={include_dirs} -DCRYPTO_INCLUDE_DIRS={include_dirs} -DZLIB_INCLUDE_DIRS={include_dirs} -DCBOR_LIBRARY_DIRS={lib_dirs} -DCRYPTO_LIBRARY_DIRS={lib_dirs} -DZLIB_LIBRARY_DIRS={lib_dirs} -DCBOR_BIN_DIRS={bin_dirs} -DCRYPTO_BIN_DIRS={bin_dirs} -DZLIB_BIN_DIRS={bin_dirs} -DCRYPTO_LIBRARIES=libcrypto {build_params}" + cmake_params = ( + f'-DWITH_ZLIB=ON -DCBOR_INCLUDE_DIRS="{include_dirs}" ' + f'-DCRYPTO_INCLUDE_DIRS="{include_dirs}" -DZLIB_INCLUDE_DIRS="{include_dirs}" ' + f'-DCBOR_LIBRARY_DIRS="{lib_dirs}" -DCRYPTO_LIBRARY_DIRS="{lib_dirs}" ' + f'-DZLIB_LIBRARY_DIRS="{lib_dirs}" -DCBOR_BIN_DIRS="{bin_dirs}" ' + f'-DCRYPTO_BIN_DIRS="{bin_dirs}" -DZLIB_BIN_DIRS="{bin_dirs}" ' + f"-DCRYPTO_LIBRARIES=libcrypto {build_params}" + ) CmakeProject.build(self, cmake_params=cmake_params, use_ninja=True) self.install(rf"output\{arch}\static\* .") diff --git a/gvsbuild/projects/librsvg.py b/gvsbuild/projects/librsvg.py index a73a61f15..1b89c8161 100644 --- a/gvsbuild/projects/librsvg.py +++ b/gvsbuild/projects/librsvg.py @@ -13,6 +13,8 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see . +from pathlib import Path + from gvsbuild.utils.base_builders import Meson from gvsbuild.utils.base_expanders import Tarball from gvsbuild.utils.base_project import Project, project_add @@ -52,9 +54,10 @@ def __init__(self): self.add_param("-Dpixbuf-loader=enabled") def build(self): - self.builder.exec_cargo("install cargo-c --locked") + self.builder.exec_cargo(["install", "cargo-c", "--locked"]) Meson.build(self) self.install(r".\COPYING.LIB share\doc\librsvg") def post_install(self): - self.exec_cmd(r"%(gtk_dir)s\bin\gdk-pixbuf-query-loaders.exe --update-cache") + exe = Path(self.builder.gtk_dir) / "bin" / "gdk-pixbuf-query-loaders.exe" + self.exec_cmd([exe, "--update-cache"]) diff --git a/gvsbuild/projects/libvpx.py b/gvsbuild/projects/libvpx.py index 2a6a27042..54b557ecc 100644 --- a/gvsbuild/projects/libvpx.py +++ b/gvsbuild/projects/libvpx.py @@ -13,7 +13,7 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see . -import os +from pathlib import Path from gvsbuild.utils.base_expanders import Tarball from gvsbuild.utils.base_project import Project, project_add @@ -53,12 +53,19 @@ def build(self): msys_path = Project.get_tool_path("msys2") + bash = str(Path(msys_path) / "bash") self.exec_vs( - rf"{msys_path}\bash ./configure --target={target} --prefix={convert_to_msys(self.builder.gtk_dir)} {configure_options}", + [ + bash, + "./configure", + f"--target={target}", + f"--prefix={convert_to_msys(self.builder.gtk_dir)}", + ] + + configure_options.split(), add_path=msys_path, ) - self.exec_vs(r"make", add_path=msys_path) - self.exec_vs(r"make install", add_path=msys_path) + self.exec_vs(["make"], add_path=msys_path) + self.exec_vs(["make", "install"], add_path=msys_path) self.install(r".\LICENSE share\doc\libvpx") @@ -72,5 +79,5 @@ def post_install(self): lib_path = f"Win32/{lib_name}" if self.builder.x86 else f"x64/{lib_name}" self.builder.exec_msys( ["mv", lib_path, "./vpx.lib"], - working_dir=os.path.join(self.builder.gtk_dir, "lib"), + working_dir=Path(self.builder.gtk_dir) / "lib", ) diff --git a/gvsbuild/projects/luajit.py b/gvsbuild/projects/luajit.py index bbb278765..3bcab698a 100644 --- a/gvsbuild/projects/luajit.py +++ b/gvsbuild/projects/luajit.py @@ -31,10 +31,11 @@ def __init__(self): ) def build(self): - option = "debug" if self.builder.opts.configuration == "debug" else "" self.push_location("src") - - self.exec_vs(r".\msvcbuild " + option) + cmd = [r".\msvcbuild"] + if self.builder.opts.configuration == "debug": + cmd.append("debug") + self.exec_vs(cmd) self.install( r".\lua.h .\lualib.h .\luaconf.h .\lauxlib.h .\luajit.h include\luajit-2.1" diff --git a/gvsbuild/projects/mit_kerberos.py b/gvsbuild/projects/mit_kerberos.py index b427a2949..96ee9c649 100644 --- a/gvsbuild/projects/mit_kerberos.py +++ b/gvsbuild/projects/mit_kerberos.py @@ -13,7 +13,7 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see . -import os +from pathlib import Path from gvsbuild.utils.base_expanders import Tarball from gvsbuild.utils.base_project import Project, project_add @@ -35,26 +35,21 @@ def __init__(self): ) def build(self): - configuration = ( - "Debug" if self.builder.opts.configuration == "debug" else "Release" - ) - add_path = os.path.join(self.builder.opts.msys_dir, "usr", "bin") + nodebug = "1" if self.builder.opts.configuration != "debug" else "0" + add_path = Path(self.builder.opts.msys_dir) / "usr" / "bin" + krb_install = f"KRB_INSTALL_DIR={self.builder.gtk_dir}" self.push_location("src") self.exec_vs( - r"nmake -f Makefile.in prep-windows NO_LEASH=1 KRB_INSTALL_DIR=%(gtk_dir)s ", + ["nmake", "-f", "Makefile.in", "prep-windows", "NO_LEASH=1", krb_install], add_path=add_path, ) self.exec_vs( - r"nmake NODEBUG=" - + str(1 if configuration == "Release" else 0) - + " NO_LEASH=1 KRB_INSTALL_DIR=%(gtk_dir)s ", + ["nmake", f"NODEBUG={nodebug}", "NO_LEASH=1", krb_install], add_path=add_path, ) self.exec_vs( - r"nmake install NODEBUG=" - + str(1 if configuration == "Release" else 0) - + " NO_LEASH=1 KRB_INSTALL_DIR=%(gtk_dir)s ", + ["nmake", "install", f"NODEBUG={nodebug}", "NO_LEASH=1", krb_install], add_path=add_path, ) self.pop_location() diff --git a/gvsbuild/projects/nuspell.py b/gvsbuild/projects/nuspell.py index 684b341c7..d70d8759c 100644 --- a/gvsbuild/projects/nuspell.py +++ b/gvsbuild/projects/nuspell.py @@ -34,9 +34,9 @@ def __init__(self): def build(self): cmake_params = ( - f"-DCMAKE_INSTALL_PREFIX={self.builder.gtk_dir} " + f'-DCMAKE_INSTALL_PREFIX="{self.builder.gtk_dir}" ' "-DBUILD_SHARED_LIBS=ON -DBUILD_TESTING=OFF -DBUILD_DOCS=OFF " - f"-DBUILD_TOOLS=OFF -DICU_ROOT={self.builder.gtk_dir}" + f'-DBUILD_TOOLS=OFF -DICU_ROOT="{self.builder.gtk_dir}"' ) CmakeProject.build(self, use_ninja=True, cmake_params=cmake_params) diff --git a/gvsbuild/projects/openssl.py b/gvsbuild/projects/openssl.py index d88058d5f..5fcfff22c 100644 --- a/gvsbuild/projects/openssl.py +++ b/gvsbuild/projects/openssl.py @@ -14,6 +14,7 @@ # along with this program; if not, see . import contextlib +from pathlib import Path from gvsbuild.utils.base_expanders import Tarball from gvsbuild.utils.base_project import Project, project_add @@ -37,22 +38,35 @@ def __init__(self): ) def build(self): - common_options = r"enable-fips no-comp no-docs no-ssl3 --openssldir=%(gtk_dir)s/etc/ssl --prefix=%(gtk_dir)s" + perl_exe = ( + Path(Project.get_tool_base_dir(Project.get_project("perl"))) + / "bin" + / "perl.exe" + ) + gtk_dir = Path(self.builder.gtk_dir) debug_option = "debug-" if self.builder.opts.configuration == "debug" else "" - target_option = "VC-WIN32 " if self.builder.x86 else "VC-WIN64A " + target = "VC-WIN32" if self.builder.x86 else "VC-WIN64A" + configure_target = f"{debug_option}{target}" self.exec_vs( - r"%(perl_dir)s\bin\perl.exe Configure " - + debug_option - + target_option - + common_options + [ + perl_exe, + "Configure", + configure_target, + "enable-fips", + "no-comp", + "no-docs", + "no-ssl3", + f"--openssldir={gtk_dir / 'etc' / 'ssl'}", + f"--prefix={gtk_dir}", + ] ) with contextlib.suppress(Exception): - self.exec_vs(r"nmake /nologo clean") - self.exec_vs(r"nmake /nologo") - self.exec_vs(r"%(perl_dir)s\bin\perl.exe mk-ca-bundle.pl -n cert.pem") - self.exec_vs(r"nmake /nologo install") + self.exec_vs(["nmake", "/nologo", "clean"]) + self.exec_vs(["nmake", "/nologo"]) + self.exec_vs([perl_exe, "mk-ca-bundle.pl", "-n", "cert.pem"]) + self.exec_vs(["nmake", "/nologo", "install"]) self.install(r".\cert.pem bin") self.install(r".\LICENSE share\doc\openssl") @@ -75,18 +89,30 @@ def __init__(self): ) def build(self): - common_options = "enable-fips no-ssl3 no-comp --openssldir=%(gtk_dir)s/etc/ssl --prefix=%(gtk_dir)s" + perl_exe = ( + Path(Project.get_tool_base_dir(Project.get_project("perl"))) + / "bin" + / "perl.exe" + ) + gtk_dir = Path(self.builder.gtk_dir) debug_option = "debug-" if self.builder.opts.configuration == "debug" else "" - target_option = "VC-WIN32 " if self.builder.x86 else "VC-WIN64A " + target = "VC-WIN32" if self.builder.x86 else "VC-WIN64A" + configure_target = f"{debug_option}{target}" self.exec_vs( - r"%(perl_dir)s\bin\perl.exe Configure " - + debug_option - + target_option - + common_options + [ + perl_exe, + "Configure", + configure_target, + "enable-fips", + "no-ssl3", + "no-comp", + f"--openssldir={gtk_dir / 'etc' / 'ssl'}", + f"--prefix={gtk_dir}", + ] ) with contextlib.suppress(Exception): - self.exec_vs(r"nmake /nologo clean") - self.exec_vs(r"nmake /nologo") - self.exec_vs(r"nmake /nologo install_fips") + self.exec_vs(["nmake", "/nologo", "clean"]) + self.exec_vs(["nmake", "/nologo"]) + self.exec_vs(["nmake", "/nologo", "install_fips"]) diff --git a/gvsbuild/projects/pkgconf.py b/gvsbuild/projects/pkgconf.py index 2abe3184c..80ee02308 100644 --- a/gvsbuild/projects/pkgconf.py +++ b/gvsbuild/projects/pkgconf.py @@ -13,6 +13,9 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see . +import shutil +from pathlib import Path + from gvsbuild.utils.base_builders import Meson from gvsbuild.utils.base_expanders import Tarball from gvsbuild.utils.base_project import Project, project_add @@ -39,6 +42,5 @@ def build(self): self.install(r".\COPYING share\doc\pkgconf") def post_install(self): - self.exec_cmd( - r"copy %(gtk_dir)s\bin\pkgconf.exe %(gtk_dir)s\bin\pkg-config.exe" - ) + gtk_bin = Path(self.builder.gtk_dir) / "bin" + shutil.copy2(gtk_bin / "pkgconf.exe", gtk_bin / "pkg-config.exe") diff --git a/gvsbuild/projects/pycairo.py b/gvsbuild/projects/pycairo.py index 21c77c8dd..85fbff3d0 100644 --- a/gvsbuild/projects/pycairo.py +++ b/gvsbuild/projects/pycairo.py @@ -38,10 +38,11 @@ def build(self): Meson.build(self, meson_params=f'-Dpython="{py_dir}\\python.exe"') cairo_inc = Path(self.builder.gtk_dir) / "include" / "cairo" self.builder.mod_env("INCLUDE", str(cairo_inc)) - self.exec_vs(r"%(python_dir)s\python.exe -m build -w") + python_exe = str(py_dir / "python.exe") + self.exec_vs([python_exe, "-m", "build", "--wheel"]) dist_dir = Path(self.build_dir) / "dist" for path in dist_dir.rglob("*.whl"): - self.exec_vs(r"%(python_dir)s\python.exe -m pip install " + str(path)) + self.exec_vs([python_exe, "-m", "pip", "install", str(path)]) if self.builder.opts.py_wheel: self.install_dir("dist", "python") self.install(r".\COPYING share\doc\pycairo") diff --git a/gvsbuild/projects/pygobject.py b/gvsbuild/projects/pygobject.py index 246624fc6..cecd5a3ea 100644 --- a/gvsbuild/projects/pygobject.py +++ b/gvsbuild/projects/pygobject.py @@ -49,12 +49,13 @@ def build(self): ] self.builder.mod_env("INCLUDE", ";".join(add_inc)) if self.builder.opts.py_wheel: - self.exec_vs(r"%(python_dir)s\python.exe -m build --wheel") + python_exe = str(py_dir / "python.exe") + self.exec_vs([python_exe, "-m", "build", "--wheel"]) dist_dir = Path(self.build_dir) / "dist" for path in dist_dir.rglob("*.whl"): self.exec_vs( - r"%(python_dir)s\python.exe -m pip install --force-reinstall " - + str(path) + [python_exe, "-m", "pip", "install", "--force-reinstall", str(path)] ) + self.install_dir("dist", "python") self.install(r".\COPYING share\doc\pygobject") diff --git a/gvsbuild/projects/sqlite.py b/gvsbuild/projects/sqlite.py index 915bbc512..2d7911ffc 100644 --- a/gvsbuild/projects/sqlite.py +++ b/gvsbuild/projects/sqlite.py @@ -33,7 +33,16 @@ def build(self): nmake_debug = ( "DEBUG=2" if self.builder.opts.configuration == "debug" else "DEBUG=0" ) - self.exec_vs(f"nmake /f Makefile.msc sqlite3.dll DYNAMIC_SHELL=1 {nmake_debug}") + self.exec_vs( + [ + "nmake", + "/f", + "Makefile.msc", + "sqlite3.dll", + "DYNAMIC_SHELL=1", + nmake_debug, + ] + ) self.install("sqlite3.h include") self.install("sqlite3ext.h include") diff --git a/gvsbuild/projects/x264.py b/gvsbuild/projects/x264.py index 594c95f1f..e2eb9b9e0 100644 --- a/gvsbuild/projects/x264.py +++ b/gvsbuild/projects/x264.py @@ -13,7 +13,7 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see . -import os +from pathlib import Path from gvsbuild.utils.base_expanders import GitRepo from gvsbuild.utils.base_project import Project, project_add @@ -43,15 +43,21 @@ def build(self): else self.opts.configuration ) msys_path = Project.get_tool_path("msys2") + bash = str(Path(msys_path) / "bash") self.exec_vs( - rf"{msys_path}\bash build\build.sh {convert_to_msys(self.builder.gtk_dir)} {configuration}", + [ + bash, + r"build\build.sh", + convert_to_msys(self.builder.gtk_dir), + configuration, + ], add_path=msys_path, ) # use the path expected when building with a dependent project self.builder.exec_msys( ["mv", "libx264.dll.lib", "libx264.lib"], - working_dir=os.path.join(self.builder.gtk_dir, "lib"), + working_dir=Path(self.builder.gtk_dir) / "lib", ) if configuration in ["debug-optimized", "debug"]: diff --git a/gvsbuild/projects/zlib.py b/gvsbuild/projects/zlib.py index eaf99a32f..1a30701e8 100644 --- a/gvsbuild/projects/zlib.py +++ b/gvsbuild/projects/zlib.py @@ -44,14 +44,17 @@ def __init__(self): ) def build(self): - options = "" + cmd = [ + "nmake", + "/nologo", + r"/f", + r"win32\Makefile.msc", + "STATICLIB=zlib-static.lib", + "IMPLIB=zlib1.lib", + ] if self.builder.opts.configuration == "debug": - options = 'CFLAGS="-nologo -MDd -W3 -Od -Zi -Fd\\"zlib\\""' - - self.exec_vs( - r"nmake /nologo /f win32\Makefile.msc STATICLIB=zlib-static.lib IMPLIB=zlib1.lib " - + options - ) + cmd.append(r'CFLAGS=-nologo -MDd -W3 -Od -Zi -Fd"zlib"') + self.exec_vs(cmd) self.install(r".\zlib.h .\zconf.h include") self.install(r".\zlib1.dll .\zlib1.pdb bin") diff --git a/gvsbuild/tools.py b/gvsbuild/tools.py index 4ec439890..f0635db3d 100644 --- a/gvsbuild/tools.py +++ b/gvsbuild/tools.py @@ -52,7 +52,13 @@ def unpack(self): f"{self.version}-{'i686' if self.opts.x86 else 'x86_64'}-pc-windows-msvc" ) subprocess.run( - f"{self.archive_file} --no-modify-path --default-toolchain {toolchain} -y", + [ + self.archive_file, + "--no-modify-path", + "--default-toolchain", + toolchain, + "-y", + ], check=True, env=env, ) diff --git a/gvsbuild/utils/base_builders.py b/gvsbuild/utils/base_builders.py index fb2353528..834724052 100644 --- a/gvsbuild/utils/base_builders.py +++ b/gvsbuild/utils/base_builders.py @@ -15,7 +15,6 @@ """Various builders (meson, CMake, ...) class.""" -import os import shutil import sys from pathlib import Path @@ -40,10 +39,10 @@ def add_param(self, par): def build(self, meson_params=None, make_tests=False, add_path=None): # where we build, with ninja, the library - ninja_build = os.path.join(self.build_dir, "_gvsbuild-meson") + ninja_build = Path(self.build_dir) / "_gvsbuild-meson" # First we check if we need to generate the meson build files - if not os.path.isfile(os.path.join(ninja_build, "build.ninja")): + if not (ninja_build / "build.ninja").is_file(): self._setup_meson_and_ninja(ninja_build, meson_params, add_path) if make_tests: # Run ninja to build all (library, .... @@ -58,25 +57,28 @@ def build(self, meson_params=None, make_tests=False, add_path=None): def _setup_meson_and_ninja(self, ninja_build, meson_params, add_path): log.start_verbose("Generating meson directory") self.builder.make_dir(ninja_build) - # base params self._ensure_params() - add_opts = " ".join(self.params) + " " if self.params else "" - # debug info build_type = self.builder.opts.configuration if self.builder.opts.release_configuration_is_actually_debug_optimized: build_type = "debugoptimized" - add_opts += f"--buildtype {build_type}" + + meson = Project.get_tool_executable("meson") + cmd = [ + sys.executable, + meson, + "setup", + self._get_working_dir(), + ninja_build, + "--prefix", + self.builder.gtk_dir, + "--buildtype", + build_type, + ] + cmd += self.params if meson_params: - add_opts += f" {meson_params}" + cmd += meson_params.split() if self.extra_opts: - extra_opts = " ".join(self.extra_opts) - add_opts += f" {extra_opts}" - # python meson.py src_dir ninja_build_dir --prefix gtk_bin options - meson = Project.get_tool_executable("meson") - python = Path(sys.executable) - if " " in str(python): - python = f'"{python}"' - cmd = f"{python} {meson} setup {self._get_working_dir()} {ninja_build} --prefix {self.builder.gtk_dir} {add_opts}" + cmd += self.extra_opts # build the ninja file to do everything (build the library, create the .pc file, install it, ...) self.exec_vs(cmd, add_path=add_path) @@ -104,25 +106,31 @@ def build( if self.builder.opts.release_configuration_is_actually_debug_optimized: cmake_config = "RelWithDebInfo" # Create the command for cmake - cmd = f'cmake -G "{cmake_gen}" -DCMAKE_INSTALL_PREFIX="%(pkg_dir)s" -DGTK_DIR="%(gtk_dir)s" -DCMAKE_BUILD_TYPE={cmake_config}' + cmd = [ + "cmake", + "-G", + cmake_gen, + f"-DCMAKE_INSTALL_PREFIX={self.pkg_dir}", + f"-DGTK_DIR={self.builder.gtk_dir}", + f"-DCMAKE_BUILD_TYPE={cmake_config}", + ] if cmake_params: - cmd += f" {cmake_params}" + cmd += cmake_params.split() if self.extra_opts: - extra_opts = " ".join(self.extra_opts) - cmd += f" {extra_opts}" + cmd += self.extra_opts if use_ninja and out_of_source is None: # For ninja the default is build out of source out_of_source = True if out_of_source: - cmake_dir = os.path.join(self.build_dir, "_gvsbuild-cmake") - + cmake_dir = Path(self.build_dir) / "_gvsbuild-cmake" self.builder.make_dir(cmake_dir) - if source_part: - src_full = os.path.join(self.build_dir, source_part) - else: - src_full = self.build_dir - cmd += f" -B{cmake_dir} -H{src_full}" + src_full = ( + Path(self.build_dir) / source_part + if source_part + else Path(self.build_dir) + ) + cmd += ["-B", cmake_dir, "-H", src_full] work_dir = cmake_dir else: work_dir = self._get_working_dir() @@ -143,9 +151,11 @@ def build( else: self.builder.exec_ninja(working_dir=work_dir) else: - self.builder.exec_vs("nmake /nologo", working_dir=work_dir) + self.builder.exec_vs(["nmake", "/nologo"], working_dir=work_dir) if do_install: - self.builder.exec_vs("nmake /nologo install", working_dir=work_dir) + self.builder.exec_vs( + ["nmake", "/nologo", "install"], working_dir=work_dir + ) class Rust(Project): @@ -176,17 +186,17 @@ def build(self, cargo_params=None, make_tests=False): if self.extra_opts: params.extend(self.extra_opts) - cargo_build = os.path.join(self.build_dir, "cargo-build") + cargo_build = Path(self.build_dir) / "cargo-build" params.append(f"--target-dir={cargo_build}") - if self.clean and os.path.exists(cargo_build): + if self.clean and cargo_build.exists(): log.debug(f"Removing cargo build dir '{cargo_build}'") shutil.rmtree(cargo_build, onerror=_rmtree_error_handler) # build self.builder.exec_cargo( - params=" ".join(["build"] + params), + params=["build"] + params, working_dir=self.build_dir, rustc_opts=rustc_opts, rust_version=self.version, @@ -195,15 +205,13 @@ def build(self, cargo_params=None, make_tests=False): # test if make_tests: self.builder.exec_cargo( - params=" ".join(["test"] + params), + params=["test"] + params, working_dir=self.build_dir, rustc_opts=rustc_opts, rust_version=self.version, ) - shutil.copytree( - os.path.join(cargo_build, folder), os.path.join(cargo_build, "lib") - ) + shutil.copytree(cargo_build / folder, cargo_build / "lib") class MakeGir: @@ -215,14 +223,23 @@ def make_single_gir(self, prj_name, prj_dir=None): if not prj_dir: prj_dir = prj_name - b_dir = f"{self.builder.working_dir}\\{prj_dir}\\build\\win32" - if not os.path.isfile(os.path.join(b_dir, "detectenv-msvc.mak")): - b_dir = f"{self.builder.working_dir}\\{prj_dir}\\win32" - if not os.path.isfile(os.path.join(b_dir, "detectenv-msvc.mak")): + b_dir = Path(self.builder.working_dir) / prj_dir / "build" / "win32" + if not (b_dir / "detectenv-msvc.mak").is_file(): + b_dir = Path(self.builder.working_dir) / prj_dir / "win32" + if not (b_dir / "detectenv-msvc.mak").is_file(): log.message(f"Unable to find detectenv-msvc.mak for {prj_name}") return - cmd = f"nmake -f {prj_name}-introspection-msvc.mak CFG={self.builder.opts.configuration} PREFIX={self.builder.gtk_dir} PYTHON={Project.get_tool_executable('python')} install-introspection" + python = Project.get_tool_executable("python") + cmd = [ + "nmake", + "-f", + f"{prj_name}-introspection-msvc.mak", + f"CFG={self.builder.opts.configuration}", + f"PREFIX={self.builder.gtk_dir}", + f"PYTHON={python}", + "install-introspection", + ] self.push_location(b_dir) self.exec_vs(cmd) diff --git a/gvsbuild/utils/base_expanders.py b/gvsbuild/utils/base_expanders.py index 417a1fbe3..9154814ea 100644 --- a/gvsbuild/utils/base_expanders.py +++ b/gvsbuild/utils/base_expanders.py @@ -20,6 +20,7 @@ import hashlib import os import shutil +import subprocess import sys import tarfile import zipfile @@ -418,13 +419,14 @@ def get_tag_name(self, src_dir): t_name = [c if c.isalnum() else "_" for c in self.tag] tag_name = "".join(t_name) else: - of = os.path.join(src_dir, ".git-temp.rsp") - self.builder.exec_msys( - f"git rev-parse --short HEAD >{of}", working_dir=src_dir + result = subprocess.run( + ["git", "rev-parse", "--short", "HEAD"], + cwd=src_dir, + capture_output=True, + text=True, + check=True, ) - with open(of, encoding="utf-8") as fi: - tag_name = fi.readline().rstrip("\n") - os.remove(of) + tag_name = result.stdout.strip() return tag_name @@ -508,10 +510,12 @@ def _update_dir(self, remove_dest=False): def _clone_and_checkout(self, dest): log.start(f"(git) Cloning {self.repository} to {dest}") - self.builder.exec_msys(f"git clone {self.repository} {dest}") + self.builder.exec_msys(["git", "clone", self.repository, dest]) if self.tag: - self.builder.exec_msys(f"git checkout -f {self.tag}", working_dir=dest) + self.builder.exec_msys( + ["git", "checkout", "-f", self.tag], working_dir=dest + ) if self.fetch_submodules: self._update_submodules("Fetch submodule(s)", dest) @@ -520,7 +524,9 @@ def _clone_and_checkout(self, dest): def _update_submodules(self, log_value, dest): log.start_verbose(log_value) - self.builder.exec_msys("git submodule update --init", working_dir=dest) + self.builder.exec_msys( + ["git", "submodule", "update", "--init"], working_dir=dest + ) log.end() def update_build_dir(self): @@ -548,7 +554,7 @@ def export(self): src_dir = os.path.join(self.opts.git_expand_dir, self.name) filename = f"{self.name}-{self.get_tag_name(src_dir)}.zip" self.builder.exec_msys( - f"git archive -o {filename} HEAD", working_dir=self.build_dir + ["git", "archive", "-o", filename, "HEAD"], working_dir=self.build_dir ) path = os.path.join(self.export_dir, f"{self.name}.zip") diff --git a/gvsbuild/utils/base_project.py b/gvsbuild/utils/base_project.py index 162e9844d..61101d0af 100644 --- a/gvsbuild/utils/base_project.py +++ b/gvsbuild/utils/base_project.py @@ -20,6 +20,7 @@ import pathlib import re import shutil +import sys from enum import Enum from typing import Generic, TypeVar @@ -195,15 +196,13 @@ def exec_vs(self, cmd, add_path=None): cmd, working_dir=self._get_working_dir(), add_path=add_path ) - def exec_msbuild(self, cmd, configuration=None, add_path=None): + def exec_msbuild(self, cmd: list[str], configuration=None, add_path=None): if not configuration: - configuration = "%(configuration)s" + configuration = self.builder.opts.configuration + python = pathlib.Path(sys.executable).parent + msbuild_opts = self.builder._create_msbuild_opts(python) self.exec_vs( - "msbuild " - + cmd - + " /p:Configuration=" - + configuration - + " %(msbuild_opts)s", + ["msbuild"] + cmd + [f"/p:Configuration={configuration}"] + msbuild_opts, add_path=add_path, ) @@ -389,11 +388,11 @@ def _msbuild_copy(self, org_path, org_platform, use_ver=True): log.log(f"Project {self.name}, using {part} directory") if part: - cmd = os.path.join(base_dir, part, sln_file) - if add_pars: - cmd += f" {add_pars}" + sln_path = os.path.join(base_dir, part, sln_file) + extra_flags = add_pars.split() if add_pars else [] if use_env: - cmd += " /p:UseEnv=True" + extra_flags.append("/p:UseEnv=True") + cmd = [sln_path] + extra_flags else: log.error_exit( f"Solution file '{sln_file}' for project '{self.name}' not found!" diff --git a/gvsbuild/utils/builder.py b/gvsbuild/utils/builder.py index 8db21ae04..1cb13c78a 100644 --- a/gvsbuild/utils/builder.py +++ b/gvsbuild/utils/builder.py @@ -25,7 +25,6 @@ import shutil import ssl import subprocess -import sys import time import traceback from pathlib import Path @@ -132,31 +131,28 @@ def __init__(self, opts): self.__check_tools(opts) self.__check_vs(opts) - def _create_msbuild_opts(self, python): - rt = [f"/nologo /p:Platform={self.opts.platform}"] + def _create_msbuild_opts(self, python) -> list[str]: + rt = ["/nologo", f"/p:Platform={self.opts.platform}"] if python: - rt.append(f'/p:PythonPath="{python}" /p:PythonDir="{python}"') + rt += [f"/p:PythonPath={python}", f"/p:PythonDir={python}"] - if log.verbose_on(): - rt.append("/v:normal") - else: - rt.append("/v:minimal") + rt.append("/v:normal" if log.verbose_on() else "/v:minimal") if self.opts.win_sdk_ver: - rt.append(f'/p:WindowsTargetPlatformVersion="{self.opts.win_sdk_ver}"') + rt.append(f"/p:WindowsTargetPlatformVersion={self.opts.win_sdk_ver}") if self.opts.net_target_framework: - rt.append(f'/p:TargetFrameworks="{self.opts.net_target_framework}"') + rt.append(f"/p:TargetFrameworks={self.opts.net_target_framework}") if self.opts.net_target_framework_version: rt.append( - f'/p:TargetFrameworkVersion="{self.opts.net_target_framework_version}"' + f"/p:TargetFrameworkVersion={self.opts.net_target_framework_version}" ) if self.opts.msbuild_opts: - rt.append(self.opts.msbuild_opts) + rt += self.opts.msbuild_opts.split() - return " ".join(rt) + return rt def __minimum_env(self): r"""Set the environment to the minimum needed to run, leaving only the @@ -261,14 +257,10 @@ def __check_tools(self, opts): missing = self.__msys_missing(msys_path) if missing: # install using pacman - cmd = ( - str(msys_path / "usr" / "bin" / "bash") - + ' -l -c "pacman --noconfirm -S ' - + " ".join(missing) - + '"' - ) + bash = str(msys_path / "usr" / "bin" / "bash") + cmd = [bash, "-l", "-c", f"pacman --noconfirm -S {' '.join(missing)}"] log.debug(f"Updating msys2 with '{cmd}'") - subprocess.check_call(cmd, shell=True) + subprocess.check_call(cmd, shell=False) missing = self.__msys_missing(msys_path) if missing: # oops @@ -913,49 +905,26 @@ def __download_one(self, proj): print(f"{proj.archive_file:{self._old_print}} - Download finished") return self.__check_hash(proj) - def __sub_vars(self, s): - if "%" not in s: - return s - d = { - "platform": self.opts.platform, - "configuration": self.opts.configuration, - "build_dir": self.opts.build_dir, - "vs_ver": self.opts.vs_ver, - "gtk_dir": self.gtk_dir, - "vs_ver_year": self.vs_ver_year, - } - python = None - if self.__project is not None: - d["pkg_dir"] = self.__project.pkg_dir - d["build_dir"] = self.__project.build_dir - python = Path(sys.executable).parent - d["python_dir"] = python - # Add perl only if the project depends on them - - p = Project.get_project("perl") - if p in self.__project.all_dependencies: - perl = Project.get_tool_base_dir(p) - d["perl_dir"] = perl - - d["msbuild_opts"] = self._create_msbuild_opts(python) - return s % d - def exec_vs(self, cmd, working_dir=None, add_path=None): self.__execute( - self.__sub_vars(cmd), + cmd, working_dir=working_dir, add_path=add_path, env=self.vs_env, ) def exec_cargo( - self, params="", working_dir=None, rustc_opts=None, rust_version="stable" + self, + params: list[str] | None = None, + working_dir=None, + rustc_opts=None, + rust_version="stable", ): - cmd = "cargo" + cmd = ["cargo"] if self.opts.cargo_opts: - cmd += f" {self.opts.cargo_opts}" + cmd += self.opts.cargo_opts.split() if params: - cmd += f" {params}" + cmd += params cargo_home = Project.get_tool_path("cargo") @@ -967,30 +936,31 @@ def exec_cargo( # set platform rustup = os.path.join(cargo_home, "rustup.exe") + arch = "i686" if self.x86 else "x86_64" self.__execute( - f"{rustup} default {rust_version}-{'i686' if self.x86 else 'x86_64'}-pc-windows-msvc", + [rustup, "default", f"{rust_version}-{arch}-pc-windows-msvc"], env=env, ) # build self.__execute( - self.__sub_vars(cmd), + cmd, working_dir=working_dir, add_path=cargo_home, env=self.vs_env, ) def exec_cmd(self, cmd, working_dir=None, add_path=None): - self.__execute(self.__sub_vars(cmd), working_dir=working_dir, add_path=add_path) + self.__execute(cmd, working_dir=working_dir, add_path=add_path) def exec_ninja(self, params="", working_dir=None, add_path=None): - cmd = "ninja" + cmd = ["ninja"] if self.opts.ninja_opts: - cmd += f" {self.opts.ninja_opts}" + cmd += self.opts.ninja_opts.split() if params: - cmd += f" {params}" + cmd += params.split() self.__execute( - self.__sub_vars(cmd), + cmd, working_dir=working_dir, add_path=add_path, env=self.vs_env, @@ -999,16 +969,16 @@ def exec_ninja(self, params="", working_dir=None, add_path=None): def install(self, build_dir, pkg_dir, *args): if len(args) == 1: args = args[0].split() - dest = os.path.join(pkg_dir, self.__sub_vars(args[-1])) + dest = os.path.join(pkg_dir, args[-1]) self.make_dir(dest) for f in args[:-1]: - src = os.path.join(self.__sub_vars(build_dir), self.__sub_vars(f)) + src = os.path.join(build_dir, f) log.debug(f"copying {src} to {dest}") self.__copy_to(src, dest) def install_dir(self, build_dir, pkg_dir, src, dest): - src = os.path.join(build_dir, self.__sub_vars(src)) - dest = os.path.join(pkg_dir, self.__sub_vars(dest)) + src = os.path.join(build_dir, src) + dest = os.path.join(pkg_dir, dest) log.debug(f"copying {src} content to {dest}") self.copy_all(src, dest) @@ -1030,7 +1000,6 @@ def __execute(self, args, working_dir=None, add_path=None, env=None): args, cwd=working_dir, env=env, - shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, @@ -1045,7 +1014,7 @@ def __execute(self, args, working_dir=None, add_path=None, env=None): log.messages_dump(res.stdout, prt=self.opts.print_out) else: - subprocess.check_call(args, cwd=working_dir, env=env, shell=True) + subprocess.check_call(args, cwd=working_dir, env=env) def __add_path(self, env, folder): key = next((k for k in env if k.lower() == "path"), None) diff --git a/tests/utils/test_execute.py b/tests/utils/test_execute.py new file mode 100644 index 000000000..b1543b38e --- /dev/null +++ b/tests/utils/test_execute.py @@ -0,0 +1,202 @@ +# Copyright (C) 2026 The Gvsbuild Authors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, see . + +"""Tests for list-arg execution — all subprocess calls use shell=False implicitly.""" + +import pytest + +from gvsbuild.utils.builder import Builder + +# Patch targets — always patch where the name is looked up +SUBPROCESS_CHECK_CALL = "gvsbuild.utils.builder.subprocess.check_call" +SUBPROCESS_RUN = "gvsbuild.utils.builder.subprocess.run" + + +@pytest.fixture +def builder(mocker): + """Minimal Builder instance for testing execution methods.""" + b = Builder.__new__(Builder) + b.opts = mocker.Mock() + b.opts.capture_out = False + b.opts.ninja_opts = None + b.opts.msys_dir = "C:\\msys64" + b.vs_env = {} + b._Builder__project = None + return b + + +def test_execute_list_calls_check_call(builder, mocker): + mock_cc = mocker.patch(SUBPROCESS_CHECK_CALL) + + builder._Builder__execute(["echo", "hello"]) + + mock_cc.assert_called_once_with(["echo", "hello"], cwd=None, env=None) + + +@pytest.mark.parametrize( + "args", + [ + ["nmake", "PREFIX=C:\\Program Files\\gtk"], + # #1726: python exe with spaces in path must not be split + [ + r"C:\Users\Mazin Marwan\AppData\Roaming\uv\tools\gvsbuild\Scripts\python.exe", + "-m", + "build", + "--wheel", + ], + # bash invocation: path\to\bash as first element, script as second + [r"C:\msys64\usr\bin\bash", r"build\build.sh", "/c/some path/gtk", "release"], + ], +) +def test_execute_list_preserves_args_verbatim(builder, mocker, args): + """List args are delivered to subprocess unchanged — no shell splitting.""" + mock_cc = mocker.patch(SUBPROCESS_CHECK_CALL) + builder._Builder__execute(args) + assert mock_cc.call_args[0][0] == args + + +def test_execute_list_capture_out_calls_run(builder, mocker): + builder.opts.capture_out = True + builder._Builder__project = mocker.Mock() + builder._Builder__project.name = "test" + mock_run = mocker.patch(SUBPROCESS_RUN, return_value=mocker.Mock(stdout="")) + + builder._Builder__execute(["ninja", "install"]) + + mock_run.assert_called_once() + assert "shell" not in mock_run.call_args[1] + + +def test_exec_vs_list_passed_to_execute_unchanged(builder, mocker): + mock_exec = mocker.patch.object(builder, "_Builder__execute") + + builder.exec_vs(["nmake", "/nologo", "install"]) + + assert mock_exec.call_args[0][0] == ["nmake", "/nologo", "install"] + + +def test_exec_vs_list_element_with_spaces_not_quoted(builder, mocker): + mock_exec = mocker.patch.object(builder, "_Builder__execute") + + token = "PREFIX=C:\\Program Files\\build" + builder.exec_vs(["nmake", token]) + + assert mock_exec.call_args[0][0][1] == token + + +def test_exec_vs_python_exe_with_spaces_regression_1726(builder, mocker): + """Regression #1726: python.exe in a path with spaces reaches subprocess verbatim.""" + mock_cc = mocker.patch(SUBPROCESS_CHECK_CALL) + python_exe = ( + r"C:\Users\Mazin Marwan\AppData\Roaming\uv\tools\gvsbuild\Scripts\python.exe" + ) + builder.exec_vs([python_exe, "-m", "build", "--wheel"]) + assert mock_cc.call_args[0][0][0] == python_exe + + +def test_exec_ninja_no_params_produces_list(builder, mocker): + mock_exec = mocker.patch.object(builder, "_Builder__execute") + + builder.exec_ninja() + + assert mock_exec.call_args[0][0] == ["ninja"] + + +def test_exec_ninja_with_params_produces_list(builder, mocker): + mock_exec = mocker.patch.object(builder, "_Builder__execute") + + builder.exec_ninja(params="install") + + assert mock_exec.call_args[0][0] == ["ninja", "install"] + + +def test_exec_ninja_with_opts_produces_list(builder, mocker): + builder.opts.ninja_opts = "-j4" + mock_exec = mocker.patch.object(builder, "_Builder__execute") + + builder.exec_ninja(params="install") + + assert mock_exec.call_args[0][0] == ["ninja", "-j4", "install"] + + +def test_exec_cmd_list_passed_unchanged(builder, mocker): + mock_exec = mocker.patch.object(builder, "_Builder__execute") + + builder.exec_cmd(["copy", "src.exe", "dst.exe"]) + + assert mock_exec.call_args[0][0] == ["copy", "src.exe", "dst.exe"] + + +def test_exec_msys_list_calls_check_call(builder, mocker): + mock_cc = mocker.patch(SUBPROCESS_CHECK_CALL) + + builder.exec_msys(["git", "fetch", "origin"]) + + mock_cc.assert_called_once() + assert "shell" not in mock_cc.call_args[1] + + +def test_exec_msys_list_passes_args_verbatim(builder, mocker): + mock_cc = mocker.patch(SUBPROCESS_CHECK_CALL) + + dest = "C:\\Program Files\\source" + builder.exec_msys(["git", "clone", "https://example.com/repo.git", dest]) + + args = mock_cc.call_args[0][0] + assert args == ["git", "clone", "https://example.com/repo.git", dest] + + +def test_exec_vs_does_not_expand_percent_substitution(builder, mocker): + """After __sub_vars removal, %(var)s literals in list elements survive verbatim.""" + builder.gtk_dir = "C:\\gtk" + mock_exec = mocker.patch.object(builder, "_Builder__execute") + + builder.exec_vs(["nmake", "PREFIX=%(gtk_dir)s"]) + + # Must be passed through unchanged, not expanded + assert mock_exec.call_args[0][0][1] == "PREFIX=%(gtk_dir)s" + + +@pytest.fixture +def cargo_builder(builder, mocker): + """Builder with cargo prerequisites pre-configured.""" + builder.opts.cargo_opts = None + builder.x86 = False + mocker.patch( + "gvsbuild.utils.builder.Project.get_tool_path", return_value="C:\\cargo" + ) + return builder + + +def test_exec_cargo_no_params_calls_bare_cargo(cargo_builder, mocker): + mock_exec = mocker.patch.object(cargo_builder, "_Builder__execute") + cargo_builder.exec_cargo() + cargo_cmd = mock_exec.call_args_list[1][0][0] + assert cargo_cmd == ["cargo"] + + +def test_exec_cargo_list_params_appended(cargo_builder, mocker): + mock_exec = mocker.patch.object(cargo_builder, "_Builder__execute") + cargo_builder.exec_cargo(["install", "cargo-c", "--locked"]) + cargo_cmd = mock_exec.call_args_list[1][0][0] + assert cargo_cmd == ["cargo", "install", "cargo-c", "--locked"] + + +def test_exec_cargo_global_opts_are_split_and_prepended(cargo_builder, mocker): + cargo_builder.opts.cargo_opts = "--color always" + mock_exec = mocker.patch.object(cargo_builder, "_Builder__execute") + cargo_builder.exec_cargo(["build"]) + cargo_cmd = mock_exec.call_args_list[1][0][0] + assert cargo_cmd == ["cargo", "--color", "always", "build"] From 3490fbff5ec432b152bf222e76c9131e2719ef34 Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Fri, 22 May 2026 15:51:11 -0400 Subject: [PATCH 2/8] Add pytest-cov, improve coverage --- pyproject.toml | 1 + tests/utils/test_builder.py | 45 ++++++++++++ tests/utils/test_execute.py | 71 ++++++++++++++++++- uv.lock | 134 ++++++++++++++++++++++++++++++++++++ 4 files changed, 250 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3054f35e3..bfd0b406f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ dev = [ "pytest>=7.1.3,<10.0.0", "tox >=4.1.2,<5.0.0", "pytest-mock >=3.10.0", + "pytest-cov>=7.1.0", ] [[tool.mypy.overrides]] diff --git a/tests/utils/test_builder.py b/tests/utils/test_builder.py index 0aa19c1ad..f8a9b040f 100644 --- a/tests/utils/test_builder.py +++ b/tests/utils/test_builder.py @@ -20,6 +20,51 @@ from gvsbuild.utils.builder import Builder, CheckVsInstallError +@pytest.fixture +def msbuild_builder(mocker): + """Builder with opts configured for _create_msbuild_opts tests.""" + b = Builder.__new__(Builder) + b.opts = mocker.Mock() + b.opts.platform = "x64" + b.opts.win_sdk_ver = None + b.opts.net_target_framework = None + b.opts.net_target_framework_version = None + b.opts.msbuild_opts = None + return b + + +def test_create_msbuild_opts_minimal(msbuild_builder): + result = msbuild_builder._create_msbuild_opts(None) + assert "/nologo" in result + assert "/p:Platform=x64" in result + assert not any("PythonPath" in s for s in result) + + +def test_create_msbuild_opts_with_python(msbuild_builder, tmp_path): + result = msbuild_builder._create_msbuild_opts(tmp_path) + assert f"/p:PythonPath={tmp_path}" in result + assert f"/p:PythonDir={tmp_path}" in result + + +def test_create_msbuild_opts_with_sdk_ver(msbuild_builder): + msbuild_builder.opts.win_sdk_ver = "10.0.22000.0" + result = msbuild_builder._create_msbuild_opts(None) + assert "/p:WindowsTargetPlatformVersion=10.0.22000.0" in result + + +def test_create_msbuild_opts_with_framework(msbuild_builder): + msbuild_builder.opts.net_target_framework = "net6.0" + result = msbuild_builder._create_msbuild_opts(None) + assert "/p:TargetFrameworks=net6.0" in result + + +def test_create_msbuild_opts_extra_opts_are_split(msbuild_builder): + msbuild_builder.opts.msbuild_opts = "/m:4 /nr:false" + result = msbuild_builder._create_msbuild_opts(None) + assert "/m:4" in result + assert "/nr:false" in result + + def test_vs_check_error_if_version_not_matching(): opts = Options() opts.vs_ver = "17" diff --git a/tests/utils/test_execute.py b/tests/utils/test_execute.py index b1543b38e..c60e9dcd1 100644 --- a/tests/utils/test_execute.py +++ b/tests/utils/test_execute.py @@ -17,6 +17,8 @@ import pytest +from gvsbuild.utils.base_builders import Rust +from gvsbuild.utils.base_project import Project from gvsbuild.utils.builder import Builder # Patch targets — always patch where the name is looked up @@ -165,7 +167,6 @@ def test_exec_vs_does_not_expand_percent_substitution(builder, mocker): builder.exec_vs(["nmake", "PREFIX=%(gtk_dir)s"]) - # Must be passed through unchanged, not expanded assert mock_exec.call_args[0][0][1] == "PREFIX=%(gtk_dir)s" @@ -200,3 +201,71 @@ def test_exec_cargo_global_opts_are_split_and_prepended(cargo_builder, mocker): cargo_builder.exec_cargo(["build"]) cargo_cmd = mock_exec.call_args_list[1][0][0] assert cargo_cmd == ["cargo", "--color", "always", "build"] + + +@pytest.fixture +def project_stub(mocker): + """Minimal Project instance for exec_msbuild tests.""" + p = Project.__new__(Project) + p.builder = mocker.Mock() + p.builder.opts.configuration = "release" + p.builder._create_msbuild_opts.return_value = [ + "/nologo", + "/p:Platform=x64", + "/v:minimal", + ] + p.build_dir = r"C:\build\myproject" + p._Project__working_dir = None # required by _get_working_dir + return p + + +def test_exec_msbuild_builds_correct_command(project_stub): + project_stub.exec_msbuild([r"src\all.sln", "/p:SkipUWP=true"]) + cmd = project_stub.builder.exec_vs.call_args[0][0] + assert cmd[0] == "msbuild" + assert r"src\all.sln" in cmd + assert "/p:SkipUWP=true" in cmd + assert "/p:Configuration=release" in cmd + assert "/nologo" in cmd + + +def test_exec_msbuild_explicit_configuration_overrides_opts(project_stub): + project_stub.exec_msbuild([r"src\all.sln"], configuration="debug") + cmd = project_stub.builder.exec_vs.call_args[0][0] + assert "/p:Configuration=debug" in cmd + + +@pytest.fixture +def rust_project(mocker): + """Minimal Rust project instance for cargo-params construction tests.""" + p = Rust.__new__(Rust) + p.builder = mocker.Mock() + p.builder.opts.configuration = "debug" + p.build_dir = r"C:\build\librsvg" + p.clean = False + p.extra_opts = [] + p.version = "stable" + # Rust.build calls shutil.copytree after exec_cargo; patch it out + mocker.patch("gvsbuild.utils.base_builders.shutil.copytree") + return p + + +def test_rust_build_debug_does_not_add_release_flag(rust_project): + rust_project.build() + params = rust_project.builder.exec_cargo.call_args[1]["params"] + assert "build" in params + assert "--release" not in params + + +def test_rust_build_release_adds_release_flag(rust_project): + rust_project.builder.opts.configuration = "release" + rust_project.build() + params = rust_project.builder.exec_cargo.call_args[1]["params"] + assert "--release" in params + + +def test_rust_build_forwards_cargo_params(rust_project): + rust_project.build(cargo_params=["--features", "foo"]) + params = rust_project.builder.exec_cargo.call_args[1]["params"] + assert "--features" in params + assert "foo" in params diff --git a/uv.lock b/uv.lock index 476f91d17..0d7de01ee 100644 --- a/uv.lock +++ b/uv.lock @@ -203,6 +203,124 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "coverage" +version = "7.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/7f/d0720730a397a999ffc0fd3f5bebef347338e3a47b727da66fbb228e2ff2/coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74", size = 919489, upload-time = "2026-05-10T18:02:31.397Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/9d/7c83ef51c3eb495f10010094e661833588b7709946da634c8b66520b97c7/coverage-7.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84c32d90bf4537f0e7b4dec9aaa9a938fb8205136b9d2ecf4d7629d5262dc075", size = 219668, upload-time = "2026-05-10T17:59:23.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/34/898546aefbd28f0af131201d0dc852c9e976f817bd7d5bfb8dc4e02863bb/coverage-7.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7c843572c605ab51cfdb5c6b5f2586e2a8467c0d28eca4bdef4ec70c5fecbd82", size = 220192, upload-time = "2026-05-10T17:59:26.095Z" }, + { url = "https://files.pythonhosted.org/packages/df/4a/b457c88aca72b0df13a98167ebd5d947135ccd9881ea88ce6a570e13aa9b/coverage-7.14.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0c451757d3fa2603354fdc789b5e58a0e327a117c370a40e3476ba4eabab228c", size = 246932, upload-time = "2026-05-10T17:59:27.806Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d9/92600e89486fd074c50f0117422b2c9592c3e144e2f25bd5ac0bc62bc7a0/coverage-7.14.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3fd43f0616e765ab78d069cf8358def7363957a45cee446d65c502dcfeea7893", size = 248762, upload-time = "2026-05-10T17:59:29.479Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e1/9ea1eb9c311da7f15853559dc1d9d82bef88ecd3e59fbeb51f16bc2ffa91/coverage-7.14.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:731e535b1498b27d13594a0527a79b0510867b0ad891532be41cb883f2128e20", size = 250625, upload-time = "2026-05-10T17:59:31.33Z" }, + { url = "https://files.pythonhosted.org/packages/a5/03/57afca1b8106f8549a5329139315041fe166d6099bd9381346b9430dfbd1/coverage-7.14.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7492f2d493b976941c7ca050f273cbda2f43c381124f7586a3e3c16d1804fec", size = 252539, upload-time = "2026-05-10T17:59:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/57/5e/2e9fc63c9928119c1dbae02222be51407d3e7ebac5811ebbda4af3557795/coverage-7.14.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc38367eaa2abb1b766ac333142bce7655335a73537f5c8b75aaa89c2b987757", size = 247636, upload-time = "2026-05-10T17:59:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e2/0b7898cda21041cc67546e19b80ba66cbbb47cbece52a76a5904de6a3aaf/coverage-7.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0a951308cde22cf77f953955a754d04dccb57fe3bb8e345d685778ed9fc1632a", size = 248666, upload-time = "2026-05-10T17:59:36.232Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/d33662a2fdaef23229c15921f39c84ec38441f3069ba26e134ed402c833b/coverage-7.14.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fab3877e4ebb06bd9d4d4d00ee53309ee5478e66873c66a382272e3ee33eb7ea", size = 246670, upload-time = "2026-05-10T17:59:38.029Z" }, + { url = "https://files.pythonhosted.org/packages/99/b2/533942c3bfbf6770b5c32d7f2ff029fe013dba31f3fe8b45cabbb250365e/coverage-7.14.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b812eb847b19876ebf33fb6c4f11819af05ab6050b0bfa1bc53412ae81779adb", size = 250484, upload-time = "2026-05-10T17:59:39.974Z" }, + { url = "https://files.pythonhosted.org/packages/d8/00/15acbad83a96de13c73831486c7627bfed73dfaec53b04e4a6315edf3fd8/coverage-7.14.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d9c8ef6ed820c433de075657d72dda1f89a2984955e58b8a75feb3f184250218", size = 246942, upload-time = "2026-05-10T17:59:41.659Z" }, + { url = "https://files.pythonhosted.org/packages/70/db/cef0228de493f2c740c760a9057a61d00c6849480073b70a75b87c7d4bab/coverage-7.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d128b1bba9361fbaaf6a19e179e6cfd6a9103ce0c0555876f72780acc93efd85", size = 247544, upload-time = "2026-05-10T17:59:43.471Z" }, + { url = "https://files.pythonhosted.org/packages/77/a0/d9ef8e148f3025c2ae8401d77cda1502b6d2a4d8102603a8af31460aedb6/coverage-7.14.0-cp310-cp310-win32.whl", hash = "sha256:65f267ca1370726ec2c1aa38bbe4df9a71a740f22878d2d4bf59d71a4cd8d323", size = 222285, upload-time = "2026-05-10T17:59:44.908Z" }, + { url = "https://files.pythonhosted.org/packages/85/c0/30c454c7d3cf47b2805d4e06f12443f5eece8a5d030d3b0350e7b74ecb49/coverage-7.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:b34ece8065914f938ed7f2c5872bb865336977a52919149846eac3744327267a", size = 223215, upload-time = "2026-05-10T17:59:46.779Z" }, + { url = "https://files.pythonhosted.org/packages/fc/e4/649c8d4f7f1709b6dbfc474358aa1bba02f67bcd52e2fec291a5014006cd/coverage-7.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a78e2a9d9c5e3b8d4ab9b9d28c985ea66fced0a7d7c2aec1f216e03a2011480", size = 219795, upload-time = "2026-05-10T17:59:48.198Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8d/46692d24b3f395d4cbf17bfcc57136b4f2f9c0c0df864b0bddfc1d71a014/coverage-7.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1816c505187592dcd1c5a5f226601a549f70365fbd00930ac88b0c225b76bb4", size = 220299, upload-time = "2026-05-10T17:59:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/12/c2/a40f5cb295bbcbb697a76947a56081c494c61950366294ee426ffe261099/coverage-7.14.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d8e1762f0e9cbc26ec315471e7b47855218e833cd5a032d706fbf43845d878c7", size = 250721, upload-time = "2026-05-10T17:59:51.494Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/202235eb5c3c14c212462cd91d61b7386bf8fc44bc7a77f4742d2a69174b/coverage-7.14.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9336e23e8bb3a3925398261385e2a1533957d3e760e91070dcb0e98bfa514eed", size = 252633, upload-time = "2026-05-10T17:59:53.244Z" }, + { url = "https://files.pythonhosted.org/packages/bb/80/5f596e8995785124ee191c42535664c5e62c65995b66f4ca21e28ae04c81/coverage-7.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd1169b2230f9cbe9c638ba38022ed7a2b1e641cc07f7cea0365e4be2a74980", size = 254743, upload-time = "2026-05-10T17:59:55.021Z" }, + { url = "https://files.pythonhosted.org/packages/1e/6d/0d178825be2350f0adb27984d0aa7cf84bbdab201f6fb926b535d23a8f5f/coverage-7.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1bb3543b58fea74d2cd1abc4054cc927e4724687cb4560cd2ed88d2c7d820c0", size = 256700, upload-time = "2026-05-10T17:59:56.511Z" }, + { url = "https://files.pythonhosted.org/packages/19/5b/9e549c2f6e9dfea472adadba06c294e64735dabc2dd19015fac082095013/coverage-7.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a93bac2cb577ef60074999ed56d8a1535894398e2ed920d4185c3ec0c8864742", size = 250854, upload-time = "2026-05-10T17:59:57.94Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1c/b94f9f5f36396021ee2f62c5834b12e6a3d31f0bed5d6fc6d1c3caec087c/coverage-7.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5904abf7e18cddc463219b17552229650c6b79e061d31a1059283051169cf7d5", size = 252433, upload-time = "2026-05-10T17:59:59.688Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cb/d192cd8e1345eccabc32016f2d39072ecd10cb4f4b983ed8d0ebdeaf00dc/coverage-7.14.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:741f57cddc9004a8c81b084660215f33a6b597dbe62c31386b983ee26310e327", size = 250494, upload-time = "2026-05-10T18:00:01.953Z" }, + { url = "https://files.pythonhosted.org/packages/53/c5/aac9f460a41d835dbddef1d377f105f6ac2311d0f3c1588e9f51046d8813/coverage-7.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:664123feb0929d7affc135717dbd70d61d98688a08ab1e5ba464739620c6252d", size = 254261, upload-time = "2026-05-10T18:00:03.779Z" }, + { url = "https://files.pythonhosted.org/packages/23/aa/7af7c0081980a9cb3d289c5a435a4b7657dcecbd128e25c580e6a50389b5/coverage-7.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c83d2399a51bbec8429266905d33616f04bc5726b1138c35844d5fcd896b2e20", size = 250216, upload-time = "2026-05-10T18:00:05.262Z" }, + { url = "https://files.pythonhosted.org/packages/35/60/a4257538ce2f6b978aeb51870d6c4208c510928a03db7e0339bb625dccb7/coverage-7.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb2e855b87321259a037429288ae85216d191c74de3e79bf57cd2bc0761992c", size = 251125, upload-time = "2026-05-10T18:00:06.858Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ab/f91af47642ec1aa53490e835a95847168d9c77fc39aa58527604c051e145/coverage-7.14.0-cp311-cp311-win32.whl", hash = "sha256:731dc15b385ac52289743d476245b61e1a2927e803bef655b52bc3b2a75a21f3", size = 222300, upload-time = "2026-05-10T18:00:08.608Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f0/a71ddbd874431e7a7cd96071f0c331cfbbad07704833c765d24ffbab8a67/coverage-7.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:bfb0ed8ec5d25e93face268115d7964db9df8b9aae8edcde9ec6b16c726a7cc1", size = 223241, upload-time = "2026-05-10T18:00:10.746Z" }, + { url = "https://files.pythonhosted.org/packages/d8/6e/d9d312a5151a96cd110efee32efc3fc97b01ebd86203fe618ccb29cf4c92/coverage-7.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:7ebb1c6df9f78046a1b1e0a89674cd4bf73b7c648914eebcf976a57fd99a5627", size = 221908, upload-time = "2026-05-10T18:00:12.242Z" }, + { url = "https://files.pythonhosted.org/packages/09/1e/2f996b2c8415cbb6f54b0f5ec1ee850c96d7911961afb4fc05f4a89d8c58/coverage-7.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7ffd19fc8aed057fd686a17a4935eef5f9859d69208f96310e893e64b9b6ccf5", size = 219967, upload-time = "2026-05-10T18:00:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/34/23/35c7aea1274aef7525bdd2dc92f710bdde6d11652239d71d1ec450067939/coverage-7.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:829994cfe1aeb773ca27bf246d4badc1e764893e3bfb98fff820fcecd1ca4662", size = 220329, upload-time = "2026-05-10T18:00:15.264Z" }, + { url = "https://files.pythonhosted.org/packages/75/cf/a8f4b43a16e194b0261257ad28ded5853ec052570afef4a84e1d81189f3b/coverage-7.14.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b4f07cf7edcb7ec39431a5074d7ea83b29a9f71fcfc494f0f40af4e65180420f", size = 251839, upload-time = "2026-05-10T18:00:17.16Z" }, + { url = "https://files.pythonhosted.org/packages/69/ff/6699e7b71e60d3049eb2bdcbc95ee3f35707b2b0e48f32e9e63d3ce30c08/coverage-7.14.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca3d9cf2c32b521bd9518385608787fa86f38daf993695307531822c3430ed67", size = 254576, upload-time = "2026-05-10T18:00:18.829Z" }, + { url = "https://files.pythonhosted.org/packages/22/ec/c936d495fcd67f48f03a9c4ad3297ff80d1f222a5df3980f15b34c186c21/coverage-7.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92af52828e7f29d827346b0294e5a0853fa206db77db0395b282918d41e28db9", size = 255690, upload-time = "2026-05-10T18:00:20.648Z" }, + { url = "https://files.pythonhosted.org/packages/5c/42/5af63f636cc62a4a2b1b3ba9146f6ee6f53a35a50d5cefc54d5670f60999/coverage-7.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b2bb6c9d7e769360d0f20a0f219603fd64f0c8f97de17ab25853261602be0fb", size = 257949, upload-time = "2026-05-10T18:00:22.28Z" }, + { url = "https://files.pythonhosted.org/packages/26/d3/a225317bd2012132a27e1176d51660b826f99bb975876463c44ea0d7ee5a/coverage-7.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c9ed6ef99f88fb8c14aa8e2bf8eb0fe55fa2edfea68f8675d78741df1a5ac0e", size = 252242, upload-time = "2026-05-10T18:00:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7f/9e65495298c3ea414742998539c37d048b5e81cc818fb1828cc6b51d10bf/coverage-7.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8231ade007f37959fbf58acc677f26b922c02eda6f0428ea307da0fd39681bf3", size = 253608, upload-time = "2026-05-10T18:00:25.588Z" }, + { url = "https://files.pythonhosted.org/packages/94/46/1522b524a35bdad22b2b8c4f9d32d0a104b524726ec380b2db68db1746f5/coverage-7.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d8b013632cc1ce1d09dbe4f32667b4d320ec2f54fc326ebeffcd0b0bcc2bb6c4", size = 251753, upload-time = "2026-05-10T18:00:27.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e9/cdf00d38817742c541ade405e115a3f7bf36e6f2a8b99d4f209861b85a2d/coverage-7.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1733198802d71ec4c524f322e2867ee05c62e9e75df86bdca545407a221827d1", size = 255823, upload-time = "2026-05-10T18:00:29.038Z" }, + { url = "https://files.pythonhosted.org/packages/38/fc/5e7877cf5f902d08a17ff1c532511476d87e1bea355bd5028cb97f902e79/coverage-7.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:72a305291fa8ee01332f1aaf38b348ca34097f6aa0b0ef627eef2837e57bbba5", size = 251323, upload-time = "2026-05-10T18:00:30.647Z" }, + { url = "https://files.pythonhosted.org/packages/18/9d/50f05a72dff8487464fdd4178dda5daed642a060e60afb644e3d45123559/coverage-7.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcaba850dd317c65423a9d63d88f9573c53b00354d6dd95724576cc98a131595", size = 253197, upload-time = "2026-05-10T18:00:32.211Z" }, + { url = "https://files.pythonhosted.org/packages/00/3f/6f61ffe6439df266c3cf60f5c99cfaa21103d0210d706a42fc6c30683ff8/coverage-7.14.0-cp312-cp312-win32.whl", hash = "sha256:5ac83957a80d0701310e96d8bec68cdcf4f90a7674b7d13f15a344315b41ab27", size = 222515, upload-time = "2026-05-10T18:00:33.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/19/93853133df2cb371083285ef6a93982a0173e7a233b0f61373ba9fd30eb2/coverage-7.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:70390b0da32cb90b501953716302906e8bcce087cb283e70d8c97729f22e92b2", size = 223324, upload-time = "2026-05-10T18:00:35.172Z" }, + { url = "https://files.pythonhosted.org/packages/74/18/9f7fe62f659f24b7a82a0be56bf94c1bd0a89e0ae7ab4c668f6e82404294/coverage-7.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:91b993743d959b8be85b4abf9d5478216a69329c321efe5be0433c1a841d691d", size = 221944, upload-time = "2026-05-10T18:00:37.014Z" }, + { url = "https://files.pythonhosted.org/packages/6b/76/b7c66ee3c66e1b0f9d894c8125983aa0c03fb2336f2fd16559f9c966157f/coverage-7.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f2bbb8254370eb4c628ff3d6fa8a7f74ddc40565394d4f7ab791d1fe568e37ef", size = 219990, upload-time = "2026-05-10T18:00:38.887Z" }, + { url = "https://files.pythonhosted.org/packages/b3/af/e567cbad5ba69c013a50146dfa886dc7193361fda77521f51274ff620e1b/coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23b81107f46d3f21d0cbce30664fcec0f5d9f585638a67081750f99738f6bf66", size = 220365, upload-time = "2026-05-10T18:00:40.864Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/9ad575d505b4d805b254febc8a5b338a2efe278f8786e56ff1cb8413f9c3/coverage-7.14.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:22a7e06a5f11a757cdfe79018e9095f9f69ae283c5cd8123774c788deec8717b", size = 251363, upload-time = "2026-05-10T18:00:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5f/b5370068b2f57787454592ed7dcd1002f0f1703b7db1fa30f6a325a4ca6e/coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d1aa57a1dc8e05bdc42e81c5d671d849577aeedf279f4c449d6d286f9ed88ca", size = 253961, upload-time = "2026-05-10T18:00:44.079Z" }, + { url = "https://files.pythonhosted.org/packages/29/1e/51adf17738976e8f2b85ddef7b7aa12a0838b056c92f175941d8862767c1/coverage-7.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c1a51bcfddf645b3bb7ec333d9e94393a8e94f55642380fa8a9a5a9e636cb7", size = 255193, upload-time = "2026-05-10T18:00:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7b/5bfd7ac1df3b881c2ac7a5cbc99c7609e6296c402f5ef587cd81c6f355b3/coverage-7.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a841fae2fadcae4f438d43b6ccc4aac2ad609f47cdb6cfdce60cbb3fe5ca7bc2", size = 257326, upload-time = "2026-05-10T18:00:47.173Z" }, + { url = "https://files.pythonhosted.org/packages/7d/38/1d37d316b174fad3843a1d76dbdfe4398771c9ecd0515935dd9ece9cd627/coverage-7.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c79d2319cabef1fe8e86df73371126931550804738f78ad7d31e3aad85a67367", size = 251582, upload-time = "2026-05-10T18:00:49.152Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/746704f95980ba220214e1a41e18cec5aea80a898eaa53c51bf2d645ff36/coverage-7.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b23b0c6f0b1db6ad769b7050c8b641c0bf215ded26c1816955b17b7f26edfa9", size = 253325, upload-time = "2026-05-10T18:00:51.252Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b9/bbe87206d9687b192352f893797825b5f5b15ecd3aa9c68fbff0c074d77b/coverage-7.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:55d3089079ce181a4566b1065ab28d2575eb76d8ac8f81f4fcda2bf037fee087", size = 251291, upload-time = "2026-05-10T18:00:52.816Z" }, + { url = "https://files.pythonhosted.org/packages/46/57/b8cdb12ac0d73ef0243218bd5e22c9df8f92edab8018213a86aec67c5324/coverage-7.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:49c005cba1e2f9677fb2845dcdf9a2e72a52a17d63e8231aaaae35d9f50215ef", size = 255448, upload-time = "2026-05-10T18:00:54.548Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d4/5002019538b2036ce3c84340f54d2fd5100d55b0a6b0894eee56128d03c7/coverage-7.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9117377b823daa28aa8635fbb08cda1cd6be3d7143257345459559aeef852d52", size = 251110, upload-time = "2026-05-10T18:00:56.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/20c5009477660f084e6ed60bc02a91894b8e234e617e86ecfd9aaf78e27b/coverage-7.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b79d646cf46d5cf9a9f40281d4441df5849e445726e369006d2b117710b33fe", size = 252885, upload-time = "2026-05-10T18:00:57.967Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ab/3cf6427ac9c1f1db747dbb1ce71dde47984876d4c2cfd018a3fef0a78d4d/coverage-7.14.0-cp313-cp313-win32.whl", hash = "sha256:fb609b3658479e33f9516d46f1a89dbb9b6c261366e3a11844a96ec487533dae", size = 222539, upload-time = "2026-05-10T18:00:59.581Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b8/9228523e80321c2cb4880d1f589bc0171f2f71432c35118ad04dc01decce/coverage-7.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0773d8329cf32b6fd222e4b52622c61fe8d503eb966cfc8d3c3c10c96266d50e", size = 223344, upload-time = "2026-05-10T18:01:01.531Z" }, + { url = "https://files.pythonhosted.org/packages/a3/99/118daa192f95e3a6cb2740100fbf8797cda1734b4134ef0b5d501a7fa8f3/coverage-7.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:b4e26a0f1b696faf283bffe5b8569e44e336c582439df5d53281ab89ee0cba96", size = 221966, upload-time = "2026-05-10T18:01:03.16Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f1/a46cc0c013be170216253184a32366d7cbdb9252feaec866b05c2d12a894/coverage-7.14.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:953f521ca9445300397e65fda3dca58b2dbd68fee983777420b57ac3c77e9f90", size = 220679, upload-time = "2026-05-10T18:01:05.058Z" }, + { url = "https://files.pythonhosted.org/packages/64/8c/9c30a3d311a34177fa432995be7fbfc64477d8bac5630bd38055b1c9b424/coverage-7.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:98af83fd65ae24b1fdd03aaead967a9f523bcd2f1aab2d4f3ffda65bb568a6f1", size = 221033, upload-time = "2026-05-10T18:01:07.002Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cd/3fb5e06c3badefd0c1b47e2044fdca67f8220a4ec2e7fcfb476aa0a67c6c/coverage-7.14.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:668b92e6958c4db7cf92e81caac328dfbbdbb215db2850ad28f0cbe1eea0bfbd", size = 262333, upload-time = "2026-05-10T18:01:08.903Z" }, + { url = "https://files.pythonhosted.org/packages/a8/e6/fbc322325c7294d3e22c1ad6b79e45d0806b25228c8e5842aed6d8169aa7/coverage-7.14.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9fbd898551762dea00d3fef2b1c4f99afd2c6a3ff952ea07d60a9bd5ed4f34bc", size = 264410, upload-time = "2026-05-10T18:01:10.531Z" }, + { url = "https://files.pythonhosted.org/packages/08/92/c497b264bec1673c47cc77e26f760fcda4654cabf1f39546d1a23a3b8c35/coverage-7.14.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68af363c07ecd8d4b7d4043d85cb376d7d227eceb54e5323ee45da73dbd3e426", size = 266836, upload-time = "2026-05-10T18:01:12.19Z" }, + { url = "https://files.pythonhosted.org/packages/78/fc/045da320987f401af5d2815d351e8aa799aec859f60e29f445e3089eeedb/coverage-7.14.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e57054a583da8ac55edf24117ea4c9133032cfc4cf72aa2d48c1e5d4b52f899", size = 267974, upload-time = "2026-05-10T18:01:13.926Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ae/227b1e379497fb7a4fc3286e620f80c8a1e7cec66d45695a01639eb1af65/coverage-7.14.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3499459bbcdd51a65b64c35ab7ed2764eaf3cba826e0df3f1d7fe2e102b70b", size = 261578, upload-time = "2026-05-10T18:01:15.564Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f5/3570342900f2acea31d33ff1590c5d8bac1a8e1a2e1c6d34a5d5e61de681/coverage-7.14.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:45899ec2138a4346ed34d601dedf5076fb74edf2d1dd9dc76a78e82397edee90", size = 264394, upload-time = "2026-05-10T18:01:17.607Z" }, + { url = "https://files.pythonhosted.org/packages/16/29/de1bbc01c935b28f89b1dc3db85b011c055e843a8e5e3b83141c3f80af7f/coverage-7.14.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8767486808c436f05b23ab98eb963fb29185e32a9357a166971685cb3459900f", size = 262022, upload-time = "2026-05-10T18:01:19.304Z" }, + { url = "https://files.pythonhosted.org/packages/35/95/f53890b0bf2fc10ab168e05d38869215e73ca24c4cb521c3bb0eb62fe16b/coverage-7.14.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a3b5ddfd6aa7ddad53ee3edb231e88a2151507a43229b7d71b953916deca127d", size = 265732, upload-time = "2026-05-10T18:01:21.494Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ea/c919e259081dd2bdf0e43b87209709ba7ec2e4117c2a7f5185379c43463c/coverage-7.14.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:63df0fe568e698e1045792399f8ab6da3a6c2dce3182813fb92afa2641087b47", size = 260921, upload-time = "2026-05-10T18:01:23.533Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2c/c2831889705a81dc5d1c6ca12e4d8e9b95dfc146d153488a6c0ea685d28e/coverage-7.14.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:827d6397dbd95144939b18f89edf31f63e1f99633e8d5f32f22ba8bdda567477", size = 263109, upload-time = "2026-05-10T18:01:25.165Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a9/2fcae5003cac3d63fe344d2166243c2756935f48420863c5272b240d550b/coverage-7.14.0-cp313-cp313t-win32.whl", hash = "sha256:7bf43e000d24012599b879791cff41589af90674722421ef11b11a5431920bab", size = 223212, upload-time = "2026-05-10T18:01:27.157Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bb/18e94d7b14b9b398164197114a587a04ab7c9fdbe1d237eef57311c5e883/coverage-7.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3f5549365af25d770e06b1f8f5682d9a5637d06eb494db91c6fa75d3950cc917", size = 224272, upload-time = "2026-05-10T18:01:29.107Z" }, + { url = "https://files.pythonhosted.org/packages/db/56/4f14fad782b035c81c4ffd09159e7103d42bb1d93ac8496d04b90a11b7da/coverage-7.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6d160217ec6fe890f16ad3a9531761589443749e448f91986c972714fad361c8", size = 222530, upload-time = "2026-05-10T18:01:31.151Z" }, + { url = "https://files.pythonhosted.org/packages/1c/18/b9a6586d73992807c26f9a5f274131be3d76b56b18a82b9392e2a25d2e45/coverage-7.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d", size = 220036, upload-time = "2026-05-10T18:01:33.057Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9b/4165a1d56ddc302a0e2d518fd9d412a4fd0b57562618c78c5f21c57194f5/coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63", size = 220368, upload-time = "2026-05-10T18:01:34.705Z" }, + { url = "https://files.pythonhosted.org/packages/69/aa/c12e52a5ba148d9995229d557e3be6e554fe469addc0e9241b2f0956d8ea/coverage-7.14.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212", size = 251417, upload-time = "2026-05-10T18:01:36.949Z" }, + { url = "https://files.pythonhosted.org/packages/d7/51/ec641c26e6dca1b25a7d2035ba6ecb7c884ef1a100a9e42fbe4ce4405139/coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3", size = 253924, upload-time = "2026-05-10T18:01:38.985Z" }, + { url = "https://files.pythonhosted.org/packages/33/c4/59c3de0bd1b538824173fd518fed51c1ce740ca5ed68e74545983f4053a9/coverage-7.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97", size = 255269, upload-time = "2026-05-10T18:01:40.957Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a9/36dfa153a62040296f6e7febfdb20a5720622f6ef5a81a41e8237b9a5344/coverage-7.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8", size = 257583, upload-time = "2026-05-10T18:01:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/26/7b/cc2c048d4114d9ab1c2409e9ee365e5ae10736df6dffcfc9444effa6c708/coverage-7.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb", size = 251434, upload-time = "2026-05-10T18:01:44.537Z" }, + { url = "https://files.pythonhosted.org/packages/ee/df/6770eaa576e604575e9a78055313250faef5faa84bd6f71a39fece519c43/coverage-7.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe", size = 253280, upload-time = "2026-05-10T18:01:46.175Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9e/1c0264514a3f98259a6d64765a397b2c8373e3ba59ee722a4802d3ec0c61/coverage-7.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa", size = 251241, upload-time = "2026-05-10T18:01:48.732Z" }, + { url = "https://files.pythonhosted.org/packages/64/16/4efdf3e3c4079cdbf0ece56a2fea872df9e8a3e15a13a0af4400e1075944/coverage-7.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5", size = 255516, upload-time = "2026-05-10T18:01:50.819Z" }, + { url = "https://files.pythonhosted.org/packages/93/69/b1de96346603881b3d1bc8d6447c83200e1c9700ffbaff926ba01ff5724c/coverage-7.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c", size = 251059, upload-time = "2026-05-10T18:01:52.773Z" }, + { url = "https://files.pythonhosted.org/packages/a4/66/2881853e0363a5e0a724d1103e53650795367471b6afb234f8b49e713bc6/coverage-7.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca", size = 252716, upload-time = "2026-05-10T18:01:54.506Z" }, + { url = "https://files.pythonhosted.org/packages/55/5c/0d3305d002c41dcde873dbe456491e663dc55152ca526b630b5c47efd62f/coverage-7.14.0-cp314-cp314-win32.whl", hash = "sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828", size = 222788, upload-time = "2026-05-10T18:01:56.487Z" }, + { url = "https://files.pythonhosted.org/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d", size = 223600, upload-time = "2026-05-10T18:01:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/00/70/a18c408e674bc26281cadaedc7351f929bd2094e191e4b15271c30b084cc/coverage-7.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9", size = 222168, upload-time = "2026-05-10T18:02:00.411Z" }, + { url = "https://files.pythonhosted.org/packages/3d/89/2681f071d238b62aff8dfc2ab44fc24cfdb38d1c01f391a80522ff5d3a16/coverage-7.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1", size = 220766, upload-time = "2026-05-10T18:02:02.313Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c7/c987babafd9207ffa1995e1ef1f9b26762cf4963aa768a66b6f0501e4616/coverage-7.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c", size = 221035, upload-time = "2026-05-10T18:02:04.017Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e9/d6a5ac3b333088143d6fc877d398a9a674dc03124a2f776e131f03864823/coverage-7.14.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84", size = 262405, upload-time = "2026-05-10T18:02:05.915Z" }, + { url = "https://files.pythonhosted.org/packages/38/b1/e70838d29a7c08e22d44398a46db90815bbcbf28de06992bd9210d1a8d8e/coverage-7.14.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436", size = 264530, upload-time = "2026-05-10T18:02:07.582Z" }, + { url = "https://files.pythonhosted.org/packages/6b/73/5c31ef97763288d03d9995152b96d5475b527c63d91c84b01caea894b83a/coverage-7.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a", size = 266932, upload-time = "2026-05-10T18:02:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/e1/76/dd56d80f29c5f05b4d76f7e7c6d47cafacae017189c75c5759d24f9ff0cc/coverage-7.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f", size = 268062, upload-time = "2026-05-10T18:02:11.399Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c7/27ba85cd5b95614f159ff93ebff1901584a8d192e2e5e24c4943a7453f59/coverage-7.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb", size = 261504, upload-time = "2026-05-10T18:02:13.257Z" }, + { url = "https://files.pythonhosted.org/packages/13/2e/e8149f60ab5d5684c6eee881bdf34b127115cddbb958b196768dd9d63473/coverage-7.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490", size = 264398, upload-time = "2026-05-10T18:02:15.063Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7f/1261b025285323225f4b4abffa5a643649dfd67e25ddca7ebcbdea3b7cb3/coverage-7.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9", size = 262000, upload-time = "2026-05-10T18:02:16.756Z" }, + { url = "https://files.pythonhosted.org/packages/d3/dc/829c54f60b9d08389439c00f813c752781c496fc5788c78d8006db4b4f2b/coverage-7.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020", size = 265732, upload-time = "2026-05-10T18:02:18.817Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b0/70bd1419941652fa062689cba9c3eeafb8f5e6fbb890bce41c3bdda5dbd6/coverage-7.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6", size = 260847, upload-time = "2026-05-10T18:02:20.528Z" }, + { url = "https://files.pythonhosted.org/packages/f2/73/be40b2390656c654d35ea0015ea7ba3d945769cf80790ad5e0bb2d56d2ba/coverage-7.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db", size = 263166, upload-time = "2026-05-10T18:02:22.337Z" }, + { url = "https://files.pythonhosted.org/packages/29/55/4a643f712fcf7cf2881f8ec1e0ccb7b164aff3108f69b51801246c8799f2/coverage-7.14.0-cp314-cp314t-win32.whl", hash = "sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2", size = 223573, upload-time = "2026-05-10T18:02:24.11Z" }, + { url = "https://files.pythonhosted.org/packages/27/96/3acae5da0953be042c0b4dea6d6789d2f080701c77b88e44d5bd41b9219b/coverage-7.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644", size = 224680, upload-time = "2026-05-10T18:02:25.896Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/6ab5d2dd8325d838737c6f8d83d62eb6230e0d70b87b51b57bbfd08fa767/coverage-7.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b", size = 222703, upload-time = "2026-05-10T18:02:27.822Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/cb8e80d6f9f55b99588625062822bf946cf03ed06315df4bd8397f5632a1/coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1", size = 211764, upload-time = "2026-05-10T18:02:29.538Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + [[package]] name = "cyclopts" version = "5.0.0a6" @@ -300,6 +418,7 @@ outdated = [ dev = [ { name = "prek" }, { name = "pytest" }, + { name = "pytest-cov" }, { name = "pytest-mock" }, { name = "tox" }, ] @@ -319,6 +438,7 @@ provides-extras = ["outdated"] dev = [ { name = "prek", specifier = ">=0.2.23" }, { name = "pytest", specifier = ">=7.1.3,<10.0.0" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, { name = "pytest-mock", specifier = ">=3.10.0" }, { name = "tox", specifier = ">=4.1.2,<5.0.0" }, ] @@ -565,6 +685,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + [[package]] name = "pytest-mock" version = "3.15.1" From 3f40c05c67d69226f0dd3f9b67f7692ec562ad63 Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Fri, 22 May 2026 15:59:22 -0400 Subject: [PATCH 3/8] Pass in a list with exec_ninja as well --- gvsbuild/utils/base_builders.py | 10 ++++----- gvsbuild/utils/builder.py | 6 ++++-- tests/utils/test_execute.py | 36 ++++++++++++++++++++++++++++++--- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/gvsbuild/utils/base_builders.py b/gvsbuild/utils/base_builders.py index 834724052..95be3fd63 100644 --- a/gvsbuild/utils/base_builders.py +++ b/gvsbuild/utils/base_builders.py @@ -48,11 +48,11 @@ def build(self, meson_params=None, make_tests=False, add_path=None): # Run ninja to build all (library, .... self.builder.exec_ninja(working_dir=ninja_build) # .. run the tests ... - self.builder.exec_ninja(params="test", working_dir=ninja_build) + self.builder.exec_ninja(params=["test"], working_dir=ninja_build) # .. and finally install everything # if we don't make the tests we simply run 'ninja install' that takes care of everything, # running explicitly from the build dir - self.builder.exec_ninja(params="install", working_dir=ninja_build) + self.builder.exec_ninja(params=["install"], working_dir=ninja_build) def _setup_meson_and_ninja(self, ninja_build, meson_params, add_path): log.start_verbose("Generating meson directory") @@ -143,11 +143,11 @@ def build( if use_ninja: if make_tests: self.builder.exec_ninja(working_dir=work_dir) - self.builder.exec_ninja(params="test", working_dir=work_dir) + self.builder.exec_ninja(params=["test"], working_dir=work_dir) if do_install: - self.builder.exec_ninja(params="install", working_dir=work_dir) + self.builder.exec_ninja(params=["install"], working_dir=work_dir) elif do_install: - self.builder.exec_ninja(params="install", working_dir=work_dir) + self.builder.exec_ninja(params=["install"], working_dir=work_dir) else: self.builder.exec_ninja(working_dir=work_dir) else: diff --git a/gvsbuild/utils/builder.py b/gvsbuild/utils/builder.py index 1cb13c78a..07c298ab2 100644 --- a/gvsbuild/utils/builder.py +++ b/gvsbuild/utils/builder.py @@ -953,12 +953,14 @@ def exec_cargo( def exec_cmd(self, cmd, working_dir=None, add_path=None): self.__execute(cmd, working_dir=working_dir, add_path=add_path) - def exec_ninja(self, params="", working_dir=None, add_path=None): + def exec_ninja( + self, params: list[str] | None = None, working_dir=None, add_path=None + ): cmd = ["ninja"] if self.opts.ninja_opts: cmd += self.opts.ninja_opts.split() if params: - cmd += params.split() + cmd += params self.__execute( cmd, working_dir=working_dir, diff --git a/tests/utils/test_execute.py b/tests/utils/test_execute.py index c60e9dcd1..b43edff68 100644 --- a/tests/utils/test_execute.py +++ b/tests/utils/test_execute.py @@ -17,7 +17,7 @@ import pytest -from gvsbuild.utils.base_builders import Rust +from gvsbuild.utils.base_builders import Meson, Rust from gvsbuild.utils.base_project import Project from gvsbuild.utils.builder import Builder @@ -119,7 +119,7 @@ def test_exec_ninja_no_params_produces_list(builder, mocker): def test_exec_ninja_with_params_produces_list(builder, mocker): mock_exec = mocker.patch.object(builder, "_Builder__execute") - builder.exec_ninja(params="install") + builder.exec_ninja(params=["install"]) assert mock_exec.call_args[0][0] == ["ninja", "install"] @@ -128,7 +128,7 @@ def test_exec_ninja_with_opts_produces_list(builder, mocker): builder.opts.ninja_opts = "-j4" mock_exec = mocker.patch.object(builder, "_Builder__execute") - builder.exec_ninja(params="install") + builder.exec_ninja(params=["install"]) assert mock_exec.call_args[0][0] == ["ninja", "-j4", "install"] @@ -269,3 +269,33 @@ def test_rust_build_forwards_cargo_params(rust_project): params = rust_project.builder.exec_cargo.call_args[1]["params"] assert "--features" in params assert "foo" in params + + +@pytest.fixture +def meson_project(mocker, tmp_path): + """Minimal Meson project with a pre-existing build.ninja to skip setup.""" + p = Meson.__new__(Meson) + p.builder = mocker.Mock() + p.build_dir = str(tmp_path) + p.params = [] + p.extra_opts = [] + ninja_build = tmp_path / "_gvsbuild-meson" + ninja_build.mkdir() + (ninja_build / "build.ninja").write_text("") + return p + + +def test_meson_build_calls_ninja_install(meson_project): + meson_project.build() + calls = meson_project.builder.exec_ninja.call_args_list + params = [c[1].get("params") for c in calls] + assert ["install"] in params + + +def test_meson_build_with_tests_calls_ninja_test_then_install(meson_project): + meson_project.build(make_tests=True) + calls = meson_project.builder.exec_ninja.call_args_list + params = [c[1].get("params") for c in calls] + assert None in params + assert ["test"] in params + assert ["install"] in params From 479ca6fa98ca49f56f47ab9a01c48d73e64929a2 Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Fri, 22 May 2026 16:49:52 -0400 Subject: [PATCH 4/8] Fix executable name lookup --- gvsbuild/utils/base_builders.py | 2 +- gvsbuild/utils/builder.py | 17 +++++++++++++++++ tests/utils/test_execute.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/gvsbuild/utils/base_builders.py b/gvsbuild/utils/base_builders.py index 95be3fd63..96f8cbb7f 100644 --- a/gvsbuild/utils/base_builders.py +++ b/gvsbuild/utils/base_builders.py @@ -130,7 +130,7 @@ def build( if source_part else Path(self.build_dir) ) - cmd += ["-B", cmake_dir, "-H", src_full] + cmd += ["-B", str(cmake_dir), "-S", str(src_full)] work_dir = cmake_dir else: work_dir = self._get_working_dir() diff --git a/gvsbuild/utils/builder.py b/gvsbuild/utils/builder.py index 07c298ab2..e92445f7b 100644 --- a/gvsbuild/utils/builder.py +++ b/gvsbuild/utils/builder.py @@ -991,11 +991,28 @@ def exec_msys(self, args, working_dir=None): add_path=os.path.join(self.opts.msys_dir, "usr", "bin"), ) + @staticmethod + def __resolve_executable(args, env): + """On Windows, CreateProcess does not use env['PATH'] for bare-name lookup. + Resolve the first argument to a full path via shutil.which so that tools + installed into vs_env['PATH'] (cmake, ninja, nmake, cargo, …) are found.""" + if not isinstance(args, (list, tuple)) or not args: + return args + first = os.fspath(args[0]) + if os.path.isabs(first) or os.sep in first or "/" in first: + return args # already qualified — let CreateProcess use it directly + search_path = (env or os.environ).get("PATH") + resolved = shutil.which(first, path=search_path) + if resolved is None: + return args # let subprocess raise a clear error + return [resolved, *args[1:]] + def __execute(self, args, working_dir=None, add_path=None, env=None): log.debug(f"running {args}, cwd={working_dir}, path+={add_path}") if add_path: env = dict(env) if env is not None else dict(os.environ) self.__add_path(env, add_path) + args = self.__resolve_executable(args, env) if self.opts.capture_out: try: res = subprocess.run( diff --git a/tests/utils/test_execute.py b/tests/utils/test_execute.py index b43edff68..068b99012 100644 --- a/tests/utils/test_execute.py +++ b/tests/utils/test_execute.py @@ -151,6 +151,8 @@ def test_exec_msys_list_calls_check_call(builder, mocker): def test_exec_msys_list_passes_args_verbatim(builder, mocker): + """List args must not be shell-split; URLs and paths with spaces are preserved.""" + mocker.patch("gvsbuild.utils.builder.shutil.which", return_value=None) mock_cc = mocker.patch(SUBPROCESS_CHECK_CALL) dest = "C:\\Program Files\\source" @@ -299,3 +301,33 @@ def test_meson_build_with_tests_calls_ninja_test_then_install(meson_project): assert None in params assert ["test"] in params assert ["install"] in params + + +def test_execute_resolves_bare_name_via_env_path(builder, mocker): + """Bare executable names must be resolved using env['PATH'], not the parent + process PATH — Windows CreateProcess does not consult env for bare lookups.""" + builder.vs_env = {"PATH": r"C:\fake\tools"} + fake_cmake = r"C:\fake\tools\cmake.exe" + mocker.patch( + "gvsbuild.utils.builder.shutil.which", + side_effect=lambda name, path=None: fake_cmake if name == "cmake" else None, + ) + mock_cc = mocker.patch(SUBPROCESS_CHECK_CALL) + + builder.exec_vs(["cmake", "-G", "Ninja"]) + + resolved_args = mock_cc.call_args[0][0] + assert resolved_args[0] == fake_cmake + assert resolved_args[1:] == ["-G", "Ninja"] + + +def test_execute_absolute_path_skips_resolution(builder, mocker): + """An absolute path in args[0] must pass through without calling shutil.which.""" + mock_which = mocker.patch("gvsbuild.utils.builder.shutil.which") + mock_cc = mocker.patch(SUBPROCESS_CHECK_CALL) + + bash = r"C:\msys64\usr\bin\bash" + builder.exec_vs([bash, "build.sh"]) + + mock_which.assert_not_called() + assert mock_cc.call_args[0][0][0] == bash From 5e5813d6c6b6e44153205246dfa02f463d5845d9 Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Fri, 22 May 2026 19:35:21 -0400 Subject: [PATCH 5/8] Refactor meson_params in to a list --- gvsbuild/build.py | 2 + gvsbuild/projects/atkmm.py | 2 +- gvsbuild/projects/cairomm.py | 6 +- gvsbuild/projects/directx_headers.py | 2 +- gvsbuild/projects/fribidi.py | 2 +- gvsbuild/projects/glib.py | 11 +- gvsbuild/projects/glibmm.py | 5 +- gvsbuild/projects/gobject_introspection.py | 5 +- gvsbuild/projects/gstreamer.py | 8 +- gvsbuild/projects/gtk.py | 5 +- gvsbuild/projects/gtkmm.py | 7 +- gvsbuild/projects/libffi.py | 2 +- gvsbuild/projects/libsigcplusplus.py | 4 +- gvsbuild/projects/pangomm.py | 4 +- gvsbuild/projects/pixman.py | 7 +- gvsbuild/projects/pycairo.py | 2 +- gvsbuild/projects/pygobject.py | 8 +- gvsbuild/utils/base_builders.py | 13 +- gvsbuild/utils/builder.py | 12 +- tests/test_base_builders.py | 154 +++++++++++++++++++++ tests/test_build.py | 52 +++++++ 21 files changed, 278 insertions(+), 35 deletions(-) create mode 100644 tests/test_base_builders.py diff --git a/gvsbuild/build.py b/gvsbuild/build.py index 2a7c726c2..3822268d3 100644 --- a/gvsbuild/build.py +++ b/gvsbuild/build.py @@ -54,6 +54,8 @@ def __get_projects_to_build(opts): if name == "all": for proj in Project.list_projects(): if proj.type == ProjectType.PROJECT: + for dep in proj.all_dependencies: + to_build.add(dep) to_build.add(proj) p = Project.get_project(name) if opts.deps: diff --git a/gvsbuild/projects/atkmm.py b/gvsbuild/projects/atkmm.py index b5a7632cb..203bb7922 100644 --- a/gvsbuild/projects/atkmm.py +++ b/gvsbuild/projects/atkmm.py @@ -35,5 +35,5 @@ def __init__(self): ) def build(self): - Meson.build(self, meson_params="-Dbuild-documentation=false") + Meson.build(self, meson_params=["-Dbuild-documentation=false"]) self.install(r".\COPYING share\doc\atkmm-1.6") diff --git a/gvsbuild/projects/cairomm.py b/gvsbuild/projects/cairomm.py index b0d2a6973..5d9ca93e8 100644 --- a/gvsbuild/projects/cairomm.py +++ b/gvsbuild/projects/cairomm.py @@ -34,7 +34,8 @@ def __init__(self): def build(self): Meson.build( - self, meson_params="-Dbuild-examples=false -Dbuild-documentation=false" + self, + meson_params=["-Dbuild-examples=false", "-Dbuild-documentation=false"], ) self.install(r".\COPYING share\doc\cairomm") @@ -54,6 +55,7 @@ def __init__(self): def build(self): Meson.build( - self, meson_params="-Dbuild-examples=false -Dbuild-documentation=false" + self, + meson_params=["-Dbuild-examples=false", "-Dbuild-documentation=false"], ) self.install(r".\COPYING share\doc\cairomm-1.0") diff --git a/gvsbuild/projects/directx_headers.py b/gvsbuild/projects/directx_headers.py index a1a64c1bf..555ec5b48 100644 --- a/gvsbuild/projects/directx_headers.py +++ b/gvsbuild/projects/directx_headers.py @@ -49,7 +49,7 @@ def __init__(self): ) def build(self): - Meson.build(self, meson_params="-Dbuild-test=false") + Meson.build(self, meson_params=["-Dbuild-test=false"]) self.install(r".\LICENSE share\doc\directx-headers") def post_install(self): diff --git a/gvsbuild/projects/fribidi.py b/gvsbuild/projects/fribidi.py index 6caa9ae8e..e48db7879 100644 --- a/gvsbuild/projects/fribidi.py +++ b/gvsbuild/projects/fribidi.py @@ -45,5 +45,5 @@ def __init__(self): ) def build(self): - Meson.build(self, meson_params="-Ddocs=false") + Meson.build(self, meson_params=["-Ddocs=false"]) self.install(r".\COPYING share\doc\fribidi") diff --git a/gvsbuild/projects/glib.py b/gvsbuild/projects/glib.py index 6f874eb05..73c80493f 100644 --- a/gvsbuild/projects/glib.py +++ b/gvsbuild/projects/glib.py @@ -52,7 +52,7 @@ def build(self): build_debug = ( "enabled" if self.builder.opts.configuration == "debug" else "disabled" ) - Meson.build(self, meson_params=f"-Dglib_debug={build_debug}") + Meson.build(self, meson_params=[f"-Dglib_debug={build_debug}"]) self.install(r".\LICENSES\* share\doc\glib") @@ -85,7 +85,7 @@ def build(self): build_debug = ( "enabled" if self.builder.opts.configuration == "debug" else "disabled" ) - Meson.build(self, meson_params=f"-Dglib_debug={build_debug}") + Meson.build(self, meson_params=[f"-Dglib_debug={build_debug}"]) @project_add @@ -112,7 +112,12 @@ def __init__(self): def build(self): Meson.build( - self, meson_params="-Dgnutls=disabled -Dopenssl=enabled -Dlibproxy=disabled" + self, + meson_params=[ + "-Dgnutls=disabled", + "-Dopenssl=enabled", + "-Dlibproxy=disabled", + ], ) self.install(r".\COPYING share\doc\glib-networking") self.install(r".\LICENSE_EXCEPTION share\doc\glib-networking") diff --git a/gvsbuild/projects/glibmm.py b/gvsbuild/projects/glibmm.py index 79eb6d748..e74e4d95a 100644 --- a/gvsbuild/projects/glibmm.py +++ b/gvsbuild/projects/glibmm.py @@ -41,7 +41,7 @@ def __init__(self): def build(self): Meson.build( self, - meson_params="-Dbuild-examples=false -Dbuild-documentation=false", + meson_params=["-Dbuild-examples=false", "-Dbuild-documentation=false"], ) self.install(r".\COPYING share\doc\glibmm") @@ -65,7 +65,8 @@ def __init__(self): def build(self): Meson.build( - self, meson_params="-Dbuild-examples=false -Dbuild-documentation=false" + self, + meson_params=["-Dbuild-examples=false", "-Dbuild-documentation=false"], ) self.install(r".\COPYING share\doc\glibmm-2.4") diff --git a/gvsbuild/projects/gobject_introspection.py b/gvsbuild/projects/gobject_introspection.py index ac252daff..7365a9af2 100644 --- a/gvsbuild/projects/gobject_introspection.py +++ b/gvsbuild/projects/gobject_introspection.py @@ -63,5 +63,8 @@ def build(self): Meson.build( self, - meson_params=f'-Dpython="{py_dir}\\python.exe" -Dcairo_libname=cairo-gobject-2.dll', + meson_params=[ + f"-Dpython={py_dir}\\python.exe", + "-Dcairo_libname=cairo-gobject-2.dll", + ], ) diff --git a/gvsbuild/projects/gstreamer.py b/gvsbuild/projects/gstreamer.py index ed271fed6..8641d01b8 100644 --- a/gvsbuild/projects/gstreamer.py +++ b/gvsbuild/projects/gstreamer.py @@ -57,7 +57,9 @@ def build(self): add_path = os.path.join(self.builder.opts.msys_dir, "usr", "bin") Meson.build( - self, add_path=add_path, meson_params="-Dtests=disabled -Dexamples=disabled" + self, + add_path=add_path, + meson_params=["-Dtests=disabled", "-Dexamples=disabled"], ) self.install(r".\COPYING share\doc\gstreamer") @@ -77,7 +79,7 @@ def __init__(self): ) def build(self): - Meson.build(self, meson_params="-Dbenchmarks=disabled -Dtools=enabled") + Meson.build(self, meson_params=["-Dbenchmarks=disabled", "-Dtools=enabled"]) self.install(r"COPYING share\doc\orc") @@ -112,7 +114,7 @@ def __init__(self): def build(self): Meson.build( - self, meson_params=f'-Dc_link_args="{self.builder.gtk_dir}\\lib\\ogg.lib"' + self, meson_params=[f"-Dc_link_args={self.builder.gtk_dir}\\lib\\ogg.lib"] ) self.install(r".\COPYING share\doc\gst-plugins-base") diff --git a/gvsbuild/projects/gtk.py b/gvsbuild/projects/gtk.py index 21ba13afc..8efd6e0e6 100644 --- a/gvsbuild/projects/gtk.py +++ b/gvsbuild/projects/gtk.py @@ -115,7 +115,10 @@ def __init__(self): self.add_param(f"-Dintrospection={enable_gi}") def build(self): - Meson.build(self, meson_params="-Dtests=false -Ddemos=false -Dexamples=false") + Meson.build( + self, + meson_params=["-Dtests=false", "-Ddemos=false", "-Dexamples=false"], + ) self.install(r".\COPYING share\doc\gtk3") diff --git a/gvsbuild/projects/gtkmm.py b/gvsbuild/projects/gtkmm.py index ae8227f1c..ee9d71c8e 100644 --- a/gvsbuild/projects/gtkmm.py +++ b/gvsbuild/projects/gtkmm.py @@ -44,7 +44,7 @@ def __init__(self): def build(self): Meson.build( self, - meson_params="-Dbuild-tests=false -Dbuild-demos=false", + meson_params=["-Dbuild-tests=false", "-Dbuild-demos=false"], ) self.install(r".\COPYING share\doc\gtkmm") @@ -75,6 +75,9 @@ def __init__(self): ) def build(self): - Meson.build(self, meson_params="-Dbuild-tests=false -Dbuild-demos=false") + Meson.build( + self, + meson_params=["-Dbuild-tests=false", "-Dbuild-demos=false"], + ) self.install(r".\COPYING share\doc\gtkmm-3.0") diff --git a/gvsbuild/projects/libffi.py b/gvsbuild/projects/libffi.py index 23d98d29e..a19337807 100644 --- a/gvsbuild/projects/libffi.py +++ b/gvsbuild/projects/libffi.py @@ -32,5 +32,5 @@ def __init__(self): ) def build(self): - Meson.build(self, meson_params="-Dtests=false") + Meson.build(self, meson_params=["-Dtests=false"]) self.install(r"LICENSE share\doc\libffi") diff --git a/gvsbuild/projects/libsigcplusplus.py b/gvsbuild/projects/libsigcplusplus.py index 21fc214f8..c572ddc26 100644 --- a/gvsbuild/projects/libsigcplusplus.py +++ b/gvsbuild/projects/libsigcplusplus.py @@ -38,7 +38,7 @@ def __init__(self): def build(self): Meson.build( self, - meson_params="-Dbuild-examples=false -Dbuild-documentation=false", + meson_params=["-Dbuild-examples=false", "-Dbuild-documentation=false"], ) self.install(r".\COPYING share\doc\libsigc++") @@ -66,7 +66,7 @@ def __init__(self): def build(self): Meson.build( self, - meson_params="-Dbuild-examples=false -Dbuild-documentation=false", + meson_params=["-Dbuild-examples=false", "-Dbuild-documentation=false"], ) self.install(r".\COPYING share\doc\libsigc++-2.0") diff --git a/gvsbuild/projects/pangomm.py b/gvsbuild/projects/pangomm.py index f6e80b7c2..5807abdb5 100644 --- a/gvsbuild/projects/pangomm.py +++ b/gvsbuild/projects/pangomm.py @@ -43,7 +43,7 @@ def __init__(self): def build(self): Meson.build( self, - meson_params="-Dbuild-documentation=false", + meson_params=["-Dbuild-documentation=false"], ) self.install(r".\COPYING share\doc\glibmm") @@ -74,7 +74,7 @@ def __init__(self): def build(self): Meson.build( self, - meson_params="-Dbuild-documentation=false", + meson_params=["-Dbuild-documentation=false"], ) self.install(r".\COPYING share\doc\pangomm-1.4") diff --git a/gvsbuild/projects/pixman.py b/gvsbuild/projects/pixman.py index 10ac52132..4e68eae50 100644 --- a/gvsbuild/projects/pixman.py +++ b/gvsbuild/projects/pixman.py @@ -35,7 +35,12 @@ def build(self): enable_mmx = "disabled" if self.builder.x64 else "enabled" Meson.build( self, - meson_params=f"-Dsse2=enabled -Dssse3=enabled -Dmmx={enable_mmx} -Dtests=disabled", + meson_params=[ + "-Dsse2=enabled", + "-Dssse3=enabled", + f"-Dmmx={enable_mmx}", + "-Dtests=disabled", + ], ) self.install(r".\COPYING share\doc\pixman") diff --git a/gvsbuild/projects/pycairo.py b/gvsbuild/projects/pycairo.py index 85fbff3d0..71cb0c445 100644 --- a/gvsbuild/projects/pycairo.py +++ b/gvsbuild/projects/pycairo.py @@ -35,7 +35,7 @@ def __init__(self): def build(self): py_dir = Path(sys.executable).parent - Meson.build(self, meson_params=f'-Dpython="{py_dir}\\python.exe"') + Meson.build(self, meson_params=[f"-Dpython={py_dir}\\python.exe"]) cairo_inc = Path(self.builder.gtk_dir) / "include" / "cairo" self.builder.mod_env("INCLUDE", str(cairo_inc)) python_exe = str(py_dir / "python.exe") diff --git a/gvsbuild/projects/pygobject.py b/gvsbuild/projects/pygobject.py index cecd5a3ea..5296fad0c 100644 --- a/gvsbuild/projects/pygobject.py +++ b/gvsbuild/projects/pygobject.py @@ -39,7 +39,13 @@ def __init__(self): def build(self): py_dir = Path(sys.executable).parent - Meson.build(self, meson_params=f'-Dpython="{py_dir}\\python.exe" -Dtests=false') + Meson.build( + self, + meson_params=[ + f"-Dpython={py_dir}\\python.exe", + "-Dtests=false", + ], + ) gtk_dir = self.builder.gtk_dir add_inc = [ str(Path(gtk_dir) / "include" / "cairo"), diff --git a/gvsbuild/utils/base_builders.py b/gvsbuild/utils/base_builders.py index 96f8cbb7f..2f85dd292 100644 --- a/gvsbuild/utils/base_builders.py +++ b/gvsbuild/utils/base_builders.py @@ -37,7 +37,12 @@ def add_param(self, par): self._ensure_params() self.params.append(par) - def build(self, meson_params=None, make_tests=False, add_path=None): + def build( + self, + meson_params: list[str] | None = None, + make_tests: bool = False, + add_path=None, + ): # where we build, with ninja, the library ninja_build = Path(self.build_dir) / "_gvsbuild-meson" @@ -54,7 +59,9 @@ def build(self, meson_params=None, make_tests=False, add_path=None): # running explicitly from the build dir self.builder.exec_ninja(params=["install"], working_dir=ninja_build) - def _setup_meson_and_ninja(self, ninja_build, meson_params, add_path): + def _setup_meson_and_ninja( + self, ninja_build, meson_params: list[str] | None, add_path + ): log.start_verbose("Generating meson directory") self.builder.make_dir(ninja_build) self._ensure_params() @@ -76,7 +83,7 @@ def _setup_meson_and_ninja(self, ninja_build, meson_params, add_path): ] cmd += self.params if meson_params: - cmd += meson_params.split() + cmd += meson_params if self.extra_opts: cmd += self.extra_opts diff --git a/gvsbuild/utils/builder.py b/gvsbuild/utils/builder.py index e92445f7b..fa931d614 100644 --- a/gvsbuild/utils/builder.py +++ b/gvsbuild/utils/builder.py @@ -998,14 +998,12 @@ def __resolve_executable(args, env): installed into vs_env['PATH'] (cmake, ninja, nmake, cargo, …) are found.""" if not isinstance(args, (list, tuple)) or not args: return args - first = os.fspath(args[0]) - if os.path.isabs(first) or os.sep in first or "/" in first: - return args # already qualified — let CreateProcess use it directly + first = Path(args[0]) + if first.parent != Path("."): + return args # absolute or path-qualified — pass through search_path = (env or os.environ).get("PATH") - resolved = shutil.which(first, path=search_path) - if resolved is None: - return args # let subprocess raise a clear error - return [resolved, *args[1:]] + resolved = shutil.which(first.name, path=search_path) + return [resolved, *args[1:]] if resolved else args def __execute(self, args, working_dir=None, add_path=None, env=None): log.debug(f"running {args}, cwd={working_dir}, path+={add_path}") diff --git a/tests/test_base_builders.py b/tests/test_base_builders.py new file mode 100644 index 000000000..d3a5171a3 --- /dev/null +++ b/tests/test_base_builders.py @@ -0,0 +1,154 @@ +# Copyright (C) 2026 The Gvsbuild Authors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, see . + +"""Tests for Meson._setup_meson_and_ninja command construction.""" + +import sys +from pathlib import Path +from unittest.mock import patch + +import pytest + +from gvsbuild.utils.base_builders import Meson +from gvsbuild.utils.simple_ui import log + + +@pytest.fixture(autouse=True) +def silence_log(mocker): + """Prevent log.start_verbose / log.end from touching uninitialised Log state.""" + mocker.patch.object(log, "start_verbose") + mocker.patch.object(log, "end") + + +@pytest.fixture +def meson_project(mocker, tmp_path): + """A minimal Meson project instance with all external dependencies mocked.""" + proj = Meson.__new__(Meson) + proj.params = [] + proj.extra_opts = None + proj.build_dir = str(tmp_path / "build") + + builder = mocker.Mock() + builder.opts.configuration = "release" + builder.opts.release_configuration_is_actually_debug_optimized = False + builder.gtk_dir = str(tmp_path / "gtk") + proj.builder = builder + + mocker.patch.object(proj, "_get_working_dir", return_value=str(tmp_path / "src")) + mocker.patch.object(proj, "exec_vs") + + return proj + + +def _captured_cmd(proj): + """Return the cmd list that was passed to exec_vs.""" + proj.exec_vs.assert_called_once() + return proj.exec_vs.call_args[0][0] + + +def test_meson_params_none_adds_nothing(meson_project, tmp_path): + """No extra args are appended when meson_params is None.""" + ninja_build = tmp_path / "ninja" + with patch( + "gvsbuild.utils.base_builders.Project.get_tool_executable", + return_value="meson.py", + ): + meson_project._setup_meson_and_ninja(ninja_build, None, None) + + cmd = _captured_cmd(meson_project) + assert cmd[0] == sys.executable + assert "meson.py" in cmd + assert "setup" in cmd + # python, meson.py, setup, src, ninja_build, --prefix, gtk_dir, --buildtype, + assert len(cmd) == 9 + + +def test_meson_params_empty_list_adds_nothing(meson_project, tmp_path): + """An empty list behaves the same as None.""" + ninja_build = tmp_path / "ninja" + with patch( + "gvsbuild.utils.base_builders.Project.get_tool_executable", + return_value="meson.py", + ): + meson_project._setup_meson_and_ninja(ninja_build, [], None) + + cmd = _captured_cmd(meson_project) + assert len(cmd) == 9 + + +def test_meson_params_single_flag(meson_project, tmp_path): + """A single-element list appends exactly that one flag.""" + ninja_build = tmp_path / "ninja" + with patch( + "gvsbuild.utils.base_builders.Project.get_tool_executable", + return_value="meson.py", + ): + meson_project._setup_meson_and_ninja(ninja_build, ["-Dtests=false"], None) + + cmd = _captured_cmd(meson_project) + assert "-Dtests=false" in cmd + assert len(cmd) == 10 + + +def test_meson_params_multiple_flags_order_preserved(meson_project, tmp_path): + """Multiple flags are appended in order.""" + ninja_build = tmp_path / "ninja" + with patch( + "gvsbuild.utils.base_builders.Project.get_tool_executable", + return_value="meson.py", + ): + meson_project._setup_meson_and_ninja(ninja_build, ["-Da=x", "-Db=y"], None) + + cmd = _captured_cmd(meson_project) + a_idx = cmd.index("-Da=x") + b_idx = cmd.index("-Db=y") + assert a_idx < b_idx + + +def test_meson_params_path_with_spaces_is_single_element(meson_project, tmp_path): + """A path containing spaces is passed as one argv element, not split.""" + ninja_build = tmp_path / "ninja" + path_with_spaces = r"C:\Users\my user\python.exe" + param = f"-Dpython={path_with_spaces}" + with patch( + "gvsbuild.utils.base_builders.Project.get_tool_executable", + return_value="meson.py", + ): + meson_project._setup_meson_and_ninja(ninja_build, [param], None) + + cmd = _captured_cmd(meson_project) + assert param in cmd + # The path must NOT have been word-split into multiple elements + assert path_with_spaces not in cmd + assert f"-Dpython={path_with_spaces.split()[0]}" not in cmd + + +def test_meson_params_no_embedded_quotes(meson_project, tmp_path): + """No embedded quote characters are present in the cmd elements.""" + ninja_build = tmp_path / "ninja" + python_path = str(Path(sys.executable).parent / "python.exe") + with patch( + "gvsbuild.utils.base_builders.Project.get_tool_executable", + return_value="meson.py", + ): + meson_project._setup_meson_and_ninja( + ninja_build, + [f"-Dpython={python_path}", "-Dcairo_libname=cairo-gobject-2.dll"], + None, + ) + + cmd = _captured_cmd(meson_project) + for element in cmd: + assert '"' not in str(element), f"Unexpected quote in cmd element: {element!r}" diff --git a/tests/test_build.py b/tests/test_build.py index edd9eb681..b04ef1694 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -116,3 +116,55 @@ def test_ninja_opts_validation_empty_value(app, runner): assert result.exit_code != 2 full_output = result.output + result.stderr assert "ninja-opts must start with a dash" not in full_output + + +def test_build_all_orders_dependencies_before_dependents(mocker): + """'build all' must emit each dependency before the project that needs it. + + Without topological ordering, projects are added in registration order. + A project registered first (e.g. 'a-app') would be built before a later + one it depends on (e.g. 'z-lib'), causing missing-binary failures at build + time (the historical gtk4-update-icon-cache issue is one such case). + """ + import gvsbuild.build as build_module + from gvsbuild.utils.base_project import Project, ProjectType + from gvsbuild.utils.utils import ordered_set + + # proj_a has no dependencies. + # proj_b depends on proj_a but is registered first (simulating alphabetical + # registration order where a project starting with "a" comes before "z"). + proj_a = mocker.Mock() + proj_a.name = "z-lib" + proj_a.type = ProjectType.PROJECT + proj_a.all_dependencies = ordered_set() + + proj_b = mocker.Mock() + proj_b.name = "a-app" + proj_b.type = ProjectType.PROJECT + b_deps = ordered_set() + b_deps.add(proj_a) + proj_b.all_dependencies = b_deps + + # Registration order: proj_b first — without dep-sorting, proj_b would be + # built before its dependency proj_a. + mocker.patch.object(Project, "list_projects", return_value=[proj_b, proj_a]) + + group_all = mocker.Mock() + group_all.all_dependencies = ordered_set() + mocker.patch.object(Project, "get_project", return_value=group_all) + + opts = mocker.Mock() + opts.projects = ["all"] + opts.deps = True + opts.skip = None + opts.clean_built = False + opts.enable_fips = False + + get_projects_fn = vars(build_module)["__get_projects_to_build"] + result = list(get_projects_fn(opts)) + + assert proj_a in result, "dependency must be present in the build list" + assert proj_b in result, "dependent must be present in the build list" + assert result.index(proj_a) < result.index(proj_b), ( + "proj_a (dependency of proj_b) must appear before proj_b in the build order" + ) From 788da29d185956b180b00e5c7add518546c49d5b Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Fri, 22 May 2026 21:56:00 -0400 Subject: [PATCH 6/8] Refactor cmake_params in to lists --- gvsbuild/patches/check-libs/meson.build | 12 --- gvsbuild/patches/check-libs/test_jasper.c | 11 --- gvsbuild/projects/abseil.py | 7 +- gvsbuild/projects/freerdp.py | 4 +- gvsbuild/projects/leveldb.py | 5 +- gvsbuild/projects/libarchive.py | 9 ++- gvsbuild/projects/libcurl.py | 2 +- gvsbuild/projects/libfido2.py | 27 ++++--- gvsbuild/projects/libpng.py | 7 +- gvsbuild/projects/libssh.py | 4 +- gvsbuild/projects/libyuv.py | 4 +- gvsbuild/projects/nuspell.py | 13 ++-- gvsbuild/projects/protobuf.py | 17 +++- gvsbuild/projects/win_iconv.py | 2 +- gvsbuild/utils/base_builders.py | 14 ++-- tests/test_base_builders.py | 95 ++++++++++++++++++++++- 16 files changed, 173 insertions(+), 60 deletions(-) delete mode 100644 gvsbuild/patches/check-libs/test_jasper.c diff --git a/gvsbuild/patches/check-libs/meson.build b/gvsbuild/patches/check-libs/meson.build index 9d23375c1..d5c1bf5d4 100644 --- a/gvsbuild/patches/check-libs/meson.build +++ b/gvsbuild/patches/check-libs/meson.build @@ -132,18 +132,6 @@ if glib_dep.found() test('glib', test_glib) endif -# jasper -jasper_required_ver = '>= 2.0.12' -jasper_dep = dependency('jasper', version: jasper_required_ver, required: check_all) -if jasper_dep.found() - test_jasper = executable('test_jasper', - [ 'test_jasper.c', 'check_utils.c', ], - dependencies: [ jasper_dep, ], - ) - - test('jasper', test_jasper) -endif - # json-glib json_glib_required_ver = '>= 1.4.2' json_glib_dep = dependency('json-glib-1.0', version: json_glib_required_ver, required: check_all) diff --git a/gvsbuild/patches/check-libs/test_jasper.c b/gvsbuild/patches/check-libs/test_jasper.c deleted file mode 100644 index 6c181e922..000000000 --- a/gvsbuild/patches/check-libs/test_jasper.c +++ /dev/null @@ -1,11 +0,0 @@ -// test for jasper - -// include - // standard - #include "check_utils.h" - // library - #include - -// check one function from the dll -CHECK_ONE(jas_init) - diff --git a/gvsbuild/projects/abseil.py b/gvsbuild/projects/abseil.py index 2d1350162..d151712df 100644 --- a/gvsbuild/projects/abseil.py +++ b/gvsbuild/projects/abseil.py @@ -38,6 +38,11 @@ def __init__(self): def build(self): CmakeProject.build( self, - cmake_params=r"-DBUILD_SHARED_LIBS=ON -DABSL_PROPAGATE_CXX_STD=ON -DCMAKE_CXX_STANDARD=17 -DCMAKE_CXX_STANDARD_REQUIRED=ON", + cmake_params=[ + "-DBUILD_SHARED_LIBS=ON", + "-DABSL_PROPAGATE_CXX_STD=ON", + "-DCMAKE_CXX_STANDARD=17", + "-DCMAKE_CXX_STANDARD_REQUIRED=ON", + ], use_ninja=True, ) diff --git a/gvsbuild/projects/freerdp.py b/gvsbuild/projects/freerdp.py index 68ed42980..ab74b3b22 100644 --- a/gvsbuild/projects/freerdp.py +++ b/gvsbuild/projects/freerdp.py @@ -42,7 +42,9 @@ def __init__(self): def build(self): CmakeProject.build( - self, use_ninja=True, cmake_params="-DWITH_SSE2=ON -DCHANNEL_URBDRC=OFF" + self, + use_ninja=True, + cmake_params=["-DWITH_SSE2=ON", "-DCHANNEL_URBDRC=OFF"], ) self.install(r".\LICENSE share\doc\freerdp") diff --git a/gvsbuild/projects/leveldb.py b/gvsbuild/projects/leveldb.py index 7fa1237f1..d3ad90f02 100644 --- a/gvsbuild/projects/leveldb.py +++ b/gvsbuild/projects/leveldb.py @@ -38,7 +38,10 @@ def build(self): CmakeProject.build( self, use_ninja=True, - cmake_params="-DLEVELDB_BUILD_TESTS=OFF -DLEVELDB_BUILD_BENCHMARKS=OFF", + cmake_params=[ + "-DLEVELDB_BUILD_TESTS=OFF", + "-DLEVELDB_BUILD_BENCHMARKS=OFF", + ], ) self.install(r".\LICENSE share\doc\leveldb") diff --git a/gvsbuild/projects/libarchive.py b/gvsbuild/projects/libarchive.py index 981b56bfe..70be04809 100644 --- a/gvsbuild/projects/libarchive.py +++ b/gvsbuild/projects/libarchive.py @@ -44,9 +44,12 @@ def __init__(self): ) def build(self): - cmake_params = ( - "-DENABLE_WERROR=OFF -DENABLE_TEST=OFF -DBUILD_TESTING=OFF -Wno-dev" - ) + cmake_params = [ + "-DENABLE_WERROR=OFF", + "-DENABLE_TEST=OFF", + "-DBUILD_TESTING=OFF", + "-Wno-dev", + ] CmakeProject.build(self, cmake_params=cmake_params, use_ninja=True) # Fix the pkg-config .pc file, correcting the library's names file_replace( diff --git a/gvsbuild/projects/libcurl.py b/gvsbuild/projects/libcurl.py index e471d859d..766f71d7e 100644 --- a/gvsbuild/projects/libcurl.py +++ b/gvsbuild/projects/libcurl.py @@ -40,7 +40,7 @@ def __init__(self): ) def build(self): - CmakeProject.build(self, use_ninja=True, cmake_params="-DUSE_WIN32_IDN=ON") + CmakeProject.build(self, use_ninja=True, cmake_params=["-DUSE_WIN32_IDN=ON"]) # Fix the pkg-config .pc file, correcting the library's names file_replace( os.path.join(self.pkg_dir, "lib", "pkgconfig", "libcurl.pc"), diff --git a/gvsbuild/projects/libfido2.py b/gvsbuild/projects/libfido2.py index 9591497cf..59cf4723c 100644 --- a/gvsbuild/projects/libfido2.py +++ b/gvsbuild/projects/libfido2.py @@ -52,15 +52,24 @@ def build(self): lib_dirs = os.path.join(self.builder.gtk_dir, "lib") bin_dirs = lib_dirs = os.path.join(self.builder.gtk_dir, "bin") - build_params = "-DBUILD_EXAMPLES=OFF -DBUILD_MANPAGES=OFF -DBUILD_TESTS=OFF -DBUILD_TOOLS=OFF -DBUILD_STATIC_LIBS=OFF" - cmake_params = ( - f'-DWITH_ZLIB=ON -DCBOR_INCLUDE_DIRS="{include_dirs}" ' - f'-DCRYPTO_INCLUDE_DIRS="{include_dirs}" -DZLIB_INCLUDE_DIRS="{include_dirs}" ' - f'-DCBOR_LIBRARY_DIRS="{lib_dirs}" -DCRYPTO_LIBRARY_DIRS="{lib_dirs}" ' - f'-DZLIB_LIBRARY_DIRS="{lib_dirs}" -DCBOR_BIN_DIRS="{bin_dirs}" ' - f'-DCRYPTO_BIN_DIRS="{bin_dirs}" -DZLIB_BIN_DIRS="{bin_dirs}" ' - f"-DCRYPTO_LIBRARIES=libcrypto {build_params}" - ) + cmake_params = [ + "-DWITH_ZLIB=ON", + f"-DCBOR_INCLUDE_DIRS={include_dirs}", + f"-DCRYPTO_INCLUDE_DIRS={include_dirs}", + f"-DZLIB_INCLUDE_DIRS={include_dirs}", + f"-DCBOR_LIBRARY_DIRS={lib_dirs}", + f"-DCRYPTO_LIBRARY_DIRS={lib_dirs}", + f"-DZLIB_LIBRARY_DIRS={lib_dirs}", + f"-DCBOR_BIN_DIRS={bin_dirs}", + f"-DCRYPTO_BIN_DIRS={bin_dirs}", + f"-DZLIB_BIN_DIRS={bin_dirs}", + "-DCRYPTO_LIBRARIES=libcrypto", + "-DBUILD_EXAMPLES=OFF", + "-DBUILD_MANPAGES=OFF", + "-DBUILD_TESTS=OFF", + "-DBUILD_TOOLS=OFF", + "-DBUILD_STATIC_LIBS=OFF", + ] CmakeProject.build(self, cmake_params=cmake_params, use_ninja=True) self.install(rf"output\{arch}\static\* .") diff --git a/gvsbuild/projects/libpng.py b/gvsbuild/projects/libpng.py index e54e5b524..0fe29da59 100644 --- a/gvsbuild/projects/libpng.py +++ b/gvsbuild/projects/libpng.py @@ -33,7 +33,12 @@ def __init__(self): ) def build(self): - cmake_params = '-DPNG_TOOLS=OFF -DPNG_TESTS=OFF -Dld-version-script=OFF -DPNG_DEBUG_POSTFIX=""' + cmake_params = [ + "-DPNG_TOOLS=OFF", + "-DPNG_TESTS=OFF", + "-Dld-version-script=OFF", + "-DPNG_DEBUG_POSTFIX=", + ] CmakeProject.build(self, cmake_params=cmake_params, use_ninja=True) self.install_pc_files() diff --git a/gvsbuild/projects/libssh.py b/gvsbuild/projects/libssh.py index be0217ab8..2f7e6eaaa 100644 --- a/gvsbuild/projects/libssh.py +++ b/gvsbuild/projects/libssh.py @@ -32,7 +32,7 @@ def __init__(self): ) def build(self): - CmakeProject.build(self, cmake_params="-DWITH_ZLIB=ON", use_ninja=True) + CmakeProject.build(self, cmake_params=["-DWITH_ZLIB=ON"], use_ninja=True) self.install(r".\COPYING share\doc\libssh") @@ -53,5 +53,5 @@ def __init__(self): ) def build(self): - CmakeProject.build(self, cmake_params="-DWITH_ZLIB=ON", use_ninja=True) + CmakeProject.build(self, cmake_params=["-DWITH_ZLIB=ON"], use_ninja=True) self.install(r".\COPYING share\doc\libssh2") diff --git a/gvsbuild/projects/libyuv.py b/gvsbuild/projects/libyuv.py index dc412cbb6..5534fec9d 100644 --- a/gvsbuild/projects/libyuv.py +++ b/gvsbuild/projects/libyuv.py @@ -40,7 +40,9 @@ def __init__(self): def build(self): CmakeProject.build( - self, cmake_params=r"-DCMAKE_POLICY_VERSION_MINIMUM=3.5", use_ninja=False + self, + cmake_params=["-DCMAKE_POLICY_VERSION_MINIMUM=3.5"], + use_ninja=False, ) self.install_pc_files() diff --git a/gvsbuild/projects/nuspell.py b/gvsbuild/projects/nuspell.py index d70d8759c..baad3b9ce 100644 --- a/gvsbuild/projects/nuspell.py +++ b/gvsbuild/projects/nuspell.py @@ -33,11 +33,14 @@ def __init__(self): ) def build(self): - cmake_params = ( - f'-DCMAKE_INSTALL_PREFIX="{self.builder.gtk_dir}" ' - "-DBUILD_SHARED_LIBS=ON -DBUILD_TESTING=OFF -DBUILD_DOCS=OFF " - f'-DBUILD_TOOLS=OFF -DICU_ROOT="{self.builder.gtk_dir}"' - ) + cmake_params = [ + f"-DCMAKE_INSTALL_PREFIX={self.builder.gtk_dir}", + "-DBUILD_SHARED_LIBS=ON", + "-DBUILD_TESTING=OFF", + "-DBUILD_DOCS=OFF", + "-DBUILD_TOOLS=OFF", + f"-DICU_ROOT={self.builder.gtk_dir}", + ] CmakeProject.build(self, use_ninja=True, cmake_params=cmake_params) self.install(r".\COPYING*", r".\share\doc\nuspell") diff --git a/gvsbuild/projects/protobuf.py b/gvsbuild/projects/protobuf.py index 87a00d60f..842dded9c 100644 --- a/gvsbuild/projects/protobuf.py +++ b/gvsbuild/projects/protobuf.py @@ -40,7 +40,16 @@ def build(self): # We need to compile with STATIC_RUNTIME off since protobuf-c also compiles with it OFF CmakeProject.build( self, - cmake_params=r'-DBUILD_SHARED_LIBS=ON -Dprotobuf_ABSL_PROVIDER=package -Dprotobuf_DEBUG_POSTFIX="" -Dprotobuf_BUILD_TESTS=OFF -Dprotobuf_WITH_ZLIB=ON -Dprotobuf_MSVC_STATIC_RUNTIME=OFF -DCMAKE_CXX_STANDARD=17 -DCMAKE_CXX_STANDARD_REQUIRED=ON', + cmake_params=[ + "-DBUILD_SHARED_LIBS=ON", + "-Dprotobuf_ABSL_PROVIDER=package", + "-Dprotobuf_DEBUG_POSTFIX=", + "-Dprotobuf_BUILD_TESTS=OFF", + "-Dprotobuf_WITH_ZLIB=ON", + "-Dprotobuf_MSVC_STATIC_RUNTIME=OFF", + "-DCMAKE_CXX_STANDARD=17", + "-DCMAKE_CXX_STANDARD_REQUIRED=ON", + ], use_ninja=True, ) @@ -71,7 +80,11 @@ def __init__(self): def build(self): CmakeProject.build( self, - cmake_params="-DBUILD_SHARED_LIBS=ON -DCMAKE_CXX_STANDARD=17 -DCMAKE_CXX_STANDARD_REQUIRED=ON", + cmake_params=[ + "-DBUILD_SHARED_LIBS=ON", + "-DCMAKE_CXX_STANDARD=17", + "-DCMAKE_CXX_STANDARD_REQUIRED=ON", + ], use_ninja=True, source_part="build-cmake", ) diff --git a/gvsbuild/projects/win_iconv.py b/gvsbuild/projects/win_iconv.py index d7e1b2722..acae5eae9 100644 --- a/gvsbuild/projects/win_iconv.py +++ b/gvsbuild/projects/win_iconv.py @@ -39,6 +39,6 @@ def __init__(self): ) def build(self): - CmakeProject.build(self, use_ninja=True, cmake_params="-DBUILD_TEST=1") + CmakeProject.build(self, use_ninja=True, cmake_params=["-DBUILD_TEST=1"]) self.install(r".\COPYING share\doc\win-iconv") diff --git a/gvsbuild/utils/base_builders.py b/gvsbuild/utils/base_builders.py index 2f85dd292..d4c58ba45 100644 --- a/gvsbuild/utils/base_builders.py +++ b/gvsbuild/utils/base_builders.py @@ -98,12 +98,12 @@ def __init__(self, name, **kwargs): def build( self, - cmake_params=None, - use_ninja=False, - make_tests=False, - do_install=True, - out_of_source=None, - source_part=None, + cmake_params: list[str] | None = None, + use_ninja: bool = False, + make_tests: bool = False, + do_install: bool = True, + out_of_source: bool | None = None, + source_part: str | None = None, ): cmake_gen = "Ninja" if use_ninja else "NMake Makefiles" @@ -122,7 +122,7 @@ def build( f"-DCMAKE_BUILD_TYPE={cmake_config}", ] if cmake_params: - cmd += cmake_params.split() + cmd += cmake_params if self.extra_opts: cmd += self.extra_opts if use_ninja and out_of_source is None: diff --git a/tests/test_base_builders.py b/tests/test_base_builders.py index d3a5171a3..e6d085fe5 100644 --- a/tests/test_base_builders.py +++ b/tests/test_base_builders.py @@ -13,7 +13,7 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see . -"""Tests for Meson._setup_meson_and_ninja command construction.""" +"""Tests for Meson._setup_meson_and_ninja and CmakeProject.build command construction.""" import sys from pathlib import Path @@ -21,7 +21,7 @@ import pytest -from gvsbuild.utils.base_builders import Meson +from gvsbuild.utils.base_builders import CmakeProject, Meson from gvsbuild.utils.simple_ui import log @@ -135,6 +135,97 @@ def test_meson_params_path_with_spaces_is_single_element(meson_project, tmp_path assert f"-Dpython={path_with_spaces.split()[0]}" not in cmd +@pytest.fixture +def cmake_project(mocker, tmp_path): + """A minimal CmakeProject instance with all external dependencies mocked.""" + proj = CmakeProject.__new__(CmakeProject) + proj.params = [] + proj.extra_opts = None + proj.build_dir = str(tmp_path / "build") + proj.pkg_dir = str(tmp_path / "pkg") + + builder = mocker.Mock() + builder.opts.configuration = "release" + builder.opts.release_configuration_is_actually_debug_optimized = False + builder.gtk_dir = str(tmp_path / "gtk") + proj.builder = builder + + mocker.patch.object(proj, "_get_working_dir", return_value=str(tmp_path / "src")) + + return proj + + +def _captured_cmake_cmd(proj): + """Return the cmd list from the first builder.exec_vs call (the cmake configure step).""" + assert proj.builder.exec_vs.call_count >= 1 + return proj.builder.exec_vs.call_args_list[0][0][0] + + +def test_cmake_params_none_adds_nothing(cmake_project): + """No extra args are appended when cmake_params is None.""" + cmake_project.build(cmake_params=None, use_ninja=False, do_install=False) + + cmd = _captured_cmake_cmd(cmake_project) + assert cmd[0] == "cmake" + # base cmd: cmake, -G, , -DCMAKE_INSTALL_PREFIX=..., -DGTK_DIR=..., -DCMAKE_BUILD_TYPE=... + assert len(cmd) == 6 + + +def test_cmake_params_empty_list_adds_nothing(cmake_project): + """An empty list behaves the same as None.""" + cmake_project.build(cmake_params=[], use_ninja=False, do_install=False) + + cmd = _captured_cmake_cmd(cmake_project) + assert len(cmd) == 6 + + +def test_cmake_params_single_flag(cmake_project): + """A single-element list appends exactly that one flag.""" + cmake_project.build( + cmake_params=["-DBUILD_TESTS=OFF"], use_ninja=False, do_install=False + ) + + cmd = _captured_cmake_cmd(cmake_project) + assert "-DBUILD_TESTS=OFF" in cmd + assert len(cmd) == 7 + + +def test_cmake_params_multiple_flags_order_preserved(cmake_project): + """Multiple flags are appended in order.""" + cmake_project.build( + cmake_params=["-Da=x", "-Db=y", "-Dc=z"], use_ninja=False, do_install=False + ) + + cmd = _captured_cmake_cmd(cmake_project) + a_idx = cmd.index("-Da=x") + b_idx = cmd.index("-Db=y") + c_idx = cmd.index("-Dc=z") + assert a_idx < b_idx < c_idx + + +def test_cmake_params_path_with_spaces_is_single_element(cmake_project, tmp_path): + """A path containing spaces is passed as one argv element, not split.""" + path_with_spaces = r"C:\Users\my user\include" + param = f"-DFOO_INCLUDE_DIRS={path_with_spaces}" + cmake_project.build(cmake_params=[param], use_ninja=False, do_install=False) + + cmd = _captured_cmake_cmd(cmake_project) + assert param in cmd + assert path_with_spaces not in cmd + assert f"-DFOO_INCLUDE_DIRS={path_with_spaces.split()[0]}" not in cmd + + +def test_cmake_params_no_embedded_quotes(cmake_project, tmp_path): + """No embedded quote characters appear in the cmd elements.""" + lib_dir = str(tmp_path / "lib") + params = [f"-DFOO_LIB_DIRS={lib_dir}", "-DBUILD_SHARED_LIBS=ON"] + cmake_project.build(cmake_params=params, use_ninja=False, do_install=False) + + cmd = _captured_cmake_cmd(cmake_project) + for element in cmd: + assert '"' not in str(element), f"Unexpected quote in cmd element: {element!r}" + + def test_meson_params_no_embedded_quotes(meson_project, tmp_path): """No embedded quote characters are present in the cmd elements.""" ninja_build = tmp_path / "ninja" From 8e0bf8856e3225830f6cedf7ee5a49024871474b Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Fri, 22 May 2026 21:59:26 -0400 Subject: [PATCH 7/8] Fix resolve executable is falsy env --- gvsbuild/utils/builder.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gvsbuild/utils/builder.py b/gvsbuild/utils/builder.py index fa931d614..6d523da99 100644 --- a/gvsbuild/utils/builder.py +++ b/gvsbuild/utils/builder.py @@ -995,13 +995,17 @@ def exec_msys(self, args, working_dir=None): def __resolve_executable(args, env): """On Windows, CreateProcess does not use env['PATH'] for bare-name lookup. Resolve the first argument to a full path via shutil.which so that tools - installed into vs_env['PATH'] (cmake, ninja, nmake, cargo, …) are found.""" + installed into vs_env['PATH'] (cmake, ninja, nmake, cargo, …) are found. + When env is None the subprocess inherits the parent process environment and + CreateProcess finds bare names normally, so no resolution is needed.""" if not isinstance(args, (list, tuple)) or not args: return args + if env is None: + return args # subprocess inherits parent env and no custom PATH to search first = Path(args[0]) if first.parent != Path("."): return args # absolute or path-qualified — pass through - search_path = (env or os.environ).get("PATH") + search_path = env.get("PATH") resolved = shutil.which(first.name, path=search_path) return [resolved, *args[1:]] if resolved else args From b44fe0b1cb6bfd418c696bac38843c6b6960e6d9 Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Fri, 22 May 2026 22:44:20 -0400 Subject: [PATCH 8/8] Add py314t and py315 to build matrix --- gvsbuild/projects/check_libs.py | 1 + pyproject.toml | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/gvsbuild/projects/check_libs.py b/gvsbuild/projects/check_libs.py index 9907db15f..679585266 100644 --- a/gvsbuild/projects/check_libs.py +++ b/gvsbuild/projects/check_libs.py @@ -46,6 +46,7 @@ def __init__(self): "libyuv", "pango", "zlib", + "wing", ], version="0.1.0", internal=True, diff --git a/pyproject.toml b/pyproject.toml index bfd0b406f..bf4ea000c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ ignore_missing_imports = true legacy_tox_ini = """ [tox] isolated_build = true -envlist = py{310,311,312,313,314} +envlist = py{310,311,312,313,314,314t, 315} [gh-actions] python = @@ -68,6 +68,8 @@ python = 3.12: py312 3.13: py313 3.14: py314 + 3.14t: py314t + 3.15: py315 [testenv] commands = pytest