summaryrefslogtreecommitdiff
path: root/mesonbuild/scripts
diff options
context:
space:
mode:
authorPaolo Bonzini <pbonzini@redhat.com>2024-11-18 11:13:04 +0100
committerDylan Baker <dylan@pnwbakers.com>2024-12-19 09:25:20 -0800
commit5dc537afd051e60ff00731f73e02d98138d9198b (patch)
tree1a68b81c09dfdb71654ed363e789f14346de3b25 /mesonbuild/scripts
parentdafa6a7ac14f18e5a2529a2251fc6d552ea37547 (diff)
downloadmeson-5dc537afd051e60ff00731f73e02d98138d9198b.tar.gz
scripts: convert run_tool to asyncio
This improves the handling of keyboard interrupt, and also makes it easy to buffer the output and not mix errors from different subprocesses. This is useful for clang-tidy and will be used by clippy as well. In addition, the new code supports MESON_NUM_PROCESSES. Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
Diffstat (limited to 'mesonbuild/scripts')
-rw-r--r--mesonbuild/scripts/clangformat.py9
-rw-r--r--mesonbuild/scripts/clangtidy.py6
-rw-r--r--mesonbuild/scripts/run_tool.py120
3 files changed, 95 insertions, 40 deletions
diff --git a/mesonbuild/scripts/clangformat.py b/mesonbuild/scripts/clangformat.py
index f0d084f2b..a3c19e9ad 100644
--- a/mesonbuild/scripts/clangformat.py
+++ b/mesonbuild/scripts/clangformat.py
@@ -4,17 +4,16 @@
from __future__ import annotations
import argparse
-import subprocess
from pathlib import Path
import sys
-from .run_tool import run_clang_tool
+from .run_tool import run_clang_tool, run_with_buffered_output
from ..environment import detect_clangformat
from ..mesonlib import version_compare
from ..programs import ExternalProgram
import typing as T
-def run_clang_format(fname: Path, exelist: T.List[str], options: argparse.Namespace, cformat_ver: T.Optional[str]) -> subprocess.CompletedProcess:
+async def run_clang_format(fname: Path, exelist: T.List[str], options: argparse.Namespace, cformat_ver: T.Optional[str]) -> int:
clangformat_10 = False
if options.check and cformat_ver:
if version_compare(cformat_ver, '>=10'):
@@ -26,14 +25,14 @@ def run_clang_format(fname: Path, exelist: T.List[str], options: argparse.Namesp
else:
original = fname.read_bytes()
before = fname.stat().st_mtime
- ret = subprocess.run(exelist + ['-style=file', '-i', str(fname)])
+ ret = await run_with_buffered_output(exelist + ['-style=file', '-i', str(fname)])
after = fname.stat().st_mtime
if before != after:
print('File reformatted: ', fname)
if options.check and not clangformat_10:
# Restore the original if only checking.
fname.write_bytes(original)
- ret.returncode = 1
+ return 1
return ret
def run(args: T.List[str]) -> int:
diff --git a/mesonbuild/scripts/clangtidy.py b/mesonbuild/scripts/clangtidy.py
index ab53bbd39..550faeef3 100644
--- a/mesonbuild/scripts/clangtidy.py
+++ b/mesonbuild/scripts/clangtidy.py
@@ -11,17 +11,17 @@ import os
import shutil
import sys
-from .run_tool import run_clang_tool
+from .run_tool import run_clang_tool, run_with_buffered_output
from ..environment import detect_clangtidy, detect_clangapply
import typing as T
-def run_clang_tidy(fname: Path, tidyexe: list, builddir: Path, fixesdir: T.Optional[Path]) -> subprocess.CompletedProcess:
+async def run_clang_tidy(fname: Path, tidyexe: list, builddir: Path, fixesdir: T.Optional[Path]) -> int:
args = []
if fixesdir is not None:
handle, name = tempfile.mkstemp(prefix=fname.name + '.', suffix='.yaml', dir=fixesdir)
os.close(handle)
args.extend(['-export-fixes', name])
- return subprocess.run(tidyexe + args + ['-quiet', '-p', str(builddir), str(fname)])
+ return await run_with_buffered_output(tidyexe + args + ['-quiet', '-p', str(builddir), str(fname)])
def run(args: T.List[str]) -> int:
parser = argparse.ArgumentParser()
diff --git a/mesonbuild/scripts/run_tool.py b/mesonbuild/scripts/run_tool.py
index 2cccb1b33..bccc4cb83 100644
--- a/mesonbuild/scripts/run_tool.py
+++ b/mesonbuild/scripts/run_tool.py
@@ -3,17 +3,85 @@
from __future__ import annotations
-import itertools
+import asyncio.subprocess
import fnmatch
-import concurrent.futures
+import itertools
+import signal
+import sys
from pathlib import Path
+from .. import mlog
from ..compilers import lang_suffixes
-from ..mesonlib import quiet_git
+from ..mesonlib import quiet_git, join_args, determine_worker_count
+from ..mtest import complete_all
import typing as T
-if T.TYPE_CHECKING:
- import subprocess
+Info = T.TypeVar("Info")
+
+async def run_with_buffered_output(cmdlist: T.List[str]) -> int:
+ """Run the command in cmdlist, buffering the output so that it is
+ not mixed for multiple child processes. Kill the child on
+ cancellation."""
+ quoted_cmdline = join_args(cmdlist)
+ p: T.Optional[asyncio.subprocess.Process] = None
+ try:
+ p = await asyncio.create_subprocess_exec(*cmdlist,
+ stdin=asyncio.subprocess.DEVNULL,
+ stdout=asyncio.subprocess.PIPE,
+ stderr=asyncio.subprocess.STDOUT)
+ stdo, _ = await p.communicate()
+ except FileNotFoundError as e:
+ print(mlog.blue('>>>'), quoted_cmdline, file=sys.stderr)
+ print(mlog.red('not found:'), e.filename, file=sys.stderr)
+ return 1
+ except asyncio.CancelledError:
+ if p:
+ p.kill()
+ await p.wait()
+ return p.returncode or 1
+ else:
+ return 0
+
+ if stdo:
+ print(mlog.blue('>>>'), quoted_cmdline, flush=True)
+ sys.stdout.buffer.write(stdo)
+ return p.returncode
+
+async def _run_workers(infos: T.Iterable[Info],
+ fn: T.Callable[[Info], T.Iterable[T.Coroutine[None, None, int]]]) -> int:
+ futures: T.List[asyncio.Future[int]] = []
+ semaphore = asyncio.Semaphore(determine_worker_count())
+
+ async def run_one(worker_coro: T.Coroutine[None, None, int]) -> int:
+ try:
+ async with semaphore:
+ return await worker_coro
+ except asyncio.CancelledError as e:
+ worker_coro.throw(e)
+ return await worker_coro
+
+ def sigterm_handler() -> None:
+ for f in futures:
+ f.cancel()
+
+ if sys.platform != 'win32':
+ loop = asyncio.get_running_loop()
+ loop.add_signal_handler(signal.SIGINT, sigterm_handler)
+ loop.add_signal_handler(signal.SIGTERM, sigterm_handler)
+
+ for i in infos:
+ futures.extend((asyncio.ensure_future(run_one(x)) for x in fn(i)))
+ if not futures:
+ return 0
+
+ try:
+ await complete_all(futures)
+ except BaseException:
+ for f in futures:
+ f.cancel()
+ raise
+
+ return max(f.result() for f in futures if f.done() and not f.cancelled())
def parse_pattern_file(fname: Path) -> T.List[str]:
patterns = []
@@ -27,7 +95,7 @@ def parse_pattern_file(fname: Path) -> T.List[str]:
pass
return patterns
-def run_clang_tool(name: str, srcdir: Path, builddir: Path, fn: T.Callable[..., subprocess.CompletedProcess], *args: T.Any) -> int:
+def all_clike_files(name: str, srcdir: Path, builddir: Path) -> T.Iterable[Path]:
patterns = parse_pattern_file(srcdir / f'.{name}-include')
globs: T.Union[T.List[T.List[Path]], T.List[T.Generator[Path, None, None]]]
if patterns:
@@ -44,29 +112,17 @@ def run_clang_tool(name: str, srcdir: Path, builddir: Path, fn: T.Callable[...,
suffixes = set(lang_suffixes['c']).union(set(lang_suffixes['cpp']))
suffixes.add('h')
suffixes = {f'.{s}' for s in suffixes}
- futures = []
- returncode = 0
- e = concurrent.futures.ThreadPoolExecutor()
- try:
- for f in itertools.chain(*globs):
- strf = str(f)
- if f.is_dir() or f.suffix not in suffixes or \
- any(fnmatch.fnmatch(strf, i) for i in ignore):
- continue
- futures.append(e.submit(fn, f, *args))
- concurrent.futures.wait(
- futures,
- return_when=concurrent.futures.FIRST_EXCEPTION
- )
- finally:
- # We try to prevent new subprocesses from being started by canceling
- # the futures, but this is not water-tight: some may have started
- # between the wait being interrupted or exited and the futures being
- # canceled. (A fundamental fix would probably require the ability to
- # terminate such subprocesses upon cancellation of the future.)
- for x in futures: # Python >=3.9: e.shutdown(cancel_futures=True)
- x.cancel()
- e.shutdown()
- if futures:
- returncode = max(x.result().returncode for x in futures)
- return returncode
+ for f in itertools.chain.from_iterable(globs):
+ strf = str(f)
+ if f.is_dir() or f.suffix not in suffixes or \
+ any(fnmatch.fnmatch(strf, i) for i in ignore):
+ continue
+ yield f
+
+def run_clang_tool(name: str, srcdir: Path, builddir: Path, fn: T.Callable[..., T.Coroutine[None, None, int]], *args: T.Any) -> int:
+ if sys.platform == 'win32':
+ asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
+
+ def wrapper(path: Path) -> T.Iterable[T.Coroutine[None, None, int]]:
+ yield fn(path, *args)
+ return asyncio.run(_run_workers(all_clike_files(name, srcdir, builddir), wrapper))