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