From f5b7cfdbf0888fb99956cf2dc661c519aa08a9a4 Mon Sep 17 00:00:00 2001 From: Sam Thursfield Date: Sat, 1 Apr 2017 20:04:53 +0100 Subject: [PATCH 1/4] Ensure rules in the generated build.ninja file are in a stable order Previously, two functionally identical builds could produce different build.ninja files. The ordering of the rules themselves doesn't affect behaviour, but unnecessary changes in commandline arguments can cause spurious rebuilds and if the ordering of the overall file is stable than it's easy to use `diff` to compare different build.ninja files and spot the differences in ordering that are triggering the unnecessary rebuilds. --- mesonbuild/backend/backends.py | 3 ++- mesonbuild/build.py | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py index 99be17240..e37b0df8a 100644 --- a/mesonbuild/backend/backends.py +++ b/mesonbuild/backend/backends.py @@ -22,6 +22,7 @@ import json import subprocess from ..mesonlib import MesonException, get_compiler_for_source, classify_unity_sources from ..compilers import CompilerArgs +from collections import OrderedDict class CleanTrees: ''' @@ -542,7 +543,7 @@ class Backend: return newargs def get_build_by_default_targets(self): - result = {} + result = OrderedDict() # Get all build and custom targets that must be built by default for name, t in self.build.get_targets().items(): if t.build_by_default or t.install or t.build_always: diff --git a/mesonbuild/build.py b/mesonbuild/build.py index ce5638d6f..ed0abc464 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -82,9 +82,9 @@ class Build: self.project_version = None self.environment = environment self.projects = {} - self.targets = {} - self.compilers = {} - self.cross_compilers = {} + self.targets = OrderedDict() + self.compilers = OrderedDict() + self.cross_compilers = OrderedDict() self.global_args = {} self.projects_args = {} self.global_link_args = {} @@ -326,6 +326,9 @@ class BuildTarget(Target): self.validate_sources() self.validate_cross_install(environment) + def __lt__(self, other): + return self.get_id() < other.get_id() + def __repr__(self): repr_str = "<{0} {1}: {2}>" return repr_str.format(self.__class__.__name__, self.get_id(), self.filename) @@ -1257,6 +1260,9 @@ class CustomTarget(Target): mlog.warning('Unknown keyword arguments in target %s: %s' % (self.name, ', '.join(unknowns))) + def __lt__(self, other): + return self.get_id() < other.get_id() + def __repr__(self): repr_str = "<{0} {1}: {2}>" return repr_str.format(self.__class__.__name__, self.get_id(), self.command) @@ -1417,6 +1423,9 @@ class RunTarget(Target): self.args = args self.dependencies = dependencies + def __lt__(self, other): + return self.get_id() < other.get_id() + def __repr__(self): repr_str = "<{0} {1}: {2}>" return repr_str.format(self.__class__.__name__, self.get_id(), self.command) From e0c5e4082689e6e49d80c574f35b9e401b7fc83a Mon Sep 17 00:00:00 2001 From: Sam Thursfield Date: Mon, 3 Apr 2017 15:57:27 +0100 Subject: [PATCH 2/4] Add a cheap OrderedSet implementation This just makes an OrderedDict look more like a set class. This results in neater code than if we use OrderedDict to hold sets directly. There is a "real" OrderedSet implementation available here, but it lacks a clear license: https://code.activestate.com/recipes/576694/ --- mesonbuild/mesonlib.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/mesonbuild/mesonlib.py b/mesonbuild/mesonlib.py index c7368d577..f0bf9eea2 100644 --- a/mesonbuild/mesonlib.py +++ b/mesonbuild/mesonlib.py @@ -16,6 +16,7 @@ import stat import platform, subprocess, operator, os, shutil, re +import collections from glob import glob @@ -672,3 +673,18 @@ def get_filenames_templates_dict(inputs, outputs): if values['@OUTDIR@'] == '': values['@OUTDIR@'] = '.' return values + +class OrderedSet(collections.OrderedDict): + ''' + A 'set' equivalent that preserves the order in which items are added. + + This is a hack implementation that wraps OrderedDict. It may not be the + most efficient solution and might need fixing to override more methods. + ''' + def __init__(self, iterable=None): + if iterable: + self.update(iterable) + + def update(self, iterable): + for item in iterable: + self[item] = True From c408bd6a8eb31b261170de09e20ecf8e0317152e Mon Sep 17 00:00:00 2001 From: Sam Thursfield Date: Sun, 2 Apr 2017 13:29:46 +0100 Subject: [PATCH 3/4] gnome: Preserve ordering of flags passed to tools This avoids unnecessary rebuilds occuring when Meson regenerates the build.ninja file. Previously, if the ordering of the commandline arguments changed then Ninja would consider the outputs dirty and rebuild them. --- mesonbuild/modules/gnome.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mesonbuild/modules/gnome.py b/mesonbuild/modules/gnome.py index 4b366bf23..481a25077 100644 --- a/mesonbuild/modules/gnome.py +++ b/mesonbuild/modules/gnome.py @@ -21,7 +21,7 @@ import sys import copy import subprocess from . import ModuleReturnValue -from ..mesonlib import MesonException, Popen_safe +from ..mesonlib import MesonException, OrderedSet, Popen_safe from ..dependencies import Dependency, PkgConfigDependency, InternalDependency from .. import mlog from .. import mesonlib @@ -154,7 +154,7 @@ class GnomeModule(ExtensionModule): # Ensure build directories of generated deps are included source_dirs += subdirs - for source_dir in set(source_dirs): + for source_dir in OrderedSet(source_dirs): cmd += ['--sourcedir', source_dir] if 'c_name' in kwargs: @@ -299,9 +299,9 @@ class GnomeModule(ExtensionModule): def _get_dependencies_flags(self, deps, state, depends=None, include_rpath=False, use_gir_args=False): - cflags = set() - ldflags = set() - gi_includes = set() + cflags = OrderedSet() + ldflags = OrderedSet() + gi_includes = OrderedSet() if not isinstance(deps, list): deps = [deps] From 2db11f1383e3d9c59f54f4a742bd44ad85dce226 Mon Sep 17 00:00:00 2001 From: Sam Thursfield Date: Mon, 3 Apr 2017 17:03:56 +0100 Subject: [PATCH 4/4] Sort user commandline options when generating 'scan-build' target We receive these options from the 'argparse' module in a random order. To ensure the build.ninja file doesn't include random variations we should sort them before writing them back out. --- mesonbuild/backend/ninjabackend.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index f29a7be62..63db854e1 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -2276,7 +2276,10 @@ rule FORTRAN_DEP_HACK cmds = [] for (k, v) in self.environment.coredata.user_options.items(): cmds.append('-D' + k + '=' + (v.value if isinstance(v.value, str) else str(v.value).lower())) - return cmds + # The order of these arguments must be the same between runs of Meson + # to ensure reproducible output. The order we pass them shouldn't + # affect behaviour in any other way. + return sorted(cmds) # For things like scan-build and other helper tools we might have. def generate_utils(self, outfile):