|
| 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 |
0 commit comments