scripts.build_janus_ft

Reference › scripts › build_janus_ft

Builds a versioned release wheel and sdist for the patched free-threaded janus-swi package, the bridge between Python and SWI-Prolog. The patched source lives in scripts/_janus_patched, and the output goes to the dist-janus/ directory. A human runs it; tests/scripts/test_build_janus_ft.py imports it.

Invocation: main takes --local-version, a PEP 440 local version segment appended to the upstream version (a default is built in). build checks that it is running on a free-threaded interpreter (_check_free_threaded_interpreter), reads the upstream version (_read_upstream_version), copies the source to a temporary directory with the new version applied (_prepare_versioned_source) and builds with a pinned system toolchain (_build_env, _pin_system_toolchain). It prints the artifacts and the next step, which is to install a wheel into a target virtual environment with scripts/install_janus_ft.sh.

At a glance

Source scripts/build_janus_ft.py
Lines of code 250
Classes 0
Functions 8
Imports (internal) none
Imported by none
Tests that import it tests/scripts/test_build_janus_ft.py

Class diagram

No classes; see Functions.

Dependency diagram

No internal imports and no importers.

API

Functions

Function Signature Summary
_pin_system_toolchain _pin_system_toolchain(env: dict[str, str]) -> dict[str, str] worktree_toolchain.pin_system_toolchain, loaded by file path so it works however this script was imported (the same seam worktrees.py uses): CC/CXX come from the env/toolchain.env…
_build_env _build_env() -> dict[str, str] Child environment for the C compile: the host shell minus the foreign toolchain contamination above, plus the warning demotion janus.c needs.
_run _run(cmd: list[str], cwd: Path | None = None, env: dict[str, str] | None = None) -> None
_read_upstream_version _read_upstream_version() -> str
_prepare_versioned_source _prepare_versioned_source(tmp: Path, local_segment: str) -> tuple[Path, str] Copy the vendored source into tmp with a local version segment.
_check_free_threaded_interpreter _check_free_threaded_interpreter() -> None
build build(local_segment: str) -> list[Path]
main main() -> int

Source

Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/scripts/build_janus_ft.py

#!/usr/bin/env python3
"""Build a versioned release wheel + sdist for the patched free-threaded janus-swi.

``scripts/_janus_patched`` vendors a thread-safe fork of janus-swi (the
SWI-Prolog <-> Python bridge) that keeps CPython's GIL genuinely disabled
under Python 3.14t (see ``docs/free-threaded-prolog.md`` for the full patch
inventory and root-cause analysis). During normal development it is built
in place by ``uv sync`` via ``[tool.uv.sources] janus-swi = { path =
"scripts/_janus_patched" }`` + ``no-build-isolation-package = ["janus-swi"]``
(pyproject.toml) — that always produces version ``1.5.3`` (the version
baked into the vendored ``setup.py``/``VERSION``), indistinguishable from a
hypothetical unpatched 1.5.3.

This script instead produces a RELEASE artifact for distribution off this
repo (a fresh machine, a container image, another project): a wheel + sdist
tagged with a local version segment (PEP 440, e.g. ``1.5.3+orchestrata.1``)
so it can never be mistaken for stock janus-swi. It does this WITHOUT
touching the vendored source tree — it copies ``scripts/_janus_patched``
into a scratch directory, rewrites the version string in the copy's
``setup.py`` (the vendored pyproject.toml declares ``dynamic = ["version"]``
and setup.py supplies it directly, no setuptools-scm involved — see the
"Version" note in docs/free-threaded-prolog.md), and builds from the copy
with ``uv build --no-build-isolation`` against the project's free-threaded
3.14t interpreter so the produced wheel carries the ``cp314t`` ABI tag and
was actually compiled with ``Py_GIL_DISABLED`` defined.

Usage::

    python scripts/build_janus_ft.py                # build wheel + sdist
    python scripts/build_janus_ft.py --local-version orchestrata.2

Bump ``LOCAL_VERSION_SEGMENT`` (or pass ``--local-version``) whenever the
vendored patch changes; the artifact filename and ``janus_swi.dist-info``
metadata both carry it. Mirrors the vendored-FT-grpcio playbook
(``scripts/build_ft_grpcio.py``): binaries are built, sha256'd, and never
committed (``dist-janus/`` is gitignored) — a fresh clone regenerates them
on demand.
"""

from __future__ import annotations

import argparse
import hashlib
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent.parent
VENDORED_DIR = REPO_ROOT / "scripts" / "_janus_patched"
DIST_DIR = REPO_ROOT / "dist-janus"
VENV_PYTHON = REPO_ROOT / ".venv" / "bin" / "python3.14t"

# Bump this when the vendored patch (scripts/_janus_patched) changes so the
# artifact filename/version always distinguishes releases. Not derived
# automatically — the vendored source has no notion of "patch generation".
LOCAL_VERSION_SEGMENT = "orchestrata.1"

# Directories/files present in the working tree (build cruft, VCS-ignored)
# that must not be copied into the release source tree.
_COPY_IGNORE = shutil.ignore_patterns(
    "build", "__pycache__", "*.egg-info", "*.so", "dist", ".git"
)

# Host-shell variables that leak a foreign toolchain's headers/libraries into
# the C compile. The Guix profile on the dev fleet exports these; with
# C_INCLUDE_PATH set, the compiler picks the Guix glibc headers and dies on
# the GCC-only _Float64 typedefs in bits/floatn.h (measured: Clang 19 vs
# /home/*/.guix-profile/include/stdlib.h), and LIBRARY_PATH misdirects the
# linker the same way. The vendored setup.py passes swipl's own include/lib
# dirs explicitly, so scrubbing these loses nothing.
_BUILD_ENV_STRIP = ("C_INCLUDE_PATH", "CPLUS_INCLUDE_PATH", "CPATH", "LIBRARY_PATH")


def _pin_system_toolchain(env: dict[str, str]) -> dict[str, str]:
    """``worktree_toolchain.pin_system_toolchain``, loaded by file path so it
    works however this script was imported (the same seam worktrees.py uses):
    CC/CXX come from the ``env/toolchain.env`` declaration, unconditionally —
    never from PATH — and a missing, bare-named or non-executable declaration
    raises ``ToolchainDeclarationError``, so the build is refused."""
    import importlib.util

    spec = importlib.util.spec_from_file_location(
        "build_janus_ft_toolchain",
        Path(__file__).resolve().parent / "worktree_toolchain.py",
    )
    assert spec and spec.loader
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module.pin_system_toolchain(env)


def _build_env() -> dict[str, str]:
    """Child environment for the C compile: the host shell minus the foreign
    toolchain contamination above, plus the warning demotion janus.c needs.

    janus.c registers foreign predicates through old-style ``foreign_t``
    pointers passed to ``PL_register_foreign_in_module``; GCC >= 14 makes
    ``-Wincompatible-pointer-types`` a hard error for that (see
    scripts/_janus_patched/BUILDING.md), so demote it for whatever ``cc``
    the build lands on (harmless where the warning never fires).
    """
    env = {k: v for k, v in os.environ.items() if k not in _BUILD_ENV_STRIP}
    # Scrubbing include paths is not enough when the COMPILER itself is the
    # foreign one: with ~/.guix-profile/bin first on PATH, `cc` is Guix's gcc,
    # whose glibc headers carry no Linux kernel headers (measured 2026-10-05:
    # `bits/local_lim.h:38: fatal error: linux/limits.h: No such file`). The
    # compiler comes from the env/toolchain.env declaration via
    # worktree_toolchain.pin_system_toolchain, unconditionally (never from
    # PATH); a missing, bare-named or non-executable declaration raises
    # ToolchainDeclarationError and the build is refused.
    env = _pin_system_toolchain(env)
    demote = "-Wno-error=incompatible-pointer-types"
    env["CFLAGS"] = f"{env.get('CFLAGS', '')} {demote}".strip()
    return env


def _run(
    cmd: list[str],
    cwd: Path | None = None,
    env: dict[str, str] | None = None,
) -> None:
    print(f"$ {' '.join(cmd)}")
    subprocess.run(cmd, check=True, cwd=cwd, env=env)


def _read_upstream_version() -> str:
    version = (VENDORED_DIR / "VERSION").read_text().strip()
    if not version:
        raise SystemExit(f"empty VERSION file: {VENDORED_DIR / 'VERSION'}")
    return version


def _prepare_versioned_source(tmp: Path, local_segment: str) -> tuple[Path, str]:
    """Copy the vendored source into ``tmp`` with a local version segment.

    Returns (source_dir, full_version). The original vendored tree under
    scripts/_janus_patched is never written to — only the scratch copy is.
    """
    base_version = _read_upstream_version()
    version = f"{base_version}+{local_segment}"

    src_copy = tmp / "_janus_patched"
    shutil.copytree(VENDORED_DIR, src_copy, ignore=_COPY_IGNORE)

    setup_py = src_copy / "setup.py"
    text = setup_py.read_text()
    marker = f"version='{base_version}'"
    if marker not in text:
        raise SystemExit(
            f"expected {marker!r} in {setup_py} (vendored setup.py drifted — "
            "update this script's version-rewrite logic to match)"
        )
    setup_py.write_text(text.replace(marker, f"version='{version}'"))
    (src_copy / "VERSION").write_text(version + "\n")

    return src_copy, version


def _check_free_threaded_interpreter() -> None:
    if not VENV_PYTHON.exists():
        raise SystemExit(
            f"{VENV_PYTHON} not found — create the project venv first "
            "(uv python install 3.14t && uv sync --dev)"
        )
    result = subprocess.run(
        [
            str(VENV_PYTHON),
            "-c",
            "import sys; print("
            "hasattr(sys, '_is_gil_enabled') and not sys._is_gil_enabled()"
            ")",
        ],
        capture_output=True,
        text=True,
        check=True,
    )
    if result.stdout.strip() != "True":
        raise SystemExit(
            f"{VENV_PYTHON} is not a free-threaded (3.14t) interpreter — "
            "janus-swi MUST be compiled against Py_GIL_DISABLED headers to "
            "produce a cp314t wheel (see docs/free-threaded-prolog.md)"
        )


def build(local_segment: str) -> list[Path]:
    _check_free_threaded_interpreter()
    if not VENDORED_DIR.exists():
        raise SystemExit(f"vendored source missing: {VENDORED_DIR}")

    DIST_DIR.mkdir(parents=True, exist_ok=True)
    with tempfile.TemporaryDirectory(prefix="janus-ft-build-") as tmp:
        tmp_path = Path(tmp)
        src_copy, version = _prepare_versioned_source(tmp_path, local_segment)

        _run(
            [
                "uv",
                "build",
                "--sdist",
                "--wheel",
                "--no-build-isolation",
                "--python",
                str(VENV_PYTHON),
                "--out-dir",
                str(DIST_DIR),
                str(src_copy),
            ],
            env=_build_env(),
        )

    artifacts = sorted(DIST_DIR.glob(f"janus_swi-{version}*"))
    if not artifacts:
        raise SystemExit(f"build finished but no artifacts found in {DIST_DIR}")

    for artifact in artifacts:
        digest = hashlib.sha256(artifact.read_bytes()).hexdigest()
        sha_file = artifact.with_suffix(artifact.suffix + ".sha256")
        sha_file.write_text(f"{digest}  {artifact.name}\n")
        print(
            f"Built: {artifact.name} ({artifact.stat().st_size} bytes) sha256={digest}"
        )

    return artifacts


def main() -> int:
    parser = argparse.ArgumentParser(
        description="Build a versioned wheel+sdist release of the patched "
        "free-threaded janus-swi (scripts/_janus_patched) into dist-janus/."
    )
    parser.add_argument(
        "--local-version",
        default=LOCAL_VERSION_SEGMENT,
        help=f"PEP 440 local version segment (default: {LOCAL_VERSION_SEGMENT})",
    )
    args = parser.parse_args()
    artifacts = build(args.local_version)
    print(f"\n{len(artifacts)} artifact(s) in {DIST_DIR.relative_to(REPO_ROOT)}/")
    print(
        "Next: scripts/install_janus_ft.sh --wheel "
        f"{DIST_DIR.relative_to(REPO_ROOT)}/<the .whl above> --venv <target>"
    )
    return 0


if __name__ == "__main__":
    sys.exit(main())