blob: 489de3570b2144d3e9a9776c263e097e83a3393c [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
30
Mike Frysinger64477332023-08-21 21:20:32 -040031
Mike Frysingere0728a52022-12-08 01:39:02 -050032TOPDIR = Path(__file__).resolve().parent.parent
Gavin Makea2e3302023-03-11 06:46:20 +000033MANDIR = TOPDIR.joinpath("man")
Mike Frysingere0728a52022-12-08 01:39:02 -050034
35# Load repo local modules.
36sys.path.insert(0, str(TOPDIR))
37from git_command import RepoSourceVersion
38import subcmds
39
Gavin Makea2e3302023-03-11 06:46:20 +000040
Mike Frysingere0728a52022-12-08 01:39:02 -050041def worker(cmd, **kwargs):
Gavin Makea2e3302023-03-11 06:46:20 +000042 subprocess.run(cmd, **kwargs)
43
Mike Frysingere0728a52022-12-08 01:39:02 -050044
45def main(argv):
Gavin Makea2e3302023-03-11 06:46:20 +000046 parser = argparse.ArgumentParser(description=__doc__)
47 parser.parse_args(argv)
Mike Frysingere0728a52022-12-08 01:39:02 -050048
Gavin Makea2e3302023-03-11 06:46:20 +000049 if not shutil.which("help2man"):
50 sys.exit("Please install help2man to continue.")
Mike Frysingere0728a52022-12-08 01:39:02 -050051
Gavin Makea2e3302023-03-11 06:46:20 +000052 # Let repo know we're generating man pages so it can avoid some dynamic
53 # behavior (like probing active number of CPUs). We use a weird name &
54 # value to make it less likely for users to set this var themselves.
55 os.environ["_REPO_GENERATE_MANPAGES_"] = " indeed! "
Mike Frysingere0728a52022-12-08 01:39:02 -050056
Gavin Makea2e3302023-03-11 06:46:20 +000057 # "repo branch" is an alias for "repo branches".
58 del subcmds.all_commands["branch"]
59 (MANDIR / "repo-branch.1").write_text(".so man1/repo-branches.1")
Mike Frysingere0728a52022-12-08 01:39:02 -050060
Gavin Makea2e3302023-03-11 06:46:20 +000061 version = RepoSourceVersion()
62 cmdlist = [
63 [
64 "help2man",
65 "-N",
66 "-n",
67 f"repo {cmd} - manual page for repo {cmd}",
68 "-S",
69 f"repo {cmd}",
70 "-m",
71 "Repo Manual",
72 f"--version-string={version}",
73 "-o",
74 MANDIR.joinpath(f"repo-{cmd}.1.tmp"),
75 "./repo",
76 "-h",
77 f"help {cmd}",
78 ]
79 for cmd in subcmds.all_commands
80 ]
81 cmdlist.append(
82 [
83 "help2man",
84 "-N",
85 "-n",
86 "repository management tool built on top of git",
87 "-S",
88 "repo",
89 "-m",
90 "Repo Manual",
91 f"--version-string={version}",
92 "-o",
93 MANDIR.joinpath("repo.1.tmp"),
94 "./repo",
95 "-h",
96 "--help-all",
97 ]
98 )
Mike Frysingere0728a52022-12-08 01:39:02 -050099
Gavin Makea2e3302023-03-11 06:46:20 +0000100 with tempfile.TemporaryDirectory() as tempdir:
101 tempdir = Path(tempdir)
102 repo_dir = tempdir / ".repo"
103 repo_dir.mkdir()
104 (repo_dir / "repo").symlink_to(TOPDIR)
Mike Frysingere0728a52022-12-08 01:39:02 -0500105
Gavin Makea2e3302023-03-11 06:46:20 +0000106 # Create a repo wrapper using the active Python executable. We can't
107 # pass this directly to help2man as it's too simple, so insert it via
108 # shebang.
109 data = (TOPDIR / "repo").read_text(encoding="utf-8")
110 tempbin = tempdir / "repo"
111 tempbin.write_text(f"#!{sys.executable}\n" + data, encoding="utf-8")
112 tempbin.chmod(0o755)
Mike Frysingere0728a52022-12-08 01:39:02 -0500113
Gavin Makea2e3302023-03-11 06:46:20 +0000114 # Run all cmd in parallel, and wait for them to finish.
115 with multiprocessing.Pool() as pool:
Mike Frysinger06ddc8c2023-08-21 21:26:51 -0400116 pool.map(
117 functools.partial(worker, cwd=tempdir, check=True), cmdlist
118 )
Mike Frysingere0728a52022-12-08 01:39:02 -0500119
Gavin Makea2e3302023-03-11 06:46:20 +0000120 for tmp_path in MANDIR.glob("*.1.tmp"):
121 path = tmp_path.parent / tmp_path.stem
122 old_data = path.read_text() if path.exists() else ""
Mike Frysingere0728a52022-12-08 01:39:02 -0500123
Gavin Makea2e3302023-03-11 06:46:20 +0000124 data = tmp_path.read_text()
125 tmp_path.unlink()
Mike Frysingere0728a52022-12-08 01:39:02 -0500126
Gavin Makea2e3302023-03-11 06:46:20 +0000127 data = replace_regex(data)
Mike Frysingere0728a52022-12-08 01:39:02 -0500128
Gavin Makea2e3302023-03-11 06:46:20 +0000129 # If the only thing that changed was the date, don't refresh. This
130 # avoids a lot of noise when only one file actually updates.
131 old_data = re.sub(
132 r'^(\.TH REPO "1" ")([^"]+)', r"\1", old_data, flags=re.M
133 )
134 new_data = re.sub(r'^(\.TH REPO "1" ")([^"]+)', r"\1", data, flags=re.M)
135 if old_data != new_data:
136 path.write_text(data)
Mike Frysingere0728a52022-12-08 01:39:02 -0500137
138
139def replace_regex(data):
Gavin Makea2e3302023-03-11 06:46:20 +0000140 """Replace semantically null regexes in the data.
Mike Frysingere0728a52022-12-08 01:39:02 -0500141
Gavin Makea2e3302023-03-11 06:46:20 +0000142 Args:
143 data: manpage text.
Mike Frysingere0728a52022-12-08 01:39:02 -0500144
Gavin Makea2e3302023-03-11 06:46:20 +0000145 Returns:
146 Updated manpage text.
147 """
148 regex = (
149 (r"(It was generated by help2man) [0-9.]+", r"\g<1>."),
150 (r"^\033\[[0-9;]*m([^\033]*)\033\[m", r"\g<1>"),
151 (r"^\.IP\n(.*:)\n", r".SS \g<1>\n"),
152 (r"^\.PP\nDescription", r".SH DETAILS"),
153 )
154 for pattern, replacement in regex:
155 data = re.sub(pattern, replacement, data, flags=re.M)
156 return data