Add a simple reproducibility test command.

pull/13739/head
Jussi Pakkanen 2 months ago
parent d584c7debb
commit 9ce607665a
  1. 28
      docs/markdown/Commands.md
  2. 15
      docs/markdown/snippets/reprotester.md
  3. 4
      mesonbuild/mesonmain.py
  4. 123
      mesonbuild/scripts/reprotest.py

@ -225,6 +225,34 @@ DESTDIR=/path/to/staging/area meson install -C builddir
Since *0.60.0* `DESTDIR` and `--destdir` can be a path relative to build
directory. An absolute path will be set into environment when executing scripts.
### reprotest
*(since 1.6.0)*
{{ reprotest_usage.inc }}
Simple reproducible build tester that compiles the project twice and
checks whether the end results are identical.
This command must be run in the source root of the project you want to
test.
{{ reprotest_arguments.inc }}
#### Examples
meson reprotest
Builds the current project with its default settings.
meson reprotest --intermediaries -- --buildtype=debugoptimized
Builds the target and also checks that all intermediate files like
object files are also identical. All command line arguments after the
`--` are passed directly to the underlying `meson` invocation. Only
use option arguments, i.e. those that start with a dash, Meson sets
directory arguments automatically.
### rewrite
*(since 0.50.0)*

@ -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.

@ -65,7 +65,7 @@ class CommandLineParser:
def __init__(self) -> None:
# only import these once we do full argparse processing
from . import mconf, mdist, minit, minstall, mintro, msetup, mtest, rewriter, msubprojects, munstable_coredata, mcompile, mdevenv, mformat
from .scripts import env2mfile
from .scripts import env2mfile, reprotest
from .wrap import wraptool
import shutil
@ -103,6 +103,8 @@ class CommandLineParser:
help_msg='Run commands in developer environment')
self.add_command('env2mfile', env2mfile.add_arguments, env2mfile.run,
help_msg='Convert current environment to a cross or native file')
self.add_command('reprotest', reprotest.add_arguments, reprotest.run,
help_msg='Test if project builds reproducibly')
self.add_command('format', mformat.add_arguments, mformat.run, aliases=['fmt'],
help_msg='Format meson source file')
# Add new commands above this line to list them in help command

@ -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…
Cancel
Save