summaryrefslogtreecommitdiff
path: root/mesonbuild/scripts
diff options
context:
space:
mode:
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))