"""Drive pyproject_hooks' BuildBackendHookCaller end to end and print key=value. pyproject_hooks is the INDIRECTION a build frontend uses to reach the PEP 517 backend a project names in its own pyproject.toml. Its whole mechanism is a SUBPROCESS: every hook is dispatched by writing input.json into a scratch dir, running ` _in_process.py ` with PEP517_BUILD_BACKEND in the environment, and reading output.json back. So exercising it on target exercises fork(2)+exec(2) through CPython's subprocess module, not only an import. THAT IS WHY THE START COUNT IS THE DESIGN CONSTRAINT. Each hook call is a fresh python3 start, and a start on this arm has been minutes -- long enough that a three-hook probe cannot finish inside the 120s window the host's guest-growth watchdog measures over, and is killed before it reports a single case (brintos/linux#21, brintos/hardwarejs#20; measured on batch-1826, 2026-08-31 and 2026-09-01). So the DEFAULT run takes TWO starts -- itself plus build_wheel -- and BST_PH_ALL_HOOKS=1 restores the other two hooks for a run that can afford them. Everything checkable without a start is checked in-process. Adding a hook call here costs the suite a start; adding a fact does not. Every line is `key=value`; a key is emitted at most once. The suite reads the keys it names and ignores the rest, so adding a fact cannot break it. Failure is reported as a key too -- `*_error` -- rather than only as an exit status, because the case that reds should say which stage stopped and why. """ import os import sys import zipfile W = sys.argv[1] _seen = set() def emit(key, value): if key in _seen: return _seen.add(key) # One line per fact: the suite greps by key, and an embedded newline would # land as a second, unkeyed line. print("%s=%s" % (key, str(value).replace("\n", " ").replace("\r", " "))) def fail(key, exc): emit(key, "%s: %s" % (type(exc).__name__, exc)) raise SystemExit(1) # ------------------------------------------------------------------ the import try: import pyproject_hooks except Exception as exc: # noqa: BLE001 - reported, not raised fail("import_error", exc) emit("version", pyproject_hooks.__version__) emit("module_file", pyproject_hooks.__file__) emit("module_dir", os.path.dirname(os.path.abspath(pyproject_hooks.__file__))) emit("api_exports", ",".join(sorted(pyproject_hooks.__all__))) # THE IN-PROCESS SCRIPT, resolved the way the library resolves it. _impl dispatches # every hook through `_in_proc_script_path()`, which goes through # importlib.resources -- so a package that installed its _in_process subpackage # without the .py beside it imports cleanly and fails at the first hook with a # message about a temporary file. Resolving it here separates the two. try: from pyproject_hooks._in_process import _in_proc_script_path with _in_proc_script_path() as _script: emit("in_process_script", os.path.basename(str(_script))) emit("in_process_exists", "yes" if os.path.isfile(str(_script)) else "no") except Exception as exc: # noqa: BLE001 fail("in_process_error", exc) # The interpreter the hook subprocesses will be started as. Emitted because a # hook failure whose cause is an empty sys.executable looks like a backend fault. emit("python_executable", sys.executable) # ------------------------------------------------- the caller's own path check # norm_and_check() guards backend_path against escaping the source tree, and it # is pure arithmetic over strings -- no subprocess. Checked here because it is the # one piece of the hook caller's own logic that can be exercised for free. from pyproject_hooks._impl import norm_and_check # noqa: E402 try: norm_and_check(W, os.path.join(os.sep, "etc")) emit("reject_absolute", "no") except ValueError: emit("reject_absolute", "yes") except Exception as exc: # noqa: BLE001 emit("reject_absolute", "%s" % type(exc).__name__) try: norm_and_check(os.path.join(W, "proj"), os.path.join("..", "..", "elsewhere")) emit("reject_escape", "no") except ValueError: emit("reject_escape", "yes") except Exception as exc: # noqa: BLE001 emit("reject_escape", "%s" % type(exc).__name__) # --------------------------------------------------------------- the fixture # Written here rather than shipped in data/ so the project tree is created on # tmpfs: the rootfs is read-only at browser runtime, and the backend writes # relative to the project root. # # THE BACKEND IS flit_core, WHICH IS A SIBLING PORT (BRINTOS-1828). It is named in # the fixture's own pyproject.toml, which is exactly the indirection under test: # this probe never imports flit_core, it asks pyproject_hooks to reach it. NAME = "bstfixture" VERSION = "1.2.3" SUMMARY = "brintOS pyproject_hooks self-test fixture" BACKEND = "flit_core.buildapi" proj = os.path.join(W, "proj") try: os.makedirs(proj, exist_ok=True) with open(os.path.join(proj, NAME + ".py"), "w") as fh: fh.write('"""%s."""\n\n__version__ = "%s"\n\n\ndef hello():\n return "hello from %s"\n' % (SUMMARY, VERSION, NAME)) # Neither `version` nor `description` is dynamic, so # get_requires_for_build_wheel has no reason to report the project's runtime # dependencies as build requirements -- which is what makes the empty list # below a statement about the hook rather than about the fixture. with open(os.path.join(proj, "pyproject.toml"), "w") as fh: fh.write( '[build-system]\n' 'requires = ["flit_core >=3.2,<4"]\n' 'build-backend = "%s"\n' '\n' '[project]\n' 'name = "%s"\n' 'version = "%s"\n' 'description = "%s"\n' 'requires-python = ">=3.8"\n' % (BACKEND, NAME, VERSION, SUMMARY)) except Exception as exc: # noqa: BLE001 fail("fixture_error", exc) emit("fixture_ok", "yes") # THE BACKEND IS READ FROM THE FIXTURE, not hard-coded into the call. A frontend # parses pyproject.toml for [build-system].build-backend and hands the string to # the caller; doing the same here is what makes this a test of the indirection. backend = None for line in open(os.path.join(proj, "pyproject.toml")): if line.startswith("build-backend"): backend = line.split("=", 1)[1].strip().strip('"') break emit("backend_declared", backend) try: hooks = pyproject_hooks.BuildBackendHookCaller(proj, backend) except Exception as exc: # noqa: BLE001 fail("caller_error", exc) emit("caller_ok", "yes") def rfc822(text): """Parse the leading header block; the first blank line ends it.""" out = {} for line in text.split("\n"): if not line.strip(): break if ":" in line and not line[:1].isspace(): key, _, val = line.partition(":") out.setdefault(key.strip().lower(), val.strip()) return out # ---------------------------------------------------- the hook start budget # EVERY HOOK CALL IS A FRESH python3 START, and on this arm a start is tens of # seconds to minutes. The host ends a guest that sustains memory growth for # 120s (brintos/linux#21, brintos/hardwarejs#20), so a suite that cannot finish # inside that window is killed before it reports anything -- which is what # happened to this one on 2026-08-31 and again on 2026-09-01, in the sweep and # on the isolated re-boot both times. # # So the default run takes ONE hook: build_wheel, which dispatches through the # same _in_process.py subprocess as the other two and is the deliverable. The # other two hooks are opt-in via BST_PH_ALL_HOOKS=1, and the keys they emit are # simply absent without it -- the suite reads hooks_deferred and skips those # cases rather than reding them. ALL_HOOKS = os.environ.get("BST_PH_ALL_HOOKS", "") == "1" emit("hooks_deferred", "no" if ALL_HOOKS else "yes") if ALL_HOOKS: # ------------------------------------------------------- hook 1: get_requires # A HOOK START, run only under BST_PH_ALL_HOOKS. The first hook call is also the # first subprocess, so a platform # that cannot spawn one reds HERE and nowhere earlier -- which is why the error # text is emitted rather than only the failure. try: requires = hooks.get_requires_for_build_wheel() except Exception as exc: # noqa: BLE001 fail("requires_error", exc) emit("requires_type", type(requires).__name__) emit("requires_count", len(requires)) emit("requires", ",".join(requires)) # --------------------------------------------- hook 2: prepare_metadata # A HOOK START, run only under BST_PH_ALL_HOOKS. The hook writes a .dist-info # directory and returns its BASENAME. # flit_core implements it natively, so no fallback build_wheel is triggered and # this really is a metadata-only call. metadir = os.path.join(W, "metadata") os.makedirs(metadir, exist_ok=True) try: di_name = hooks.prepare_metadata_for_build_wheel(metadir) except Exception as exc: # noqa: BLE001 fail("prepare_metadata_error", exc) emit("metadata_dir_name", di_name) di_path = os.path.join(metadir, di_name) emit("metadata_dir_exists", "yes" if os.path.isdir(di_path) else "no") if os.path.isdir(di_path): emit("metadata_dir_files", ",".join(sorted(os.listdir(di_path)))) try: with open(os.path.join(di_path, "METADATA"), encoding="utf-8") as fh: pm = rfc822(fh.read()) except Exception as exc: # noqa: BLE001 fail("metadata_read_error", exc) emit("prepared_name", pm.get("name", "")) emit("prepared_version", pm.get("version", "")) emit("prepared_summary", pm.get("summary", "")) # ------------------------------------------------------- hook 3: build_wheel # THE ONE HOOK START THE DEFAULT RUN TAKES, and the deliverable: it dispatches # through the same _in_process.py subprocess as the two above, so the mechanism # under test is measured even when they are deferred. Unlike flit_core's own # buildapi, the hook # caller does NOT require the CWD to be the project: it passes cwd= to the # subprocess itself. Leaving the CWD alone here is therefore an assertion in its # own right -- a caller that had silently required a chdir would fail. outdir = os.path.join(W, "wheelhouse") os.makedirs(outdir, exist_ok=True) try: wheel_name = hooks.build_wheel(outdir) except Exception as exc: # noqa: BLE001 fail("build_wheel_error", exc) emit("wheel_name", wheel_name) emit("cwd_unchanged", "yes" if os.path.abspath(os.getcwd()) != os.path.abspath(proj) else "no") # PEP 517 requires the hook to return the BASENAME of what it wrote, and the # suite resolves the path from it. A backend returning an absolute path, or a # name that is not on disk, is a real defect and is visible as wheel_exists=no. wheel_path = os.path.join(outdir, wheel_name) emit("wheel_exists", "yes" if os.path.isfile(wheel_path) else "no") if not os.path.isfile(wheel_path): raise SystemExit(1) emit("wheel_size", os.path.getsize(wheel_path)) # ---------------------------------------------------------- the wheel is a zip emit("wheel_is_zip", "yes" if zipfile.is_zipfile(wheel_path) else "no") try: zf = zipfile.ZipFile(wheel_path) except Exception as exc: # noqa: BLE001 fail("zip_open_error", exc) with zf: # testzip() re-reads and CRCs every member, so this is the case that says the # deflate stream is intact rather than merely that the central directory # parses. It returns the first bad name, or None. try: bad = zf.testzip() except Exception as exc: # noqa: BLE001 fail("zip_test_error", exc) emit("zip_intact", "yes" if bad is None else "corrupt:%s" % bad) names = sorted(zf.namelist()) emit("zip_entry_count", len(names)) emit("zip_entries", ",".join(names)) emit("zip_has_module", "yes" if (NAME + ".py") in names else "no") dist_info = sorted({n.split("/", 1)[0] for n in names if "/" in n and n.split("/", 1)[0].endswith(".dist-info")}) emit("dist_info_dir", dist_info[0] if len(dist_info) == 1 else ",".join(dist_info)) if len(dist_info) != 1: raise SystemExit(1) di = dist_info[0] # THE TWO HOOKS AGREE. prepare_metadata_for_build_wheel and build_wheel are # separate code paths in the backend and a frontend may call either; a # dist-info name that differed between them would make a frontend that # prepared metadata and then built a wheel produce two names for one project. # Only when hook 2 ran: without it there is no prepared name to compare against. if ALL_HOOKS: emit("dist_info_agrees", "yes" if di == di_name else "no:%s vs %s" % (di, di_name)) try: metadata = zf.read(di + "/METADATA").decode("utf-8") except Exception as exc: # noqa: BLE001 fail("metadata_error", exc) fields = rfc822(metadata) emit("metadata_name", fields.get("name", "")) emit("metadata_version", fields.get("version", "")) emit("metadata_summary", fields.get("summary", "")) emit("metadata_version_field", fields.get("metadata-version", "")) try: record = zf.read(di + "/RECORD").decode("utf-8") except Exception as exc: # noqa: BLE001 fail("record_error", exc) # RECORD is CSV: ,,. Only the path column is asserted -- # the digest is over bytes this script wrote, so pinning it here would be # pinning the fixture rather than the backend. paths = [ln.split(",", 1)[0] for ln in record.split("\n") if ln.strip()] emit("record_entry_count", len(paths)) emit("record_has_module", "yes" if (NAME + ".py") in paths else "no") emit("record_has_metadata", "yes" if (di + "/METADATA") in paths else "no") emit("record_has_itself", "yes" if (di + "/RECORD") in paths else "no") try: wheel_meta = zf.read(di + "/WHEEL").decode("utf-8") except Exception as exc: # noqa: BLE001 fail("wheel_meta_error", exc) wfields = {} for line in wheel_meta.split("\n"): if ":" in line: key, _, val = line.partition(":") wfields.setdefault(key.strip().lower(), []).append(val.strip()) emit("wheel_generator", wfields.get("generator", [""])[0]) # Every Tag line, comma-joined in file order: a universal wheel carries one # per tag and the set is the assertion, not whichever happened to be first. # py3-none-any says the backend asked for no ABI and no platform -- the only # answer this arm could satisfy. emit("wheel_tag", ",".join(wfields.get("tag", [""]))) emit("wheel_is_purelib", wfields.get("root-is-purelib", [""])[0]) emit("done", "yes")