From b418a3b68a4c48a008a31a9c9b0606162f26870d Mon Sep 17 00:00:00 2001 From: Bart Dubbeldam Date: Sat, 21 Feb 2026 00:09:11 +0100 Subject: [PATCH 1/4] Fix platform-agnostic native artifact check in build backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The build backend checked for native artifacts using `any()` across all platforms (win32, darwin, linux), so a stale artifact from a different OS (e.g. `libkson.dylib` left over on a Linux builder) would skip the Gradle build, leaving the actually-needed native library uncompiled. Additionally, the artifact check included `kson_api.h` which is not produced by the build—the Gradle `CopyNativeArtifactsTask` produces `jni_simplified.h` and the runtime loads `jni_simplified.h`. This would have caused fresh wheel builds to always trigger a rebuild attempt. Finally, when both artifacts and `kson-sdist` were absent, the function silently returned, producing broken wheels with no error. Fixes: - Check only the native library for *this* platform, plus `jni_simplified.h` (the header actually produced and loaded at runtime) - Add a post-condition that errors clearly when required artifacts are missing, guiding users to install from a pre-built wheel or ensure a JDK is available - Use `subprocess.run(cwd=...)` instead of `os.chdir` for thread safety - Log both stdout and stderr on Gradle build failure for easier debugging - Exclude platform-specific native binaries from `prepareSdistBuildEnvironment` in build.gradle.kts so sdists don't bundle stale cross-platform artifacts Fixes #307 --- lib-python/build.gradle.kts | 9 +- lib-python/build_backend.py | 110 ++++++++++++-------- lib-python/pyproject.toml | 4 + lib-python/tests/test_build_backend.py | 137 +++++++++++++++++++++++++ lib-python/uv.lock | 23 +++-- 5 files changed, 228 insertions(+), 55 deletions(-) create mode 100644 lib-python/tests/test_build_backend.py diff --git a/lib-python/build.gradle.kts b/lib-python/build.gradle.kts index d15d13f6..8049707d 100644 --- a/lib-python/build.gradle.kts +++ b/lib-python/build.gradle.kts @@ -105,11 +105,16 @@ tasks { exclude(".gradle/**") } - // Copy lib-python (build files and source) + // Copy lib-python (build files and source, excluding native binaries + // which are platform-specific and must be built from source) + // Keep in sync with PLATFORM_NATIVE_LIBRARIES in build_backend.py from(project.projectDir) { into("lib-python") include("build.gradle.kts") include("src/**") + exclude("src/kson/kson.dll") + exclude("src/kson/libkson.dylib") + exclude("src/kson/libkson.so") } } @@ -138,7 +143,7 @@ tasks { environment("CIBW_SKIP", "*-musllinux_*") // Skip musl Linux builds environment("CIBW_ARCHS", "native") // Build only for native architecture environment("CIBW_TEST_REQUIRES", "pytest") // Install pytest for testing - environment("CIBW_TEST_COMMAND", "pytest -v {project}/tests") + environment("CIBW_TEST_COMMAND", "pytest -v {project}/tests --ignore={project}/tests/test_build_backend.py") doLast { println("Successfully built platform-specific wheel using cibuildwheel") diff --git a/lib-python/build_backend.py b/lib-python/build_backend.py index 46b50dd6..ee306975 100644 --- a/lib-python/build_backend.py +++ b/lib-python/build_backend.py @@ -6,10 +6,31 @@ import os import subprocess import shutil +import sys from pathlib import Path from setuptools import build_meta as _orig from setuptools.build_meta import * +# See also LIBRARY_NAMES in src/kson/__init__.py +PLATFORM_NATIVE_LIBRARIES = { + "win32": "kson.dll", + "darwin": "libkson.dylib", + "linux": "libkson.so", +} + + +def _platform_native_library(): + """Return the native library filename for the current platform.""" + lib = PLATFORM_NATIVE_LIBRARIES.get(sys.platform) + if lib is None: + raise RuntimeError(f"Unsupported platform: {sys.platform}") + return lib + + +def _required_artifacts(): + """Return the filenames that must be present for a working installation.""" + return [_platform_native_library(), "jni_simplified.h"] + def _ensure_native_artifacts(): """Build native artifacts using the bundled Gradle setup.""" @@ -18,55 +39,60 @@ def _ensure_native_artifacts(): src_dir = lib_python_dir / "src" src_kson_dir = src_dir / "kson" - # Check if native artifacts already exist - native_files = ["kson.dll", "libkson.dylib", "libkson.so", "kson_api.h"] - artifacts_exist = any((src_kson_dir / f).exists() for f in native_files) + # Only check for the native library needed on *this* platform + required = _required_artifacts() + artifacts_exist = all((src_kson_dir / f).exists() for f in required) if not artifacts_exist and kson_copy_dir.exists(): - print("Building native artifacts with bundled Gradle setup...") - # Run gradle from the kson-sdist directory - original_dir = os.getcwd() - try: - os.chdir(kson_copy_dir) - - # Run the Gradle build - gradlew = "./gradlew" if os.name != "nt" else "gradlew.bat" - result = subprocess.run( - [gradlew, "lib-python:build"], capture_output=True, text=True - ) - - if result.returncode != 0: - print(f"Gradle build failed:\n{result.stderr}") - raise RuntimeError("Failed to build native artifacts") - - print("Native artifacts built successfully") - - # Replace the entire src directory with the one from kson-sdist - kson_copy_src = kson_copy_dir / "lib-python" / "src" - if kson_copy_src.exists(): - print("Replacing src directory with built artifacts...") - # Save _marker.c if it exists - marker_c = src_kson_dir / "_marker.c" - marker_c_content = None - if marker_c.exists(): - marker_c_content = marker_c.read_bytes() - - shutil.rmtree(src_dir, ignore_errors=True) - shutil.copytree(kson_copy_src, src_dir) - - # Restore _marker.c if it existed - if marker_c_content is not None: - marker_c_new = src_kson_dir / "_marker.c" - marker_c_new.parent.mkdir(parents=True, exist_ok=True) - marker_c_new.write_bytes(marker_c_content) - - finally: - os.chdir(original_dir) + print(f"Building native artifacts for {sys.platform} with bundled Gradle setup...") + + gradlew = "./gradlew" if os.name != "nt" else "gradlew.bat" + result = subprocess.run( + [gradlew, "lib-python:build"], + capture_output=True, + text=True, + cwd=kson_copy_dir, + ) + + if result.returncode != 0: + print(f"Gradle build stdout:\n{result.stdout}") + print(f"Gradle build stderr:\n{result.stderr}") + raise RuntimeError("Failed to build native artifacts") + + print("Native artifacts built successfully") + + # Replace the entire src directory with the one from kson-sdist + kson_copy_src = kson_copy_dir / "lib-python" / "src" + if kson_copy_src.exists(): + print("Replacing src directory with built artifacts...") + # Save _marker.c if it exists + marker_c = src_kson_dir / "_marker.c" + marker_c_content = None + if marker_c.exists(): + marker_c_content = marker_c.read_bytes() + + shutil.rmtree(src_dir, ignore_errors=True) + shutil.copytree(kson_copy_src, src_dir) + + # Restore _marker.c if it existed + if marker_c_content is not None: + marker_c_new = src_kson_dir / "_marker.c" + marker_c_new.parent.mkdir(parents=True, exist_ok=True) + marker_c_new.write_bytes(marker_c_content) # Clean up kson-sdist after successful build print("Cleaning up build files...") shutil.rmtree(kson_copy_dir, ignore_errors=True) + # Post-condition: verify all required artifacts are present + missing = [f for f in required if not (src_kson_dir / f).exists()] + if missing: + raise RuntimeError( + f"Required native artifacts missing for {sys.platform}: {', '.join(missing)}. " + f"Install from a pre-built wheel instead, or ensure a JDK is available " + f"so the Gradle build can produce them." + ) + def build_sdist(sdist_directory, config_settings=None): """Build source distribution.""" diff --git a/lib-python/pyproject.toml b/lib-python/pyproject.toml index 6fceaa8b..6d5c8127 100644 --- a/lib-python/pyproject.toml +++ b/lib-python/pyproject.toml @@ -48,11 +48,15 @@ package-dir = {"" = "src"} [tool.setuptools.package-data] kson = ["kson_api.h", "kson.dll", "libkson.dylib", "libkson.so", "jni_simplified.h"] +[tool.pytest.ini_options] +pythonpath = ["."] + [dependency-groups] dev = [ "pyright>=1.1.403", "pytest>=8.4.1", "ruff>=0.12.8", + "setuptools>=61.0", "cibuildwheel>=2.16.0", "twine>=6.2.0", ] diff --git a/lib-python/tests/test_build_backend.py b/lib-python/tests/test_build_backend.py new file mode 100644 index 00000000..50d9ac83 --- /dev/null +++ b/lib-python/tests/test_build_backend.py @@ -0,0 +1,137 @@ +"""Tests for the custom build backend's platform-aware native artifact detection.""" + +import subprocess +import sys +from unittest.mock import patch + +import pytest + +import build_backend + + +class TestPlatformNativeLibrary: + def test_darwin(self): + with patch.object(sys, "platform", "darwin"): + assert build_backend._platform_native_library() == "libkson.dylib" + + def test_linux(self): + with patch.object(sys, "platform", "linux"): + assert build_backend._platform_native_library() == "libkson.so" + + def test_windows(self): + with patch.object(sys, "platform", "win32"): + assert build_backend._platform_native_library() == "kson.dll" + + def test_unsupported_platform(self): + with patch.object(sys, "platform", "freebsd"): + with pytest.raises(RuntimeError, match="Unsupported platform: freebsd"): + build_backend._platform_native_library() + + +class TestEnsureNativeArtifacts: + """Verify that _ensure_native_artifacts only considers the current platform's library.""" + + def _place_all_required_artifacts(self, directory): + """Place all required artifacts for the current platform.""" + for f in build_backend._required_artifacts(): + (directory / f).touch() + + def test_skips_build_when_current_platform_artifacts_exist(self, tmp_path): + """Build is skipped when all required artifacts for *this* platform are present.""" + src_kson = tmp_path / "src" / "kson" + src_kson.mkdir(parents=True) + kson_sdist = tmp_path / "kson-sdist" + kson_sdist.mkdir() + + self._place_all_required_artifacts(src_kson) + + with patch.object(build_backend, "__file__", str(tmp_path / "build_backend.py")): + with patch.object(build_backend.subprocess, "run") as mock_run: + build_backend._ensure_native_artifacts() + mock_run.assert_not_called() + + def test_triggers_build_when_only_foreign_artifact_exists(self, tmp_path): + """Build is triggered when only a *different* platform's library is present.""" + src_kson = tmp_path / "src" / "kson" + src_kson.mkdir(parents=True) + kson_sdist = tmp_path / "kson-sdist" + kson_sdist.mkdir() + + # Place foreign platform libraries (but not the current one) + current_lib = build_backend._platform_native_library() + for lib in build_backend.PLATFORM_NATIVE_LIBRARIES.values(): + if lib != current_lib: + (src_kson / lib).touch() + (src_kson / "jni_simplified.h").touch() + + with patch.object(build_backend, "__file__", str(tmp_path / "build_backend.py")): + with patch.object(build_backend.subprocess, "run") as mock_run: + mock_run.return_value = subprocess.CompletedProcess( + args=[], returncode=1, stdout="", stderr="fail", + ) + with pytest.raises(RuntimeError, match="Failed to build native artifacts"): + build_backend._ensure_native_artifacts() + mock_run.assert_called_once() + + def test_triggers_build_when_header_missing(self, tmp_path): + """Build is triggered when the native lib exists but jni_simplified.h is missing.""" + src_kson = tmp_path / "src" / "kson" + src_kson.mkdir(parents=True) + kson_sdist = tmp_path / "kson-sdist" + kson_sdist.mkdir() + + current_lib = build_backend._platform_native_library() + (src_kson / current_lib).touch() + + with patch.object(build_backend, "__file__", str(tmp_path / "build_backend.py")): + with patch.object(build_backend.subprocess, "run") as mock_run: + mock_run.return_value = subprocess.CompletedProcess( + args=[], returncode=1, stdout="", stderr="fail", + ) + with pytest.raises(RuntimeError, match="Failed to build native artifacts"): + build_backend._ensure_native_artifacts() + mock_run.assert_called_once() + + def test_errors_when_artifacts_missing_and_no_kson_sdist(self, tmp_path): + """Raises when artifacts are missing and there's no kson-sdist to build from.""" + src_kson = tmp_path / "src" / "kson" + src_kson.mkdir(parents=True) + + with patch.object(build_backend, "__file__", str(tmp_path / "build_backend.py")): + with pytest.raises(RuntimeError, match="Required native artifacts missing"): + build_backend._ensure_native_artifacts() + + def test_successful_build_replaces_src_and_preserves_marker(self, tmp_path): + """On successful build, src is replaced with kson-sdist output and _marker.c is preserved.""" + src_kson = tmp_path / "src" / "kson" + src_kson.mkdir(parents=True) + kson_sdist = tmp_path / "kson-sdist" + + # Set up the kson-sdist build output that Gradle would produce + native_lib = build_backend._platform_native_library() + kson_sdist_src = kson_sdist / "lib-python" / "src" / "kson" + kson_sdist_src.mkdir(parents=True) + (kson_sdist_src / "__init__.py").write_text("# built") + (kson_sdist_src / native_lib).write_bytes(b"built-lib") + (kson_sdist_src / "jni_simplified.h").write_text("/* header */") + + # Place _marker.c in the original src (should be preserved) + marker_content = b"/* platform marker */" + (src_kson / "_marker.c").write_bytes(marker_content) + + with patch.object(build_backend, "__file__", str(tmp_path / "build_backend.py")): + with patch.object(build_backend.subprocess, "run") as mock_run: + mock_run.return_value = subprocess.CompletedProcess( + args=[], returncode=0, stdout="", stderr="", + ) + build_backend._ensure_native_artifacts() + + # src was replaced with kson-sdist output + assert (tmp_path / "src" / "kson" / "__init__.py").read_text() == "# built" + assert (tmp_path / "src" / "kson" / native_lib).read_bytes() == b"built-lib" + + # _marker.c was preserved + assert (tmp_path / "src" / "kson" / "_marker.c").read_bytes() == marker_content + + # kson-sdist was cleaned up + assert not kson_sdist.exists() diff --git a/lib-python/uv.lock b/lib-python/uv.lock index 0dfc977f..07337615 100644 --- a/lib-python/uv.lock +++ b/lib-python/uv.lock @@ -298,10 +298,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2f/53/f0b865a971e4e8b3e90e648b6f828950dea4c221bb699421e82ef45f0ef9/cryptography-46.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d1eccae15d5c28c74b2bea228775c63ac5b6c36eedb574e002440c0bc28750d3", size = 4571982, upload-time = "2025-09-16T21:05:57.322Z" }, { url = "https://files.pythonhosted.org/packages/d4/c8/035be5fd63a98284fd74df9e04156f9fed7aa45cef41feceb0d06cbdadd0/cryptography-46.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1b4fba84166d906a22027f0d958e42f3a4dbbb19c28ea71f0fb7812380b04e3c", size = 4307996, upload-time = "2025-09-16T21:05:59.043Z" }, { url = "https://files.pythonhosted.org/packages/aa/4a/dbb6d7d0a48b95984e2d4caf0a4c7d6606cea5d30241d984c0c02b47f1b6/cryptography-46.0.0-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:523153480d7575a169933f083eb47b1edd5fef45d87b026737de74ffeb300f69", size = 4015692, upload-time = "2025-09-16T21:06:01.324Z" }, - { url = "https://files.pythonhosted.org/packages/65/48/aafcffdde716f6061864e56a0a5908f08dcb8523dab436228957c8ebd5df/cryptography-46.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:f09a3a108223e319168b7557810596631a8cb864657b0c16ed7a6017f0be9433", size = 4982192, upload-time = "2025-09-16T21:06:03.367Z" }, { url = "https://files.pythonhosted.org/packages/4c/ab/1e73cfc181afc3054a09e5e8f7753a8fba254592ff50b735d7456d197353/cryptography-46.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c1f6ccd6f2eef3b2eb52837f0463e853501e45a916b3fc42e5d93cf244a4b97b", size = 4603944, upload-time = "2025-09-16T21:06:05.29Z" }, { url = "https://files.pythonhosted.org/packages/3a/02/d71dac90b77c606c90c366571edf264dc8bd37cf836e7f902253cbf5aa77/cryptography-46.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:80a548a5862d6912a45557a101092cd6c64ae1475b82cef50ee305d14a75f598", size = 4308149, upload-time = "2025-09-16T21:06:07.006Z" }, - { url = "https://files.pythonhosted.org/packages/29/e6/4dcb67fdc6addf4e319a99c4bed25776cb691f3aa6e0c4646474748816c6/cryptography-46.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6c39fd5cd9b7526afa69d64b5e5645a06e1b904f342584b3885254400b63f1b3", size = 4947449, upload-time = "2025-09-16T21:06:11.244Z" }, { url = "https://files.pythonhosted.org/packages/26/04/91e3fad8ee33aa87815c8f25563f176a58da676c2b14757a4d3b19f0253c/cryptography-46.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d5c0cbb2fb522f7e39b59a5482a1c9c5923b7c506cfe96a1b8e7368c31617ac0", size = 4603549, upload-time = "2025-09-16T21:06:13.268Z" }, { url = "https://files.pythonhosted.org/packages/9c/6e/caf4efadcc8f593cbaacfbb04778f78b6d0dac287b45cec25e5054de38b7/cryptography-46.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6d8945bc120dcd90ae39aa841afddaeafc5f2e832809dc54fb906e3db829dfdc", size = 4435976, upload-time = "2025-09-16T21:06:16.514Z" }, { url = "https://files.pythonhosted.org/packages/c1/c0/704710f349db25c5b91965c3662d5a758011b2511408d9451126429b6cd6/cryptography-46.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:88c09da8a94ac27798f6b62de6968ac78bb94805b5d272dbcfd5fdc8c566999f", size = 4709447, upload-time = "2025-09-16T21:06:19.246Z" }, @@ -309,10 +307,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/b9/07aec6b183ef0054b5f826ae43f0b4db34c50b56aff18f67babdcc2642a3/cryptography-46.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:da7f93551d39d462263b6b5c9056c49f780b9200bf9fc2656d7c88c7bdb9b363", size = 4545583, upload-time = "2025-09-16T21:06:31.914Z" }, { url = "https://files.pythonhosted.org/packages/39/4a/7d25158be8c607e2b9ebda49be762404d675b47df335d0d2a3b979d80213/cryptography-46.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:be7479f9504bfb46628544ec7cb4637fe6af8b70445d4455fbb9c395ad9b7290", size = 4299196, upload-time = "2025-09-16T21:06:33.724Z" }, { url = "https://files.pythonhosted.org/packages/15/3f/65c8753c0dbebe769cc9f9d87d52bce8b74e850ef2818c59bfc7e4248663/cryptography-46.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f85e6a7d42ad60024fa1347b1d4ef82c4df517a4deb7f829d301f1a92ded038c", size = 3994419, upload-time = "2025-09-16T21:06:35.877Z" }, - { url = "https://files.pythonhosted.org/packages/d5/b4/69a271873cfc333a236443c94aa07e0233bc36b384e182da2263703b5759/cryptography-46.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:d349af4d76a93562f1dce4d983a4a34d01cb22b48635b0d2a0b8372cdb4a8136", size = 4960228, upload-time = "2025-09-16T21:06:38.182Z" }, { url = "https://files.pythonhosted.org/packages/af/e0/ab62ee938b8d17bd1025cff569803cfc1c62dfdf89ffc78df6e092bff35f/cryptography-46.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:35aa1a44bd3e0efc3ef09cf924b3a0e2a57eda84074556f4506af2d294076685", size = 4577257, upload-time = "2025-09-16T21:06:39.998Z" }, { url = "https://files.pythonhosted.org/packages/49/67/09a581c21da7189676678edd2bd37b64888c88c2d2727f2c3e0350194fba/cryptography-46.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c457ad3f151d5fb380be99425b286167b358f76d97ad18b188b68097193ed95a", size = 4299023, upload-time = "2025-09-16T21:06:42.182Z" }, - { url = "https://files.pythonhosted.org/packages/af/28/2cb6d3d0d2c8ce8be4f19f4d83956c845c760a9e6dfe5b476cebed4f4f00/cryptography-46.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:399ef4c9be67f3902e5ca1d80e64b04498f8b56c19e1bc8d0825050ea5290410", size = 4925802, upload-time = "2025-09-16T21:06:44.31Z" }, { url = "https://files.pythonhosted.org/packages/88/0b/1f31b6658c1dfa04e82b88de2d160e0e849ffb94353b1526dfb3a225a100/cryptography-46.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:378eff89b040cbce6169528f130ee75dceeb97eef396a801daec03b696434f06", size = 4577107, upload-time = "2025-09-16T21:06:46.324Z" }, { url = "https://files.pythonhosted.org/packages/c2/af/507de3a1d4ded3068ddef188475d241bfc66563d99161585c8f2809fee01/cryptography-46.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c3648d6a5878fd1c9a22b1d43fa75efc069d5f54de12df95c638ae7ba88701d0", size = 4422506, upload-time = "2025-09-16T21:06:47.963Z" }, { url = "https://files.pythonhosted.org/packages/47/aa/08e514756504d92334cabfe7fe792d10d977f2294ef126b2056b436450eb/cryptography-46.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fc30be952dd4334801d345d134c9ef0e9ccbaa8c3e1bc18925cbc4247b3e29c", size = 4684081, upload-time = "2025-09-16T21:06:49.667Z" }, @@ -320,10 +316,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/94/f1c1f30110c05fa5247bf460b17acfd52fa3f5c77e94ba19cff8957dc5e6/cryptography-46.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c3cd09b1490c1509bf3892bde9cef729795fae4a2fee0621f19be3321beca7e4", size = 4562561, upload-time = "2025-09-16T21:07:03.386Z" }, { url = "https://files.pythonhosted.org/packages/5d/54/8decbf2f707350bedcd525833d3a0cc0203d8b080d926ad75d5c4de701ba/cryptography-46.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d14eaf1569d6252280516bedaffdd65267428cdbc3a8c2d6de63753cf0863d5e", size = 4301974, upload-time = "2025-09-16T21:07:04.962Z" }, { url = "https://files.pythonhosted.org/packages/82/63/c34a2f3516c6b05801f129616a5a1c68a8c403b91f23f9db783ee1d4f700/cryptography-46.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ab3a14cecc741c8c03ad0ad46dfbf18de25218551931a23bca2731d46c706d83", size = 4009462, upload-time = "2025-09-16T21:07:06.569Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c5/92ef920a4cf8ff35fcf9da5a09f008a6977dcb9801c709799ec1bf2873fb/cryptography-46.0.0-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8e8b222eb54e3e7d3743a7c2b1f7fa7df7a9add790307bb34327c88ec85fe087", size = 4980769, upload-time = "2025-09-16T21:07:08.269Z" }, { url = "https://files.pythonhosted.org/packages/a9/8f/1705f7ea3b9468c4a4fef6cce631db14feb6748499870a4772993cbeb729/cryptography-46.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7f3f88df0c9b248dcc2e76124f9140621aca187ccc396b87bc363f890acf3a30", size = 4591812, upload-time = "2025-09-16T21:07:10.288Z" }, { url = "https://files.pythonhosted.org/packages/34/b9/2d797ce9d346b8bac9f570b43e6e14226ff0f625f7f6f2f95d9065e316e3/cryptography-46.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9aa85222f03fdb30defabc7a9e1e3d4ec76eb74ea9fe1504b2800844f9c98440", size = 4301844, upload-time = "2025-09-16T21:07:12.522Z" }, - { url = "https://files.pythonhosted.org/packages/a8/2d/8efc9712997b46aea2ac8f74adc31f780ac4662e3b107ecad0d5c1a0c7f8/cryptography-46.0.0-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f9aaf2a91302e1490c068d2f3af7df4137ac2b36600f5bd26e53d9ec320412d3", size = 4943257, upload-time = "2025-09-16T21:07:14.289Z" }, { url = "https://files.pythonhosted.org/packages/c4/0c/bc365287a97d28aa7feef8810884831b2a38a8dc4cf0f8d6927ad1568d27/cryptography-46.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:32670ca085150ff36b438c17f2dfc54146fe4a074ebf0a76d72fb1b419a974bc", size = 4591154, upload-time = "2025-09-16T21:07:16.271Z" }, { url = "https://files.pythonhosted.org/packages/51/3b/0b15107277b0c558c02027da615f4e78c892f22c6a04d29c6ad43fcddca6/cryptography-46.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0f58183453032727a65e6605240e7a3824fd1d6a7e75d2b537e280286ab79a52", size = 4428200, upload-time = "2025-09-16T21:07:18.118Z" }, { url = "https://files.pythonhosted.org/packages/cf/24/814d69418247ea2cfc985eec6678239013500d745bc7a0a35a32c2e2f3be/cryptography-46.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4bc257c2d5d865ed37d0bd7c500baa71f939a7952c424f28632298d80ccd5ec1", size = 4699862, upload-time = "2025-09-16T21:07:20.219Z" }, @@ -411,7 +405,7 @@ name = "importlib-metadata" version = "8.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp" }, + { name = "zipp", marker = "python_full_version < '3.14' or platform_python_implementation == 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } wheels = [ @@ -505,6 +499,7 @@ dev = [ { name = "pyright" }, { name = "pytest" }, { name = "ruff" }, + { name = "setuptools" }, { name = "twine" }, ] @@ -517,6 +512,7 @@ dev = [ { name = "pyright", specifier = ">=1.1.403" }, { name = "pytest", specifier = ">=8.4.1" }, { name = "ruff", specifier = ">=0.12.8" }, + { name = "setuptools", specifier = ">=61.0" }, { name = "twine", specifier = ">=6.2.0" }, ] @@ -610,10 +606,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b1/a7/8c4f86c78ec03db954d05fd9c57a114cc3a172a2d3e4a8b949cd5ff89471/patchelf-0.17.2.4-py3-none-macosx_10_9_universal2.whl", hash = "sha256:343bb1b94e959f9070ca9607453b04390e36bbaa33c88640b989cefad0aa049e", size = 184436, upload-time = "2025-07-23T21:16:20.578Z" }, { url = "https://files.pythonhosted.org/packages/7e/19/f7821ef31aab01fa7dc8ebe697ece88ec4f7a0fdd3155dab2dfee4b00e5c/patchelf-0.17.2.4-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:d9b35ebfada70c02679ad036407d9724ffe1255122ba4ac5e4be5868618a5689", size = 482846, upload-time = "2025-07-23T21:16:23.73Z" }, { url = "https://files.pythonhosted.org/packages/d1/50/107fea848ecfd851d473b079cab79107487d72c4c3cdb25b9d2603a24ca2/patchelf-0.17.2.4-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:2931a1b5b85f3549661898af7bf746afbda7903c7c9a967cfc998a3563f84fad", size = 477811, upload-time = "2025-07-23T21:16:25.145Z" }, - { url = "https://files.pythonhosted.org/packages/89/a9/a9a2103e159fd65bffbc21ecc5c8c36e44eb34fe53b4ef85fb6d08c2a635/patchelf-0.17.2.4-py3-none-manylinux2014_armv7l.manylinux_2_17_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:ae44cb3c857d50f54b99e5697aa978726ada33a8a6129d4b8b7ffd28b996652d", size = 431226, upload-time = "2025-07-23T21:16:26.765Z" }, - { url = "https://files.pythonhosted.org/packages/87/93/897d612f6df7cfd987bdf668425127efeff8d8e4ad8bfbab1c69d2a0d861/patchelf-0.17.2.4-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:680a266a70f60a7a4f4c448482c5bdba80cc8e6bb155a49dcc24238ba49927b0", size = 540276, upload-time = "2025-07-23T21:16:27.983Z" }, - { url = "https://files.pythonhosted.org/packages/5d/b8/2b92d11533482bac9ee989081d6880845287751b5f528adbd6bb27667fbd/patchelf-0.17.2.4-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.musllinux_1_1_s390x.whl", hash = "sha256:d842b51f0401460f3b1f3a3a67d2c266a8f515a5adfbfa6e7b656cb3ac2ed8bc", size = 596632, upload-time = "2025-07-23T21:16:29.253Z" }, - { url = "https://files.pythonhosted.org/packages/14/e2/975d4bdb418f942b53e6187b95bd9e0d5e0488b7bc214685a1e43e2c2751/patchelf-0.17.2.4-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:7076d9e127230982e20a81a6e2358d3343004667ba510d9f822d4fdee29b0d71", size = 508281, upload-time = "2025-07-23T21:16:30.865Z" }, ] [[package]] @@ -812,6 +804,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/ff/2e2eed29e02c14a5cb6c57f09b2d5b40e65d6cc71f45b52e0be295ccbc2f/secretstorage-3.4.0-py3-none-any.whl", hash = "sha256:0e3b6265c2c63509fb7415717607e4b2c9ab767b7f344a57473b779ca13bd02e", size = 15272, upload-time = "2025-09-09T16:42:12.744Z" }, ] +[[package]] +name = "setuptools" +version = "82.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/f3/748f4d6f65d1756b9ae577f329c951cda23fb900e4de9f70900ced962085/setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb", size = 1144893, upload-time = "2026-02-08T15:08:40.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" }, +] + [[package]] name = "tomli" version = "2.2.1" From b50ac8955c27d17eded5bd2a44dcd735ff54311a Mon Sep 17 00:00:00 2001 From: Bart Dubbeldam Date: Sat, 21 Feb 2026 02:00:31 +0100 Subject: [PATCH 2/4] Declare inputs and outputs for buildWithGraalVmNativeImage task PixiExecTask only declares inputs (command strings, env vars) but no outputs, so Gradle always considered this task out-of-date and re-ran the full native-image build every time. Add explicit input/output declarations so Gradle can properly cache the task: the project JAR, runtime classpath, and JNI config as inputs, and the native binary as the output. The output is the specific binary file rather than the whole directory, since krossover's generateJniBindingsJvm also writes into that directory. --- kson-lib/build.gradle.kts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/kson-lib/build.gradle.kts b/kson-lib/build.gradle.kts index 26d76bcf..2a8f03bf 100644 --- a/kson-lib/build.gradle.kts +++ b/kson-lib/build.gradle.kts @@ -267,6 +267,18 @@ tasks.register("buildWithGraalVmNativeImage") { val ksonCoreJarTask = project.rootProject.tasks.named("jvmJar") dependsOn(ksonLibJarTask, ksonCoreJarTask, "generateJniBindingsJvm") + val nativeImageOutputDir = project.projectDir.resolve("build/kotlin/compileGraalVmNativeImage") + val jniConfig = project.projectDir.resolve("build/kotlin/krossover/metadata/jni-config.json") + + // ksonLibJarTask is this project's own JAR (not in its own runtime classpath). + // ksonCoreJarTask (root project) is a dependency, so it's covered by jvmRuntimeClasspath. + inputs.files(ksonLibJarTask.map { it.archiveFile }) + inputs.files(configurations.named("jvmRuntimeClasspath")) + inputs.file(jniConfig) + // Declare the specific binary rather than the whole directory, since krossover's + // generateJniBindingsJvm also writes jni_simplified.h into nativeImageOutputDir. + outputs.file(nativeImageOutputDir.resolve(BinaryArtifactPaths.binaryFileName())) + // Configure the command at configuration time using providers command.set(provider { val graalHome = GraalVmHelper.getGraalVMHome() @@ -277,7 +289,7 @@ tasks.register("buildWithGraalVmNativeImage") { } // Ensure build dir exists - val buildDir = project.projectDir.resolve("build/kotlin/compileGraalVmNativeImage").toPath() + val buildDir = nativeImageOutputDir.toPath() buildDir.createDirectories() val buildArtifactPath = buildDir.resolve(BinaryArtifactPaths.binaryFileNameWithoutExtension()).toAbsolutePath().pathString @@ -305,9 +317,6 @@ tasks.register("buildWithGraalVmNativeImage") { } val classPath = jars.joinToString(cpSeparator) - // The `jniConfig` file tells graal which classes should be publicly exposed - val jniConfig = project.projectDir.resolve("build/kotlin/krossover/metadata/jni-config.json") - listOf( nativeImageExe.absolutePath, "--shared", From 44f2356530f54b7c929463010a68be1102413cd4 Mon Sep 17 00:00:00 2001 From: Bart Dubbeldam Date: Sat, 21 Feb 2026 02:42:15 +0100 Subject: [PATCH 3/4] Centralize cibuildwheel configuration in pyproject.toml Move all cibuildwheel configuration from environment variables in the Gradle buildWheel task to pyproject.toml under [tool.cibuildwheel]. This is the canonical location for cibuildwheel config, and centralizing it means the settings apply regardless of how cibuildwheel is invoked. Also pin MACOSX_DEPLOYMENT_TARGET to 11.0 (Big Sur, the first ARM-native macOS) so that builds don't fail when the host macOS version is newer than what the Python packaging tools recognize. --- lib-python/build.gradle.kts | 7 +------ lib-python/pyproject.toml | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/lib-python/build.gradle.kts b/lib-python/build.gradle.kts index 8049707d..8f4aded0 100644 --- a/lib-python/build.gradle.kts +++ b/lib-python/build.gradle.kts @@ -138,12 +138,7 @@ tasks { errorOutput = System.err isIgnoreExitValue = false - // Configure cibuildwheel - environment("CIBW_BUILD", "cp310-*") // Build for Python 3.10+ - environment("CIBW_SKIP", "*-musllinux_*") // Skip musl Linux builds - environment("CIBW_ARCHS", "native") // Build only for native architecture - environment("CIBW_TEST_REQUIRES", "pytest") // Install pytest for testing - environment("CIBW_TEST_COMMAND", "pytest -v {project}/tests --ignore={project}/tests/test_build_backend.py") + // cibuildwheel configuration lives in pyproject.toml under [tool.cibuildwheel] doLast { println("Successfully built platform-specific wheel using cibuildwheel") diff --git a/lib-python/pyproject.toml b/lib-python/pyproject.toml index 6d5c8127..eff38f60 100644 --- a/lib-python/pyproject.toml +++ b/lib-python/pyproject.toml @@ -51,6 +51,20 @@ kson = ["kson_api.h", "kson.dll", "libkson.dylib", "libkson.so", "jni_simplified [tool.pytest.ini_options] pythonpath = ["."] +[tool.cibuildwheel] +# One wheel targeting 3.10 is sufficient: the abi3 stable ABI tag (see setup.py) +# makes it installable on any CPython >= 3.10. +build = "cp310-*" +skip = "*-musllinux_*" +archs = "native" +test-requires = "pytest" +test-command = "pytest -v {project}/tests --ignore={project}/tests/test_build_backend.py" + +[tool.cibuildwheel.macos] +# Pin deployment target so builds don't break when the host macOS is +# newer than what the Python packaging tools recognize yet. +environment = { MACOSX_DEPLOYMENT_TARGET = "11.0" } + [dependency-groups] dev = [ "pyright>=1.1.403", From edbefb3e65f3250571f2eead4116c781f917f8bb Mon Sep 17 00:00:00 2001 From: Bart Dubbeldam Date: Sat, 21 Feb 2026 13:07:57 +0100 Subject: [PATCH 4/4] Gitignore lib-python/LICENSE This file is copied from the project root by the copyLicense Gradle task at wheel-build time. It's a build artifact, not a source file. --- lib-python/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/lib-python/.gitignore b/lib-python/.gitignore index 9cdbe3db..65886729 100644 --- a/lib-python/.gitignore +++ b/lib-python/.gitignore @@ -8,6 +8,7 @@ kson-sdist *.h *.egg-info *.pyd +LICENSE # uvw generated files .uv \ No newline at end of file