From 67d8d0a1cdb4948158a4fe0387df44e960575993 Mon Sep 17 00:00:00 2001 From: Thomas Coldrick Date: Thu, 22 Nov 2018 16:39:24 +0000 Subject: [PATCH 1/3] bst fmt: Add basic functionality Adds basic functionality for `bst fmt`, formatting the yaml files of elements TODO: * Weird issue in yaml dumping - see #767 * Add a check option, which just checks if format correct * Add a canonical order of top-level nodes on a per project basis --- buildstream/_frontend/cli.py | 23 +++++++++++++++ buildstream/_scheduler/__init__.py | 1 + buildstream/_scheduler/queues/formatqueue.py | 13 +++++++++ buildstream/_stream.py | 30 +++++++++++++++++++- buildstream/element.py | 8 ++++++ 5 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 buildstream/_scheduler/queues/formatqueue.py diff --git a/buildstream/_frontend/cli.py b/buildstream/_frontend/cli.py index 26bdf0a92..e9eecd932 100644 --- a/buildstream/_frontend/cli.py +++ b/buildstream/_frontend/cli.py @@ -354,6 +354,29 @@ def build(app, elements, all_, track_, track_save, track_all, track_except, trac track_cross_junctions=track_cross_junctions, build_all=all_) +################################################################## +# Format Command # +################################################################## +@cli.command(short_help="Format element files") +@click.option('--all', 'all_', default=False, is_flag=True, + help="Format all dependencies of the targets") +@click.option('--except', 'except_', multiple=True, + type=click.Path(readable=False), + help="Except certain files from formatting (has no effect without `--all`)") +@click.argument('elements', nargs=-1, + type=click.Path(readable=False)) +@click.pass_obj +def fmt(app, elements, all_, except_): + """Formats element files into a consistent style. This style is the one + defaulted to by ruamel.yaml, so is the recommended format for a + BuildStream project. + + By default this will format just the specified element, but you can format + all the elements at once by passing the `--all` flag. + """ + with app.initialized(session_name="Format"): + app.stream.format(elements, except_targets=except_, format_all=all_) + ################################################################## # Pull Command # diff --git a/buildstream/_scheduler/__init__.py b/buildstream/_scheduler/__init__.py index b6e3eeb94..035c52195 100644 --- a/buildstream/_scheduler/__init__.py +++ b/buildstream/_scheduler/__init__.py @@ -24,6 +24,7 @@ from .queues.buildqueue import BuildQueue from .queues.pushqueue import PushQueue from .queues.pullqueue import PullQueue +from .queues.formatqueue import FormatQueue from .scheduler import Scheduler, SchedStatus from .jobs import ElementJob diff --git a/buildstream/_scheduler/queues/formatqueue.py b/buildstream/_scheduler/queues/formatqueue.py new file mode 100644 index 000000000..0744ff56c --- /dev/null +++ b/buildstream/_scheduler/queues/formatqueue.py @@ -0,0 +1,13 @@ +# Local imports +from . import Queue +from ..resources import ResourceType + + +class FormatQueue(Queue): + + action_name = "Format" + complete_name = "Formatted" + resources = [ResourceType.PROCESS] + + def process(self, element): + element._format() diff --git a/buildstream/_stream.py b/buildstream/_stream.py index 85ead3330..bd85ac06e 100644 --- a/buildstream/_stream.py +++ b/buildstream/_stream.py @@ -30,7 +30,7 @@ from ._exceptions import StreamError, ImplError, BstError, set_last_task_error from ._message import Message, MessageType -from ._scheduler import Scheduler, SchedStatus, TrackQueue, FetchQueue, BuildQueue, PullQueue, PushQueue +from ._scheduler import Scheduler, SchedStatus, TrackQueue, FetchQueue, BuildQueue, PullQueue, PushQueue, FormatQueue from ._pipeline import Pipeline, PipelineSelection from . import utils, _yaml, _site from . import Scope, Consistency @@ -284,6 +284,34 @@ def track(self, targets, *, self._enqueue_plan(elements, queue=track_queue) self._run() + # format() + # + # Formats elements into a "canonical" format. + # + # Args: + # targets (list of str): Targets to format + # except_targets (list of str): Specifiedtargets to except from formatting + # format_all (bool): Whether to format all elements or just the targets + # + def format(self, targets, *, + except_targets=None, + format_all=False): + if format_all: + modify_selection = PipelineSelection.ALL + else: + modify_selection = PipelineSelection.REDIRECT + + # Only pass targets to track, in order to load a rewritable pipeline + _, elements = \ + self._load([], targets, + track_selection=modify_selection, + track_except_targets=except_targets, + fetch_subprojects=True) + fmt_queue = FormatQueue(self._scheduler) + self._add_queue(fmt_queue, track=False) + self._enqueue_plan(elements, queue=fmt_queue) + self._run() + # pull() # # Pulls artifacts from remote artifact server(s) diff --git a/buildstream/element.py b/buildstream/element.py index e44a7cc32..9a9e0157d 100644 --- a/buildstream/element.py +++ b/buildstream/element.py @@ -1332,6 +1332,14 @@ def _track(self): return refs + # _format() + # + # Dumps an element's yaml file in canonical format + # + def _format(self): + provenance = self._get_provenance() + _yaml.dump(provenance.toplevel, provenance.filename.name) + # _prepare_sandbox(): # # This stages things for either _shell() (below) or also From 9b5e21337660f7085093c8d745497bce90c7f59e Mon Sep 17 00:00:00 2001 From: Thomas Coldrick Date: Thu, 22 Nov 2018 16:44:14 +0000 Subject: [PATCH 2/3] bst fmt: Add tests for core functionality --- tests/completions/completions.py | 1 + tests/integration/fmt.py | 75 +++++++++++++++++++ .../project/elements/fmt/bad-format.bst | 4 + .../project/elements/fmt/except.bst | 4 + .../project/elements/fmt/good-format.bst | 4 + .../project/elements/fmt/stack.bst | 5 ++ 6 files changed, 93 insertions(+) create mode 100644 tests/integration/fmt.py create mode 100644 tests/integration/project/elements/fmt/bad-format.bst create mode 100644 tests/integration/project/elements/fmt/except.bst create mode 100644 tests/integration/project/elements/fmt/good-format.bst create mode 100644 tests/integration/project/elements/fmt/stack.bst diff --git a/tests/completions/completions.py b/tests/completions/completions.py index 372ed7840..303bc945b 100644 --- a/tests/completions/completions.py +++ b/tests/completions/completions.py @@ -9,6 +9,7 @@ 'artifact ', 'build ', 'checkout ', + 'fmt ', 'help ', 'init ', 'pull ', diff --git a/tests/integration/fmt.py b/tests/integration/fmt.py new file mode 100644 index 000000000..4580d5e32 --- /dev/null +++ b/tests/integration/fmt.py @@ -0,0 +1,75 @@ +import os +import pytest + +from tests.testutils import cli_integration as cli +from tests.testutils.site import IS_LINUX + + +DATA_DIR = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + "project" +) + + +@pytest.mark.integration +@pytest.mark.datafiles(DATA_DIR) +def test_fmt_single(cli, tmpdir, datafiles): + project = os.path.join(datafiles.dirname, datafiles.basename) + element_name = 'fmt/bad-format.bst' + good_element = 'fmt/good-format.bst' + + with open(os.path.join(project, 'elements', good_element)) as element: + good = element.readlines() + + res = cli.run(project=project, args=['fmt', element_name]) + assert res.exit_code == 0 + + with open(os.path.join(project, 'elements', element_name)) as element: + final = element.readlines() + + # Have to test only the last line, rather than full file because of #767 + assert final[-1] == good[-1] + + +@pytest.mark.integration +@pytest.mark.datafiles(DATA_DIR) +def test_fmt_all(cli, tmpdir, datafiles): + project = os.path.join(datafiles.dirname, datafiles.basename) + check_elements = ['fmt/bad-format.bst', 'fmt/except.bst'] + target = 'fmt/stack.bst' + + initial_lines = [] + for e in check_elements: + with open(os.path.join(project, 'elements', e)) as element: + initial_lines.append(element.readlines()[-1]) + + with open(os.path.join(project, 'elements', 'fmt/good-format.bst')) as element: + expected = element.readlines()[-1] + + res = cli.run(project=project, args=['fmt', '--all', target]) + assert res.exit_code == 0 + + for i, _ in enumerate(initial_lines): + with open(os.path.join(project, 'elements', check_elements[i])) as element: + assert element.readlines()[-1] == expected + + +@pytest.mark.integration +@pytest.mark.datafiles(DATA_DIR) +def test_fmt_except(cli, tmpdir, datafiles): + project = os.path.join(datafiles.dirname, datafiles.basename) + bad_target = 'fmt/bad-format.bst' + except_target = 'fmt/except.bst' + target = 'fmt/stack.bst' + + with open(os.path.join(project, 'elements', 'fmt/good-format.bst')) as element: + formatted = element.readlines()[-1] + + res = cli.run(project=project, args=['fmt', '--all', target, '--except', except_target]) + assert res.exit_code == 0 + + with open(os.path.join(project, 'elements', except_target)) as element: + assert formatted != element.readlines()[-1] + + with open(os.path.join(project, 'elements', bad_target)) as element: + assert formatted == element.readlines()[-1] diff --git a/tests/integration/project/elements/fmt/bad-format.bst b/tests/integration/project/elements/fmt/bad-format.bst new file mode 100644 index 000000000..bbecc3a43 --- /dev/null +++ b/tests/integration/project/elements/fmt/bad-format.bst @@ -0,0 +1,4 @@ +kind: stack + +depends: + - filename: base.bst diff --git a/tests/integration/project/elements/fmt/except.bst b/tests/integration/project/elements/fmt/except.bst new file mode 100644 index 000000000..bbecc3a43 --- /dev/null +++ b/tests/integration/project/elements/fmt/except.bst @@ -0,0 +1,4 @@ +kind: stack + +depends: + - filename: base.bst diff --git a/tests/integration/project/elements/fmt/good-format.bst b/tests/integration/project/elements/fmt/good-format.bst new file mode 100644 index 000000000..44e2293a7 --- /dev/null +++ b/tests/integration/project/elements/fmt/good-format.bst @@ -0,0 +1,4 @@ +kind: stack + +depends: +- filename: base.bst diff --git a/tests/integration/project/elements/fmt/stack.bst b/tests/integration/project/elements/fmt/stack.bst new file mode 100644 index 000000000..0252d5417 --- /dev/null +++ b/tests/integration/project/elements/fmt/stack.bst @@ -0,0 +1,5 @@ +kind: stack +depends: +- filename: fmt/bad-format.bst +- filename: fmt/good-format.bst +- filename: fmt/except.bst From ecaf433fa507ce0ec95ddc86e2fe2c7692667b74 Mon Sep 17 00:00:00 2001 From: Thomas Coldrick Date: Thu, 20 Dec 2018 11:41:05 +0000 Subject: [PATCH 3/3] bst-fmt: Allow greater control over node order Allow authors of plugins to utilise the Plugin.keyorder attribute to define an order for the sub-headers in config. Modify `bst fmt` to modify the yaml into that order. Add canonical keyorder for every plugin already in core. In terms of implementation, this adds a custom yaml dumper subclassed from ruamel.yaml's RoundTripDumper, which will dump a dict in the order defined in the keyorder attribute. The Plugin class is given a keyorder attribute, which defines the order in which keys should be dumped. The element and source classes add the common fields used in them, so as to minimise work for plugin authors. Plugin authors should add their custom keys at configure time. This causes some stripping of comments, which seems to be due to a bug in the ruamel.yaml RoundTripDumper, as that also strips the same comments. It also will remove blank spaces, again probably due to limitations in ruamel. --- buildstream/_frontend/cli.py | 1 + buildstream/_yaml.py | 30 +++++++++++++++++-- buildstream/buildelement.py | 1 + buildstream/element.py | 15 +++++++++- buildstream/plugin.py | 2 ++ buildstream/plugins/elements/compose.py | 2 ++ buildstream/plugins/elements/filter.py | 2 ++ buildstream/plugins/elements/junction.py | 1 + buildstream/plugins/elements/script.py | 1 + .../sources/_downloadablefilesource.py | 2 ++ buildstream/plugins/sources/bzr.py | 2 ++ buildstream/plugins/sources/git.py | 2 ++ buildstream/plugins/sources/local.py | 1 + buildstream/plugins/sources/ostree.py | 1 + buildstream/plugins/sources/patch.py | 1 + buildstream/plugins/sources/pip.py | 1 + buildstream/plugins/sources/remote.py | 1 + buildstream/plugins/sources/tar.py | 1 + buildstream/plugins/sources/zip.py | 1 + buildstream/source.py | 2 ++ 20 files changed, 67 insertions(+), 3 deletions(-) diff --git a/buildstream/_frontend/cli.py b/buildstream/_frontend/cli.py index e9eecd932..c8df1810c 100644 --- a/buildstream/_frontend/cli.py +++ b/buildstream/_frontend/cli.py @@ -354,6 +354,7 @@ def build(app, elements, all_, track_, track_save, track_all, track_except, trac track_cross_junctions=track_cross_junctions, build_all=all_) + ################################################################## # Format Command # ################################################################## diff --git a/buildstream/_yaml.py b/buildstream/_yaml.py index 8d7302b80..2d659c7a3 100644 --- a/buildstream/_yaml.py +++ b/buildstream/_yaml.py @@ -51,6 +51,31 @@ def __init__(self, name, shortname, project): self.project = project +# A custom yaml dumper to reorder keys in dicts to a canonical order, defined +# in each plugin +class BstFormatter(yaml.RoundTripDumper): + + keyorder = [] + + @classmethod + def _iter_in_global_order(cls, mapping): + for key in cls.keyorder: + if key in mapping.keys(): + yield key, mapping[key] + for key in sorted(mapping.keys()): + if key not in cls.keyorder: + yield key, mapping[key] + + @classmethod + def _represent_dict(cls, dumper, mapping): + return dumper.represent_mapping('tag:yaml.org,2002:map', cls._iter_in_global_order(mapping)) + + def __init__(self, *args, **kwargs): + yaml.RoundTripDumper.__init__(self, *args, **kwargs) + self.no_newline = False + self.add_representer(dict, self._represent_dict) + + # Provenance tracks the origin of a given node in the parsed dictionary. # # Args: @@ -244,15 +269,16 @@ def load_data(data, file=None, copy_tree=False): # Args: # node (dict): A node previously loaded with _yaml.load() above # filename (str): The YAML file to load +# dumper (yaml.Dumper): The yaml dumper to be used # -def dump(node, filename=None): +def dump(node, filename=None, dumper=yaml.RoundTripDumper): with ExitStack() as stack: if filename: from . import utils f = stack.enter_context(utils.save_file_atomic(filename, 'w')) else: f = sys.stdout - yaml.round_trip_dump(node, f) + yaml.round_trip_dump(node, f, Dumper=dumper) # node_decorated_copy() diff --git a/buildstream/buildelement.py b/buildstream/buildelement.py index 6ef060f12..0b83ea0e2 100644 --- a/buildstream/buildelement.py +++ b/buildstream/buildelement.py @@ -172,6 +172,7 @@ def configure(self, node): for command_name in _legacy_command_steps: if command_name in _command_steps: self.__commands[command_name] = self.__get_commands(node, command_name) + self.keyorder.append(command_name) else: self.__commands[command_name] = [] diff --git a/buildstream/element.py b/buildstream/element.py index 9a9e0157d..a8ee747e4 100644 --- a/buildstream/element.py +++ b/buildstream/element.py @@ -269,6 +269,9 @@ def __init__(self, context, project, meta, plugin_conf): # This will taint the artifact, disable pushing. self.__sandbox_config_supported = False + self.keyorder = ['kind', 'description', 'depends', 'variables', + 'environment', 'config', 'public', 'sandbox', 'sources'] + def __lt__(self, other): return self.name < other.name @@ -1338,7 +1341,17 @@ def _track(self): # def _format(self): provenance = self._get_provenance() - _yaml.dump(provenance.toplevel, provenance.filename.name) + + _yaml.BstFormatter.keyorder = self.keyorder + + # We need to add the key orders for each source into the + # global keyorder, as sources are dumped with the element. + for s in self.sources(): + for key in s.keyorder: + if key not in _yaml.BstFormatter.keyorder: + _yaml.BstFormatter.keyorder += s.keyorder + + _yaml.dump(provenance.toplevel, provenance.filename.name, dumper=_yaml.BstFormatter) # _prepare_sandbox(): # diff --git a/buildstream/plugin.py b/buildstream/plugin.py index 2f51c8807..181a31962 100644 --- a/buildstream/plugin.py +++ b/buildstream/plugin.py @@ -188,6 +188,8 @@ def __init__(self, name, context, project, provenance, type_tag): self.__kind = modulename.split('.')[-1] self.debug("Created: {}".format(self)) + self.keyorder = [] + def __del__(self): # Dont send anything through the Message() pipeline at destruction time, # any subsequent lookup of plugin by unique id would raise KeyError. diff --git a/buildstream/plugins/elements/compose.py b/buildstream/plugins/elements/compose.py index d61a324cc..ed4da4dba 100644 --- a/buildstream/plugins/elements/compose.py +++ b/buildstream/plugins/elements/compose.py @@ -70,6 +70,8 @@ def configure(self, node): self.exclude = self.node_get_member(node, list, 'exclude') self.include_orphans = self.node_get_member(node, bool, 'include-orphans') + self.keyorder += ['integrate', 'include', 'exclude', 'include-orphans'] + def preflight(self): pass diff --git a/buildstream/plugins/elements/filter.py b/buildstream/plugins/elements/filter.py index 672325304..9f86baacf 100644 --- a/buildstream/plugins/elements/filter.py +++ b/buildstream/plugins/elements/filter.py @@ -65,6 +65,8 @@ def configure(self, node): self.exclude = self.node_get_member(node, list, 'exclude') self.include_orphans = self.node_get_member(node, bool, 'include-orphans') + self.keyorder += ['include', 'exclude', 'include-orphans'] + def preflight(self): # Exactly one build-depend is permitted build_deps = list(self.dependencies(Scope.BUILD, recurse=False)) diff --git a/buildstream/plugins/elements/junction.py b/buildstream/plugins/elements/junction.py index 7f9817359..2a5b17fdb 100644 --- a/buildstream/plugins/elements/junction.py +++ b/buildstream/plugins/elements/junction.py @@ -140,6 +140,7 @@ class JunctionElement(Element): def configure(self, node): self.path = self.node_get_member(node, str, 'path', default='') self.options = self.node_get_member(node, Mapping, 'options', default={}) + self.keyorder += ['path', 'options'] def preflight(self): pass diff --git a/buildstream/plugins/elements/script.py b/buildstream/plugins/elements/script.py index 4e422c5db..b695fade2 100644 --- a/buildstream/plugins/elements/script.py +++ b/buildstream/plugins/elements/script.py @@ -51,6 +51,7 @@ def configure(self, node): self.node_validate(node, [ 'commands', 'root-read-only', 'layout' ]) + self.keyorder += ['layout', 'root-read-only', 'commands'] cmds = self.node_subst_list(node, "commands") self.add_commands("commands", cmds) diff --git a/buildstream/plugins/sources/_downloadablefilesource.py b/buildstream/plugins/sources/_downloadablefilesource.py index f5c5b3d08..4cb2d50f5 100644 --- a/buildstream/plugins/sources/_downloadablefilesource.py +++ b/buildstream/plugins/sources/_downloadablefilesource.py @@ -82,6 +82,8 @@ def configure(self, node): self.url = self.translate_url(self.original_url) self._warn_deprecated_etag(node) + self.keyorder += ['url', 'ref', 'etag'] + def preflight(self): return diff --git a/buildstream/plugins/sources/bzr.py b/buildstream/plugins/sources/bzr.py index f52472918..544bf2dc9 100644 --- a/buildstream/plugins/sources/bzr.py +++ b/buildstream/plugins/sources/bzr.py @@ -68,6 +68,8 @@ class BzrSource(Source): def configure(self, node): self.node_validate(node, ['url', 'track', 'ref'] + Source.COMMON_CONFIG_KEYS) + self.keyorder += ['url', 'track', 'ref'] + self.original_url = self.node_get_member(node, str, 'url') self.tracking = self.node_get_member(node, str, 'track') self.ref = self.node_get_member(node, str, 'ref', None) diff --git a/buildstream/plugins/sources/git.py b/buildstream/plugins/sources/git.py index 74d632b6d..e93517c9e 100644 --- a/buildstream/plugins/sources/git.py +++ b/buildstream/plugins/sources/git.py @@ -506,6 +506,8 @@ def configure(self, node): 'track-tags', 'tags'] self.node_validate(node, config_keys + Source.COMMON_CONFIG_KEYS) + self.keyorder += config_keys + tags_node = self.node_get_member(node, list, 'tags', []) for tag_node in tags_node: self.node_validate(tag_node, ['tag', 'commit', 'annotated']) diff --git a/buildstream/plugins/sources/local.py b/buildstream/plugins/sources/local.py index 55cdc14d3..c6b6963bc 100644 --- a/buildstream/plugins/sources/local.py +++ b/buildstream/plugins/sources/local.py @@ -53,6 +53,7 @@ def __init__(self, context, project, meta): def configure(self, node): self.node_validate(node, ['path'] + Source.COMMON_CONFIG_KEYS) + self.keyorder += ['path'] self.path = self.node_get_project_path(node, 'path') self.fullpath = os.path.join(self.get_project_directory(), self.path) diff --git a/buildstream/plugins/sources/ostree.py b/buildstream/plugins/sources/ostree.py index 770cfd55f..1bc4ef6fc 100644 --- a/buildstream/plugins/sources/ostree.py +++ b/buildstream/plugins/sources/ostree.py @@ -65,6 +65,7 @@ class OSTreeSource(Source): def configure(self, node): self.node_validate(node, ['url', 'ref', 'track', 'gpg-key'] + Source.COMMON_CONFIG_KEYS) + self.keyorder += ['url', 'track', 'ref', 'gpg-key'] self.original_url = self.node_get_member(node, str, 'url') self.url = self.translate_url(self.original_url) diff --git a/buildstream/plugins/sources/patch.py b/buildstream/plugins/sources/patch.py index 8e833b411..48aab13bd 100644 --- a/buildstream/plugins/sources/patch.py +++ b/buildstream/plugins/sources/patch.py @@ -53,6 +53,7 @@ class PatchSource(Source): # pylint: disable=attribute-defined-outside-init def configure(self, node): + self.keyorder += ['strip-level', 'path'] self.path = self.node_get_project_path(node, 'path', check_is_file=True) self.strip_level = self.node_get_member(node, int, "strip-level", 1) diff --git a/buildstream/plugins/sources/pip.py b/buildstream/plugins/sources/pip.py index abef1fd0d..04e8bab4e 100644 --- a/buildstream/plugins/sources/pip.py +++ b/buildstream/plugins/sources/pip.py @@ -111,6 +111,7 @@ class PipSource(Source): def configure(self, node): self.node_validate(node, ['url', 'packages', 'ref', 'requirements-files'] + Source.COMMON_CONFIG_KEYS) + self.keyorder += ['url', 'packages', 'ref', 'requirements-files'] self.ref = self.node_get_member(node, str, 'ref', None) self.original_url = self.node_get_member(node, str, 'url', _PYPI_INDEX_URL) self.index_url = self.translate_url(self.original_url) diff --git a/buildstream/plugins/sources/remote.py b/buildstream/plugins/sources/remote.py index a6b02fd1c..9247c12fc 100644 --- a/buildstream/plugins/sources/remote.py +++ b/buildstream/plugins/sources/remote.py @@ -61,6 +61,7 @@ class RemoteSource(DownloadableFileSource): def configure(self, node): super().configure(node) + self.keyorder += ['filename'] self.filename = self.node_get_member(node, str, 'filename', os.path.basename(self.url)) self.executable = self.node_get_member(node, bool, 'executable', False) diff --git a/buildstream/plugins/sources/tar.py b/buildstream/plugins/sources/tar.py index 195c05958..a830df8d3 100644 --- a/buildstream/plugins/sources/tar.py +++ b/buildstream/plugins/sources/tar.py @@ -75,6 +75,7 @@ def configure(self, node): self.base_dir = self.node_get_member(node, str, 'base-dir', '*') or None self.node_validate(node, DownloadableFileSource.COMMON_CONFIG_KEYS + ['base-dir']) + self.keyorder += ['base-dir'] def preflight(self): self.host_lzip = None diff --git a/buildstream/plugins/sources/zip.py b/buildstream/plugins/sources/zip.py index f5fac3a48..27e5fd915 100644 --- a/buildstream/plugins/sources/zip.py +++ b/buildstream/plugins/sources/zip.py @@ -75,6 +75,7 @@ def configure(self, node): self.base_dir = self.node_get_member(node, str, 'base-dir', '*') or None self.node_validate(node, DownloadableFileSource.COMMON_CONFIG_KEYS + ['base-dir']) + self.keyorder += ['base-dir'] def get_unique_key(self): return super().get_unique_key() + [self.base_dir] diff --git a/buildstream/source.py b/buildstream/source.py index bb54110ca..bc4bae9db 100644 --- a/buildstream/source.py +++ b/buildstream/source.py @@ -302,6 +302,8 @@ def __init__(self, context, project, meta, *, alias_override=None): # FIXME: Reconstruct a MetaSource from a Source instead of storing it. self.__meta = meta # MetaSource stored so we can copy this source later. + self.keyorder = ['directory'] + self.keyorder + # Collect the composited element configuration and # ask the element to configure itself. self.__init_defaults(meta)