blob: 9ada83ff3e82affcf86ab77fdba6d52fefe0c92f [file] [log] [blame]
Mike Frysingere0728a52022-12-08 01:39:02 -05001# Copyright (C) 2021 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Helper tool for generating manual page for all repo commands.
16
17Most code lives in this module so it can be unittested.
18"""
19
Mike Frysingere0728a52022-12-08 01:39:02 -050020import argparse
Mike Frysinger06ddc8c2023-08-21 21:26:51 -040021import functools
Mike Frysingere0728a52022-12-08 01:39:02 -050022import multiprocessing
23import os
Mike Frysinger64477332023-08-21 21:20:32 -040024from pathlib import Path
Mike Frysingere0728a52022-12-08 01:39:02 -050025import re
26import shutil
27import subprocess
28import sys
29import tempfile
Mike Frysinger21cbcc52025-04-22 14:10:52 -040030from typing import List
Mike Frysingere0728a52022-12-08 01:39:02 -050031
Mike Frysinger64477332023-08-21 21:20:32 -040032
Mike Frysinger21cbcc52025-04-22 14:10:52 -040033THIS_FILE = Path(__file__).resolve()
34TOPDIR = THIS_FILE.parent.parent
Gavin Makea2e3302023-03-11 06:46:20 +000035MANDIR = TOPDIR.joinpath("man")
Mike Frysingere0728a52022-12-08 01:39:02 -050036
37# Load repo local modules.
38sys.path.insert(0, str(TOPDIR))
39from git_command import RepoSourceVersion
40import subcmds
41
Gavin Makea2e3302023-03-11 06:46:20 +000042
Mike Frysingere0728a52022-12-08 01:39:02 -050043def worker(cmd, **kwargs):
Gavin Makea2e3302023-03-11 06:46:20 +000044 subprocess.run(cmd, **kwargs)
45
Mike Frysingere0728a52022-12-08 01:39:02 -050046
Mike Frysinger21cbcc52025-04-22 14:10:52 -040047def get_parser() -> argparse.ArgumentParser:
48 """Get argument parser."""
Gavin Makea2e3302023-03-11 06:46:20 +000049 parser = argparse.ArgumentParser(description=__doc__)
Mike Frysinger21cbcc52025-04-22 14:10:52 -040050 parser.add_argument(
51 "-n",
52 "--check",
53 "--dry-run",
54 action="store_const",
55 const=True,
56 help="Check if changes are necessary; don't actually change files",
57 )
58 return parser
59
60
61def main(argv: List[str]) -> int:
62 parser = get_parser()
63 opts = parser.parse_args(argv)
Mike Frysingere0728a52022-12-08 01:39:02 -050064
Gavin Makea2e3302023-03-11 06:46:20 +000065 if not shutil.which("help2man"):
66 sys.exit("Please install help2man to continue.")
Mike Frysingere0728a52022-12-08 01:39:02 -050067
Gavin Makea2e3302023-03-11 06:46:20 +000068 # Let repo know we're generating man pages so it can avoid some dynamic
69 # behavior (like probing active number of CPUs). We use a weird name &
70 # value to make it less likely for users to set this var themselves.
71 os.environ["_REPO_GENERATE_MANPAGES_"] = " indeed! "
Mike Frysingere0728a52022-12-08 01:39:02 -050072
Gavin Makea2e3302023-03-11 06:46:20 +000073 # "repo branch" is an alias for "repo branches".
74 del subcmds.all_commands["branch"]
75 (MANDIR / "repo-branch.1").write_text(".so man1/repo-branches.1")
Mike Frysingere0728a52022-12-08 01:39:02 -050076
Gavin Makea2e3302023-03-11 06:46:20 +000077 version = RepoSourceVersion()
78 cmdlist = [
79 [
80 "help2man",
81 "-N",
82 "-n",
83 f"repo {cmd} - manual page for repo {cmd}",
84 "-S",
85 f"repo {cmd}",
86 "-m",
87 "Repo Manual",
88 f"--version-string={version}",
89 "-o",
90 MANDIR.joinpath(f"repo-{cmd}.1.tmp"),
91 "./repo",
92 "-h",
93 f"help {cmd}",
94 ]
95 for cmd in subcmds.all_commands
96 ]
97 cmdlist.append(
98 [
99 "help2man",
100 "-N",
101 "-n",
102 "repository management tool built on top of git",
103 "-S",
104 "repo",
105 "-m",
106 "Repo Manual",
107 f"--version-string={version}",
108 "-o",
109 MANDIR.joinpath("repo.1.tmp"),
110 "./repo",
111 "-h",
112 "--help-all",
113 ]
114 )
Mike Frysingere0728a52022-12-08 01:39:02 -0500115
Gavin Makea2e3302023-03-11 06:46:20 +0000116 with tempfile.TemporaryDirectory() as tempdir:
117 tempdir = Path(tempdir)
118 repo_dir = tempdir / ".repo"
119 repo_dir.mkdir()
120 (repo_dir / "repo").symlink_to(TOPDIR)
Mike Frysingere0728a52022-12-08 01:39:02 -0500121
Gavin Makea2e3302023-03-11 06:46:20 +0000122 # Create a repo wrapper using the active Python executable. We can't
123 # pass this directly to help2man as it's too simple, so insert it via
124 # shebang.
125 data = (TOPDIR / "repo").read_text(encoding="utf-8")
126 tempbin = tempdir / "repo"
127 tempbin.write_text(f"#!{sys.executable}\n" + data, encoding="utf-8")
128 tempbin.chmod(0o755)
Mike Frysingere0728a52022-12-08 01:39:02 -0500129
Gavin Makea2e3302023-03-11 06:46:20 +0000130 # Run all cmd in parallel, and wait for them to finish.
131 with multiprocessing.Pool() as pool:
Mike Frysinger06ddc8c2023-08-21 21:26:51 -0400132 pool.map(
133 functools.partial(worker, cwd=tempdir, check=True), cmdlist
134 )
Mike Frysingere0728a52022-12-08 01:39:02 -0500135
Mike Frysinger21cbcc52025-04-22 14:10:52 -0400136 ret = 0
Gavin Makea2e3302023-03-11 06:46:20 +0000137 for tmp_path in MANDIR.glob("*.1.tmp"):
138 path = tmp_path.parent / tmp_path.stem
139 old_data = path.read_text() if path.exists() else ""
Mike Frysingere0728a52022-12-08 01:39:02 -0500140
Gavin Makea2e3302023-03-11 06:46:20 +0000141 data = tmp_path.read_text()
142 tmp_path.unlink()
Mike Frysingere0728a52022-12-08 01:39:02 -0500143
Gavin Makea2e3302023-03-11 06:46:20 +0000144 data = replace_regex(data)
Mike Frysingere0728a52022-12-08 01:39:02 -0500145
Gavin Makea2e3302023-03-11 06:46:20 +0000146 # If the only thing that changed was the date, don't refresh. This
147 # avoids a lot of noise when only one file actually updates.
148 old_data = re.sub(
149 r'^(\.TH REPO "1" ")([^"]+)', r"\1", old_data, flags=re.M
150 )
151 new_data = re.sub(r'^(\.TH REPO "1" ")([^"]+)', r"\1", data, flags=re.M)
152 if old_data != new_data:
Mike Frysinger21cbcc52025-04-22 14:10:52 -0400153 if opts.check:
154 ret = 1
155 print(
156 f"{THIS_FILE.name}: {path.name}: "
157 "man page needs regenerating",
158 file=sys.stderr,
159 )
160 else:
161 path.write_text(data)
162
163 return ret
Mike Frysingere0728a52022-12-08 01:39:02 -0500164
165
166def replace_regex(data):
Gavin Makea2e3302023-03-11 06:46:20 +0000167 """Replace semantically null regexes in the data.
Mike Frysingere0728a52022-12-08 01:39:02 -0500168
Gavin Makea2e3302023-03-11 06:46:20 +0000169 Args:
170 data: manpage text.
Mike Frysingere0728a52022-12-08 01:39:02 -0500171
Gavin Makea2e3302023-03-11 06:46:20 +0000172 Returns:
173 Updated manpage text.
174 """
175 regex = (
176 (r"(It was generated by help2man) [0-9.]+", r"\g<1>."),
177 (r"^\033\[[0-9;]*m([^\033]*)\033\[m", r"\g<1>"),
178 (r"^\.IP\n(.*:)\n", r".SS \g<1>\n"),
179 (r"^\.PP\nDescription", r".SH DETAILS"),
180 )
181 for pattern, replacement in regex:
182 data = re.sub(pattern, replacement, data, flags=re.M)
183 return data