diff options
author | Jussi Pakkanen <jpakkane@gmail.com> | 2024-09-18 18:05:30 +0300 |
---|---|---|
committer | Jussi Pakkanen <jpakkane@gmail.com> | 2024-09-18 18:21:50 +0300 |
commit | bdbb8535cf38306d8aef0f006d4bf753f12e9d15 (patch) | |
tree | 51c4fdf24518bf00ab8a62029533bd0f2d44234e /mesonbuild/scripts/reprotest.py | |
parent | 81c50885689488d701ec442d08234b442e975d78 (diff) | |
download | meson-reprotest.zip meson-reprotest.tar.gz meson-reprotest.tar.bz2 |
Add a simple reproducibility test command.reprotest
Diffstat (limited to 'mesonbuild/scripts/reprotest.py')
-rwxr-xr-x | mesonbuild/scripts/reprotest.py | 130 |
1 files changed, 130 insertions, 0 deletions
diff --git a/mesonbuild/scripts/reprotest.py b/mesonbuild/scripts/reprotest.py new file mode 100755 index 0000000..165c450 --- /dev/null +++ b/mesonbuild/scripts/reprotest.py @@ -0,0 +1,130 @@ +# 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 pathlib.Path('meson.build').is_file(): + sys.exit('This command needs to be run at your project source root.') + self.check_ccache() + self.cleanup() + self.build() + self.check_output() + self.print_results() + if not self.issues: + self.cleanup() + return len(self.issues) + + def check_ccache(self) -> None: + for evar in ('CC', 'CXX'): + evalue = os.environ.get(evar, '') + if 'ccache' in evalue: + print(f'Environment variable {evar} set to value "{evalue}".') + print('This implies using a compiler cache, which is incompatible with reproducible builds.') + sys.exit(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[T.Union[pathlib.Path, str]] = self.meson + ['setup', self.builddir] + self.args + build_command: T.Sequence[T.Union[pathlib.Path, str]] = self.meson + ['compile', '-C', 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: + for e in INTERMEDIATE_EXTENSIONS: + if fstr.endswith(e): + return True + return False + + def check_contents(self, fromdir: str, todir: str, check_contents: bool) -> None: + frompath = fromdir + '/' + topath = todir + '/' + for fromfile in pathlib.Path(fromdir).glob('**/*'): + if not fromfile.is_file(): + continue + fstr = str(fromfile) + 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: + fromdata = fromfile.read_bytes() + todata = tofile.read_bytes() + if fromdata != todata: + 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: T.Any) -> None: + rt = ReproTester(options) + try: + sys.exit(rt.run()) + except FileNotFoundError as e: + print(e) + sys.exit(1) |