Skip to content

Commit 7270b15

Browse files
committed
Add make.py script to simplify build/rebuild/tests tasks
1 parent 058c916 commit 7270b15

File tree

2 files changed

+199
-1
lines changed

2 files changed

+199
-1
lines changed

make.py

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
#! /usr/bin/env python
2+
3+
from __future__ import annotations
4+
5+
import argparse
6+
import itertools
7+
import os
8+
import random
9+
import shutil
10+
import subprocess
11+
import sys
12+
import textwrap
13+
from pathlib import Path
14+
from typing import Iterable, Union
15+
16+
17+
BASE_DIR = Path(__file__).parent.resolve()
18+
# Use relative path by default for cleaner help output (BASE_DIR is used as cwd anyway)
19+
BUILD_DIR = os.environ.get("BUILD_DIR", "build")
20+
21+
22+
CYAN = "\x1b[36m"
23+
GREY = "\x1b[37m"
24+
PINK = "\x1b[35m"
25+
BOLD_YELLOW = "\x1b[1;33m"
26+
NO_COLOR = "\x1b[0;0m"
27+
CUTENESS = [
28+
"ฅ^◕ﻌ◕^ฅ",
29+
"(^・㉨・^)∫",
30+
"^•ﻌ•^ฅ",
31+
"✺◟(⏓ᴥ⏓▽)◞✺",
32+
"ฅ(・㉨・˶)ฅ",
33+
"(ミꆤ ﻌ ꆤミ)∫",
34+
"(ミㆁ㉨ㆁミ)",
35+
"ฅ(=ච ω ච=)",
36+
]
37+
38+
39+
class Op:
40+
def display(self, extra_cmd_args: Iterable[str]) -> str:
41+
raise NotImplementedError
42+
43+
def run(self, cwd: Path, extra_cmd_args: Iterable[str]) -> None:
44+
raise NotImplementedError
45+
46+
47+
class Cwd(Op):
48+
def __init__(self, cwd: Path) -> None:
49+
self.cwd = cwd
50+
51+
def display(self, extra_cmd_args: Iterable[str]) -> str:
52+
return f"cd {GREY}{self.cwd.relative_to(BASE_DIR).as_posix()}{NO_COLOR}"
53+
54+
55+
class Rmdir(Op):
56+
def __init__(self, target: Path) -> None:
57+
self.target = target
58+
59+
def display(self, extra_cmd_args: Iterable[str]) -> str:
60+
return f"{CYAN}rm -rf {self.target.relative_to(BASE_DIR).as_posix()}{NO_COLOR}"
61+
62+
def run(self, cwd: Path, extra_cmd_args: Iterable[str]) -> None:
63+
target = self.target if self.target.is_absolute() else cwd / self.target
64+
shutil.rmtree(target, ignore_errors=True)
65+
66+
67+
class Echo(Op):
68+
def __init__(self, msg: str) -> None:
69+
self.msg = msg
70+
71+
def display(self, extra_cmd_args: Iterable[str]) -> str:
72+
return f"{CYAN}echo {self.msg!r}{NO_COLOR}"
73+
74+
def run(self, cwd: Path, extra_cmd_args: Iterable[str]) -> None:
75+
print(self.msg, flush=True)
76+
77+
78+
class Cmd(Op):
79+
def __init__(
80+
self,
81+
cmd: str,
82+
extra_env: dict[str, str] = {},
83+
) -> None:
84+
self.cmd = cmd
85+
self.extra_env = extra_env
86+
87+
def cmd_with_extra_cmd_args(self, extra_cmd_args: Iterable[str]) -> str:
88+
cooked_extra_cmds_args = " ".join(extra_cmd_args) if extra_cmd_args else ""
89+
if "{extra_cmd_args}" in self.cmd:
90+
return self.cmd.format(extra_cmd_args=cooked_extra_cmds_args)
91+
else:
92+
return f"{self.cmd} {cooked_extra_cmds_args}"
93+
94+
def display(self, extra_cmd_args: Iterable[str]) -> str:
95+
display_extra_env = " ".join(
96+
[f"{GREY}{k}={v}{NO_COLOR}" for k, v in self.extra_env.items()]
97+
)
98+
cmd = self.cmd_with_extra_cmd_args(extra_cmd_args)
99+
return f"{display_extra_env} {CYAN}{cmd}{NO_COLOR}"
100+
101+
def run(self, cwd: Path, extra_cmd_args: Iterable[str]) -> None:
102+
args = self.cmd_with_extra_cmd_args(extra_cmd_args).split()
103+
subprocess.check_call(
104+
args,
105+
env={**os.environ, **self.extra_env},
106+
cwd=cwd,
107+
)
108+
109+
110+
COMMANDS: dict[tuple[str, ...], Union[Op, tuple[Op, ...]]] = {
111+
("init", "i"): (
112+
Cmd(f"meson setup {BUILD_DIR}"),
113+
Cmd(f"meson compile -C {BUILD_DIR}"),
114+
),
115+
("rebuild", "r"): (Cmd(f"meson compile -C {BUILD_DIR}"),),
116+
("tests", "t"): (
117+
Cmd(f"python tests/run.py --build-dir={BUILD_DIR} {{extra_cmd_args}} -- --headless"),
118+
),
119+
}
120+
121+
122+
if __name__ == "__main__":
123+
parser = argparse.ArgumentParser(
124+
formatter_class=argparse.RawDescriptionHelpFormatter,
125+
description=textwrap.dedent(
126+
"""\
127+
Tired of remembering multiple silly commands ? Now here is a single silly command to remember !
128+
129+
Examples:
130+
python make.py init # Initial setup & build
131+
python make.py rebuild # Subsequent build
132+
python make.py tests -- --headless # Additional args passed to subcommand
133+
"""
134+
),
135+
)
136+
parser.add_argument("--quiet", "-q", action="store_true", help="🎵 The sound of silence 🎵")
137+
parser.add_argument("--dry", action="store_true", help="Don't actually run, just display")
138+
parser.add_argument(
139+
"command",
140+
help="The command to run",
141+
nargs="?",
142+
choices=list(itertools.chain.from_iterable(COMMANDS.keys())),
143+
metavar="command",
144+
)
145+
146+
# Handle `-- <extra_cmd_args>` in argv
147+
# (argparse doesn't understand `--`, so we have to implement it by hand)
148+
has_reached_cmd_extra_args = False
149+
extra_cmd_args = []
150+
argv = []
151+
for arg in sys.argv[1:]:
152+
if has_reached_cmd_extra_args:
153+
extra_cmd_args.append(arg)
154+
elif arg == "--":
155+
has_reached_cmd_extra_args = True
156+
else:
157+
argv.append(arg)
158+
159+
args = parser.parse_args(argv)
160+
if not args.command:
161+
print("Available commands:\n")
162+
for aliases, cmds in COMMANDS.items():
163+
print(f"{BOLD_YELLOW}{', '.join(aliases)}{NO_COLOR}")
164+
cmds = (cmds,) if isinstance(cmds, Op) else cmds
165+
display_cmds = [cmd.display(extra_cmd_args) for cmd in cmds]
166+
join = f"{GREY}; and {NO_COLOR}" if "fish" in os.environ.get("SHELL", "") else " && "
167+
print(f"\t{join.join(display_cmds)}\n")
168+
169+
else:
170+
for aliases, cmds in COMMANDS.items():
171+
if args.command in aliases:
172+
cmds = (cmds,) if isinstance(cmds, Op) else cmds
173+
break
174+
else:
175+
raise SystemExit(f"Unknown command alias `{args.command}`")
176+
177+
cwd = BASE_DIR
178+
for cmd in cmds:
179+
if not args.quiet:
180+
# Flush is required to prevent mixing with the output of sub-command
181+
print(f"{cmd.display(extra_cmd_args)}\n", flush=True)
182+
if not isinstance(cmd, Cwd):
183+
try:
184+
print(f"{PINK}{random.choice(CUTENESS)}{NO_COLOR}", flush=True)
185+
except UnicodeEncodeError:
186+
# Windows crappy term couldn't encode kitty unicode :'(
187+
pass
188+
189+
if args.dry:
190+
continue
191+
192+
if isinstance(cmd, Cwd):
193+
cwd = cmd.cwd
194+
else:
195+
try:
196+
cmd.run(cwd, extra_cmd_args)
197+
except subprocess.CalledProcessError as err:
198+
raise SystemExit(str(err)) from err

tests/run.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ def run_test(
178178

179179
if __name__ == "__main__":
180180
parser = argparse.ArgumentParser()
181-
parser.add_argument("tests", nargs="*", help="Filter the tests to run", default=["helloworld"])
181+
parser.add_argument("tests", nargs="*", help="Filter the tests to run")
182182
parser.add_argument(
183183
"--build-dir",
184184
type=Path,

0 commit comments

Comments
 (0)