summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/markdown/Commands.md28
-rw-r--r--docs/markdown/snippets/reprotester.md15
-rw-r--r--mesonbuild/mesonmain.py4
-rwxr-xr-xmesonbuild/scripts/reprotest.py123
4 files changed, 169 insertions, 1 deletions
diff --git a/docs/markdown/Commands.md b/docs/markdown/Commands.md
index 542f1b269..8e34800a4 100644
--- a/docs/markdown/Commands.md
+++ b/docs/markdown/Commands.md
@@ -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)*
diff --git a/docs/markdown/snippets/reprotester.md b/docs/markdown/snippets/reprotester.md
new file mode 100644
index 000000000..dc86acdb9
--- /dev/null
+++ b/docs/markdown/snippets/reprotester.md
@@ -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.
diff --git a/mesonbuild/mesonmain.py b/mesonbuild/mesonmain.py
index faa0f426d..2c1ca97a3 100644
--- a/mesonbuild/mesonmain.py
+++ b/mesonbuild/mesonmain.py
@@ -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
diff --git a/mesonbuild/scripts/reprotest.py b/mesonbuild/scripts/reprotest.py
new file mode 100755
index 000000000..fc9315c8f
--- /dev/null
+++ b/mesonbuild/scripts/reprotest.py
@@ -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)