From 5ba76feb609e1b539b4148b68af98cd2b3f6806e Mon Sep 17 00:00:00 2001 From: Manu Garg Date: Sat, 22 Aug 2026 13:05:27 -0700 Subject: [PATCH 1/3] Ship C sources and version.mk in the python sdist The sdist built from src/pymod only contained pacparser_py.c and pacparser/__init__.py, so building it from source failed: pacparser.h was missing for compilation, and pacparser.o/libquickjs.a for linking. The sdist command now copies pacparser.h, pacparser.c, pac_utils.h and the quickjs sources into the archive and writes a version.mk pinning the version (there is no git metadata in an sdist). When no prebuilt objects are present, setup.py compiles them from the shipped sources. This also fixes the version falling back to 1.0.0 when building the sdist, which produced mismatched wheel metadata. --- .gitignore | 9 ++++ src/pymod/MANIFEST.in | 8 +++- src/pymod/setup.py | 101 ++++++++++++++++++++++++++++++++++++++---- 3 files changed, 108 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index e9e07680..78702c9d 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,15 @@ src/pac_utils_dump # build **build +__pycache__ + +# C sources copied into pymod by sdist and wheel builds +src/pymod/pacparser.c +src/pymod/pacparser.h +src/pymod/pac_utils.h +src/pymod/quickjs +src/pymod/version.mk + # Sonar scanner .scannerwork diff --git a/src/pymod/MANIFEST.in b/src/pymod/MANIFEST.in index f0507230..ce413744 100644 --- a/src/pymod/MANIFEST.in +++ b/src/pymod/MANIFEST.in @@ -1,2 +1,8 @@ include pacparser_py.c -include pacparser/* +include pacparser/__init__.py +include pacparser.h +include pacparser.c +include pac_utils.h +include version.mk +include quickjs/quickjs.c +include quickjs/quickjs.h diff --git a/src/pymod/setup.py b/src/pymod/setup.py index 956eaa75..4b4538bc 100644 --- a/src/pymod/setup.py +++ b/src/pymod/setup.py @@ -29,11 +29,15 @@ import os import platform import re +import shlex import setuptools +import setuptools.command.sdist import shutil import subprocess import sys +import sysconfig +from distutils.command.clean import clean as _clean_cmd from unittest.mock import patch @@ -76,11 +80,15 @@ def pacparser_version(): ): return git_version() - # Check if we have version.mk. It's added in the manual release tarball. - version_file = os.path.join(setup_dir(), "..", "version.mk") - if os.path.exists(version_file): - with open(version_file) as f: - return sanitize_version(f.read().replace("VERSION=", "")) + # Check if we have version.mk. It's added in the manual release tarball + # (next to src/) and in the python sdist (at its root). + for version_file in ( + os.path.join(setup_dir(), "..", "version.mk"), + os.path.join(setup_dir(), "version.mk"), + ): + if os.path.exists(version_file): + with open(version_file) as f: + return sanitize_version(f.read().replace("VERSION=", "")) return sanitize_version(os.environ.get("PACPARSER_VERSION", "1.0.0")) @@ -123,21 +131,94 @@ def run(self): ) +class SDistCmd(setuptools.command.sdist.sdist): + """Build pacparser python source distribution.""" + + description = "Build pacparser python source distribution." + + def run(self): + # The sdist must be buildable on its own, so pull in the C sources + # that pacparser_py.c needs to compile and link, along with a + # version.mk pinning the version (there is no git metadata in the + # sdist). + for name in ("pacparser.h", "pacparser.c", "pac_utils.h"): + shutil.copy(os.path.join("..", name), name) + if not os.path.isdir("quickjs"): + os.mkdir("quickjs") + for name in ("quickjs.c", "quickjs.h"): + shutil.copy( + os.path.join("..", "quickjs", name), os.path.join("quickjs", name) + ) + with open("version.mk", "w") as f: + f.write("VERSION=%s\n" % pacparser_version()) + setuptools.command.sdist.sdist.run(self) + + +class CleanCmd(_clean_cmd): + """Clean pacparser python build artifacts.""" + + def run(self): + _clean_cmd.run(self) + # Remove the C sources copied in by the sdist and wheel builds. + for name in ("pacparser.h", "pacparser.c", "pac_utils.h", "version.mk"): + if os.path.exists(name): + os.remove(name) + if os.path.isdir("quickjs"): + shutil.rmtree("quickjs") + + +def build_c_objects(): + """Compile pacparser.o and quickjs/libquickjs.a from the C sources. + + The sdist ships the C sources but no prebuilt objects, so they have to + be built before the _pacparser extension can be linked. + """ + cc = shlex.split(sysconfig.get_config_var("CC") or "cc") + ar = shlex.split(sysconfig.get_config_var("AR") or "ar") + if not os.path.exists(os.path.join("quickjs", "libquickjs.a")): + quickjs_obj = os.path.join("quickjs", "quickjs.o") + if not os.path.exists(quickjs_obj): + subprocess.check_call( + cc + ["-fPIC", "-c", os.path.join("quickjs", "quickjs.c"), + "-o", quickjs_obj] + ) + subprocess.check_call( + ar + ["rcs", os.path.join("quickjs", "libquickjs.a"), quickjs_obj] + ) + if not os.path.exists("pacparser.o"): + subprocess.check_call( + cc + ["-g", "-Wall", "-DVERSION=%s" % pacparser_version(), + "-Iquickjs", "-fPIC", "-c", "pacparser.c", + "-o", "pacparser.o"] + ) + + @patch("setuptools._distutils.cygwinccompiler.get_msvcr") def main(patched_func): python_home = os.path.dirname(sys.executable) - extra_objects = [] obj_search_path = { "pacparser.o": ["..", "."], - "libquickjs.a": ["../quickjs", "."], + "libquickjs.a": ["../quickjs", "quickjs", "."], } + found_objects = {} for obj, paths in obj_search_path.items(): for path in paths: if os.path.exists(os.path.join(path, obj)): - extra_objects.append(os.path.join(path, obj)) + found_objects[obj] = os.path.join(path, obj) break + # When building from the sdist, the C sources are present but the + # prebuilt objects are not, so compile them. + if len(found_objects) < len(obj_search_path) and sys.platform != "win32": + build_c_objects() + found_objects["pacparser.o"] = found_objects.get( + "pacparser.o", "pacparser.o") + found_objects["libquickjs.a"] = found_objects.get( + "libquickjs.a", os.path.join("quickjs", "libquickjs.a")) + + extra_objects = list(found_objects.values()) + libraries = [] extra_link_args = [] @@ -151,7 +232,7 @@ def main(patched_func): pacparser_module = setuptools.Extension( "_pacparser", - include_dirs=[".."], + include_dirs=["..", "."], sources=["pacparser_py.c"], libraries=libraries, extra_link_args=extra_link_args, @@ -159,7 +240,9 @@ def main(patched_func): ) setuptools.setup( cmdclass={ + "clean": CleanCmd, "dist": DistCmd, + "sdist": SDistCmd, }, name="pacparser", version=pacparser_version(), From 18d509c8e94449098cf81e3b4808c27b935aa7f2 Mon Sep 17 00:00:00 2001 From: Manu Garg Date: Sat, 22 Aug 2026 13:15:08 -0700 Subject: [PATCH 2/3] Only build C objects from source in the sdist layout make clean removes pacparser.o before running setup.py clean --all, which made the missing-objects check try to compile the sdist sources that do not exist in the source tree. Require the copied pacparser.c to be present before building the objects from source. --- src/pymod/setup.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/pymod/setup.py b/src/pymod/setup.py index 4b4538bc..7a950152 100644 --- a/src/pymod/setup.py +++ b/src/pymod/setup.py @@ -208,9 +208,16 @@ def main(patched_func): found_objects[obj] = os.path.join(path, obj) break - # When building from the sdist, the C sources are present but the - # prebuilt objects are not, so compile them. - if len(found_objects) < len(obj_search_path) and sys.platform != "win32": + # When building from the sdist, the C sources are copied to the setup + # directory but the prebuilt objects are not, so compile them. The + # copied pacparser.c is the marker for the sdist layout; in the source + # tree the sources live in the parent directory and are built by the + # Makefile. + if ( + len(found_objects) < len(obj_search_path) + and sys.platform != "win32" + and os.path.exists("pacparser.c") + ): build_c_objects() found_objects["pacparser.o"] = found_objects.get( "pacparser.o", "pacparser.o") From 4a14a40136a56f4d8d2d64b0207ee56bbbd301e7 Mon Sep 17 00:00:00 2001 From: Manu Garg Date: Sat, 22 Aug 2026 13:53:24 -0700 Subject: [PATCH 3/3] Tighten sdist object build and clean handling Only compile the C objects from source for commands that build the extension (build, build_ext, bdist, bdist_wheel, install), so that plain sdist/egg_info runs in the copied-sources layout do not require a C toolchain. clean --all now also removes pacparser.o and skips the copied-source removal under --dry-run. --- src/pymod/setup.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/pymod/setup.py b/src/pymod/setup.py index 7a950152..a659920f 100644 --- a/src/pymod/setup.py +++ b/src/pymod/setup.py @@ -159,8 +159,14 @@ class CleanCmd(_clean_cmd): def run(self): _clean_cmd.run(self) - # Remove the C sources copied in by the sdist and wheel builds. - for name in ("pacparser.h", "pacparser.c", "pac_utils.h", "version.mk"): + if self.dry_run: + return + # Remove the C sources and objects copied in by the sdist and + # wheel builds. + for name in ( + "pacparser.h", "pacparser.c", "pac_utils.h", "version.mk", + "pacparser.o", + ): if os.path.exists(name): os.remove(name) if os.path.isdir("quickjs"): @@ -212,11 +218,15 @@ def main(patched_func): # directory but the prebuilt objects are not, so compile them. The # copied pacparser.c is the marker for the sdist layout; in the source # tree the sources live in the parent directory and are built by the - # Makefile. + # Makefile. Only do this for commands that build the extension, so + # that other commands (sdist, egg_info, clean, ...) do not require a + # C toolchain. + build_commands = ("build", "build_ext", "bdist", "bdist_wheel", "install") if ( len(found_objects) < len(obj_search_path) and sys.platform != "win32" and os.path.exists("pacparser.c") + and any(cmd in build_commands for cmd in sys.argv[1:]) ): build_c_objects() found_objects["pacparser.o"] = found_objects.get(