| # SPDX-License-Identifier: Apache-2.0 | |
| # Copyright 2024 The Meson development team | |
| from __future__ import annotations | |
| import sys, os, subprocess, shutil | |
| import pathlib | |
| import typing as T | |
| if T.TYPE_CHECKING: | |
| import argparse | |
| from ..mesonlib import get_meson_command | |
| # Note: when adding arguments, please also add them to the completion | |
| # scripts in $MESONSRC/data/shell-completions/ | |
| def add_arguments(parser: 'argparse.ArgumentParser') -> None: | |
| parser.add_argument('--intermediaries', | |
| default=False, | |
| action='store_true', | |
| help='Check intermediate files.') | |
| parser.add_argument('mesonargs', nargs='*', | |
| help='Arguments to pass to "meson setup".') | |
| IGNORE_PATTERNS = ('.ninja_log', | |
| '.ninja_deps', | |
| 'meson-private', | |
| 'meson-logs', | |
| 'meson-info', | |
| ) | |
| INTERMEDIATE_EXTENSIONS = ('.gch', | |
| '.pch', | |
| '.o', | |
| '.obj', | |
| '.class', | |
| ) | |
| class ReproTester: | |
| def __init__(self, options: T.Any): | |
| self.args = options.mesonargs | |
| self.meson = get_meson_command()[:] | |
| self.builddir = pathlib.Path('buildrepro') | |
| self.storagedir = pathlib.Path('buildrepro.1st') | |
| self.issues: T.List[str] = [] | |
| self.check_intermediaries = options.intermediaries | |
| def run(self) -> int: | |
| if not os.path.isfile('meson.build'): | |
| sys.exit('This command needs to be run at your project source root.') | |
| self.disable_ccache() | |
| self.cleanup() | |
| self.build() | |
| self.check_output() | |
| self.print_results() | |
| if not self.issues: | |
| self.cleanup() | |
| return len(self.issues) | |
| def disable_ccache(self) -> None: | |
| os.environ['CCACHE_DISABLE'] = '1' | |
| def cleanup(self) -> None: | |
| if self.builddir.exists(): | |
| shutil.rmtree(self.builddir) | |
| if self.storagedir.exists(): | |
| shutil.rmtree(self.storagedir) | |
| def build(self) -> None: | |
| setup_command: T.Sequence[str] = self.meson + ['setup', str(self.builddir)] + self.args | |
| build_command: T.Sequence[str] = self.meson + ['compile', '-C', str(self.builddir)] | |
| subprocess.check_call(setup_command) | |
| subprocess.check_call(build_command) | |
| self.builddir.rename(self.storagedir) | |
| subprocess.check_call(setup_command) | |
| subprocess.check_call(build_command) | |
| def ignore_file(self, fstr: str) -> bool: | |
| for p in IGNORE_PATTERNS: | |
| if p in fstr: | |
| return True | |
| if not self.check_intermediaries: | |
| if fstr.endswith(INTERMEDIATE_EXTENSIONS): | |
| return True | |
| return False | |
| def check_contents(self, fromdir: str, todir: str, check_contents: bool) -> None: | |
| import filecmp | |
| frompath = fromdir + '/' | |
| topath = todir + '/' | |
| for fromfile in pathlib.Path(fromdir).glob('**/*'): | |
| if not fromfile.is_file(): | |
| continue | |
| fstr = fromfile.as_posix() | |
| if self.ignore_file(fstr): | |
| continue | |
| assert fstr.startswith(frompath) | |
| tofile = pathlib.Path(fstr.replace(frompath, topath, 1)) | |
| if not tofile.exists(): | |
| self.issues.append(f'Missing file: {tofile}') | |
| elif check_contents: | |
| if not filecmp.cmp(fromfile, tofile, shallow=False): | |
| self.issues.append(f'File contents differ: {fromfile}') | |
| def print_results(self) -> None: | |
| if self.issues: | |
| print('Build differences detected') | |
| for i in self.issues: | |
| print(i) | |
| else: | |
| print('No differences detected.') | |
| def check_output(self) -> None: | |
| self.check_contents('buildrepro', 'buildrepro.1st', True) | |
| self.check_contents('buildrepro.1st', 'buildrepro', False) | |
| def run(options: argparse.Namespace) -> int: | |
| rt = ReproTester(options) | |
| try: | |
| return rt.run() | |
| except Exception as e: | |
| print(e) | |
| return 1 |