parent
d584c7debb
commit
9ce607665a
4 changed files with 169 additions and 1 deletions
@ -0,0 +1,15 @@ |
|||||||
|
## Simple tool to test build reproducibility |
||||||
|
|
||||||
|
Meson now ships with a command for testing whether your project can be |
||||||
|
[built reprodicibly](https://reproducible-builds.org/). It can be used |
||||||
|
by running a command like the following in the source root of your |
||||||
|
project: |
||||||
|
|
||||||
|
meson reprotest --intermediaries -- --buildtype=debugoptimized |
||||||
|
|
||||||
|
All command line options after the `--` are passed to the build |
||||||
|
invocations directly. |
||||||
|
|
||||||
|
This tool is not meant to be exhaustive, but instead easy and |
||||||
|
convenient to run. It will detect some but definitely not all |
||||||
|
reproducibility issues. |
@ -0,0 +1,123 @@ |
|||||||
|
# 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: T.Any) -> None: |
||||||
|
rt = ReproTester(options) |
||||||
|
try: |
||||||
|
sys.exit(rt.run()) |
||||||
|
except Exception as e: |
||||||
|
print(e) |
||||||
|
sys.exit(1) |
Loading…
Reference in new issue