Coverage for plugin/scripts/_make_targets_chain.py: 100%

37 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-10-26 04:21 +0000

1"""The static half of ``check_make_targets.py``: read a repo's makefiles as text. 

2 

3Split from the orchestrator by instrument, the same line ``_make_targets_runner`` was cut 

4along. Everything here reads files and nothing runs: which makefile a repo has, the chain 

5of files it ``include``s, and the targets those files document with the ``## description`` 

6convention. Asking ``make -n`` what resolves is behaviour, and stays with the 

7orchestrator — a makefile can answer a target this parser never sees (a shim's ``%:`` 

8catch-all) and document one make cannot build, so the two answers are kept apart rather 

9than blended. 

10 

11Like ``_make_targets_runner`` it never imports its orchestrator; the dependency runs one 

12way. 

13""" 

14 

15from __future__ import annotations 

16 

17import re 

18from pathlib import Path 

19 

20_MAKEFILES = ("Makefile", "makefile", "GNUmakefile") 

21# A self-documenting target — `test: ## Run the suite` — the convention every rhiza 

22# Makefile uses for `make help`. Undocumented internal targets are deliberately not 

23# matched: they are not gates anyone meant to expose. `test::` (a double-colon rule, 

24# which rust.mk uses) counts too. 

25_DOCUMENTED = re.compile(r"^([a-z][a-z0-9_-]*)::?.*?##\s*(.+)$", re.MULTILINE) 

26# An `include`/`-include` line, with its (possibly glob) operands. 

27_INCLUDE = re.compile(r"^\s*-?include\s+(.+?)\s*$", re.MULTILINE) 

28# How deep to follow includes. Makefile -> .rhiza/rhiza.mk -> .rhiza/make.d/*.mk is two, 

29# so three leaves room without risking a pathological chain. 

30_INCLUDE_DEPTH = 3 

31 

32 

33def find_makefile(target_dir: Path) -> Path | None: 

34 """Return the repo's makefile, or None when there isn't one.""" 

35 return next((target_dir / n for n in _MAKEFILES if (target_dir / n).is_file()), None) 

36 

37 

38def makefile_chain(target_dir: Path, *, depth: int = _INCLUDE_DEPTH) -> list[Path]: 

39 """Return the repo's makefile plus the files it ``include``s, in reading order. 

40 

41 **Reading only the root makefile finds nothing on a pre-v1.4 repo.** Up to template 

42 v1.3 a synced repo's `Makefile` was a stub — a few variables and 

43 `include .rhiza/rhiza.mk` — which in turn ended with `-include .rhiza/make.d/*.mk`, 

44 and *that* is where every gate lived. Probing was unaffected (``make -n`` follows 

45 includes itself), but discovery read one file where make reads a dozen, so a synced 

46 Rust repo reported zero discovered targets while `.rhiza/make.d/rust.mk` was sitting 

47 there defining `deps`, `license` and `coverage`. The mechanism that exists to stop 

48 `/quality` reporting "nothing could be checked" was doing exactly that. 

49 

50 Template v1.4 retired that layer: neither file is shipped any more, the gates are 

51 `rhiza-task` tasks, and the `Makefile` is a shim that forwards to the pinned CLI. 

52 Following includes is kept because a repo may pin any ref, so both shapes are live — 

53 and on a current one the chain is simply the root makefile plus whatever `local.mk` 

54 the repo added itself. 

55 

56 Globs are expanded and each file is visited once. An operand containing `$` is 

57 skipped: it is a make variable this parser cannot resolve, and guessing is worse 

58 than omitting. 

59 """ 

60 root = find_makefile(target_dir) 

61 if root is None: 

62 return [] 

63 chain: list[Path] = [] 

64 seen: set[Path] = set() 

65 

66 def _walk(path: Path, remaining: int) -> None: 

67 resolved = path.resolve() 

68 if resolved in seen or not path.is_file(): 

69 return 

70 seen.add(resolved) 

71 chain.append(path) 

72 if remaining <= 0: 

73 return 

74 for operands in _INCLUDE.findall(path.read_text(encoding="utf-8", errors="ignore")): 

75 for operand in operands.split(): 

76 if "$" in operand: 

77 continue 

78 for included in sorted(target_dir.glob(operand)): 

79 _walk(included, remaining - 1) 

80 

81 _walk(root, depth) 

82 return chain 

83 

84 

85def documented_targets(target_dir: Path) -> dict[str, str]: 

86 """Return the repo's self-documenting `make` targets, mapped to their descriptions. 

87 

88 The gate list in the prose is the **Python** profile. A Go or Rust repo synced from 

89 a sibling template offers a different set, and naming those from a table here would 

90 mean asserting targets for templates this plugin has never seen — the failure mode 

91 that had `/quality` scoring repos against gates that did not exist. 

92 

93 So they are discovered instead, from the ``target: ## description`` convention every 

94 rhiza Makefile uses to build ``make help`` — across the whole include chain, because 

95 that is where make itself looks (see :func:`makefile_chain`). What comes back is what 

96 the repo really offers, whatever language it is. 

97 """ 

98 found: dict[str, str] = {} 

99 for makefile in makefile_chain(target_dir): 

100 for name, description in _DOCUMENTED.findall( 

101 makefile.read_text(encoding="utf-8", errors="ignore") 

102 ): 

103 found.setdefault(name, description.strip()) 

104 return found