Coverage for src/rhiza_hooks/check_makefile_targets.py: 100%
42 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-15 06:14 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-15 06:14 +0000
1#!/usr/bin/env python3
2"""Check that Makefile contains expected targets for rhiza projects.
4A catch-all rule satisfies every recommended target at once. Since rhiza v1.4.0 the
5root Makefile is a ``rhiza-task`` shim that defines only ``help`` and forwards every
6other goal through ``%:``, so ``make test`` works with no ``test:`` rule in sight;
7reporting ``fmt``, ``install`` and ``test`` missing there is a false report on a
8Makefile that is correct, and under ``--strict`` it blocks the commit. The rule is
9applied by :meth:`rhiza_hooks._makefile.MakefileTargets.defines`, shared with
10``check-workflow-make-targets`` so the two agree on what "defined" means.
11"""
13from __future__ import annotations
15import argparse
16import sys
17from pathlib import Path
19from rhiza_hooks._makefile import extract_targets
21# Common targets expected in rhiza-based projects
22RECOMMENDED_TARGETS = {
23 "install",
24 "test",
25 "fmt",
26 "help",
27}
30def check_makefile(filepath: Path, recommended: set[str] = RECOMMENDED_TARGETS) -> list[str]:
31 """Check a Makefile for recommended targets.
33 Args:
34 filepath: Path to the Makefile
35 recommended: Target names that must be present (defaults to RECOMMENDED_TARGETS)
37 Returns:
38 List of warning messages (empty if all recommended targets exist)
39 """
40 warnings: list[str] = []
42 try:
43 content = filepath.read_text(encoding="utf-8")
44 except FileNotFoundError:
45 return [f"File not found: {filepath}"]
47 targets = extract_targets(content)
49 # Only check the main Makefile for recommended targets
50 if filepath.name == "Makefile":
51 missing = {target for target in recommended if not targets.defines(target)}
52 if missing:
53 warnings.append(f"Missing recommended targets: {', '.join(sorted(missing))}")
55 return warnings
58def resolve_recommended_targets(targets: list[str] | None, extra_targets: list[str] | None) -> set[str]:
59 """Build the effective set of required targets from the CLI options.
61 Args:
62 targets: Values of ``--target``. When non-empty they *replace* the defaults.
63 extra_targets: Values of ``--extend-target``, always *added* to the active set.
65 Returns:
66 The set of target names a Makefile is expected to define.
68 With neither option, the built-in defaults apply:
70 >>> sorted(resolve_recommended_targets(None, None))
71 ['fmt', 'help', 'install', 'test']
73 ``--target`` *replaces* them, so a project with its own vocabulary is not
74 also held to the defaults:
76 >>> sorted(resolve_recommended_targets(["build"], None))
77 ['build']
79 ``--extend-target`` *adds* to whichever set is active:
81 >>> sorted(resolve_recommended_targets(None, ["deploy"]))
82 ['deploy', 'fmt', 'help', 'install', 'test']
83 >>> sorted(resolve_recommended_targets(["build"], ["deploy"]))
84 ['build', 'deploy']
85 """
86 base = set(targets) if targets else set(RECOMMENDED_TARGETS)
87 return base | set(extra_targets or [])
90def main(argv: list[str] | None = None) -> int:
91 """Main entry point for the hook."""
92 parser = argparse.ArgumentParser(description="Check Makefile for recommended targets")
93 parser.add_argument(
94 "filenames",
95 nargs="*",
96 help="Filenames to check",
97 )
98 parser.add_argument(
99 "--strict",
100 action="store_true",
101 help="Exit with error if recommended targets are missing",
102 )
103 parser.add_argument(
104 "--target",
105 action="append",
106 metavar="NAME",
107 help="Required target name; repeatable. When given, replaces the default set.",
108 )
109 parser.add_argument(
110 "--extend-target",
111 action="append",
112 metavar="NAME",
113 help="Extra required target name; repeatable. Added on top of the active set.",
114 )
115 args = parser.parse_args(argv)
117 recommended = resolve_recommended_targets(args.target, args.extend_target)
119 retval = 0
120 for filename in args.filenames:
121 filepath = Path(filename)
122 warnings = check_makefile(filepath, recommended)
123 if warnings:
124 print(f"{filename}:", file=sys.stderr)
125 for warning in warnings:
126 print(f" - {warning}", file=sys.stderr)
127 if args.strict:
128 retval = 1
130 return retval
133if __name__ == "__main__": # pragma: no mutate
134 sys.exit(main())