Qinusty pushed to branch Qinusty/235-manifest at BuildStream / buildstream
Commits:
7 changed files:
- buildstream/_frontend/cli.py
- + buildstream/_manifest.py
- buildstream/source.py
- + tests/manifest/manifest.py
- + tests/manifest/project/elements/base.bst
- + tests/manifest/project/files/hello/file.txt
- + tests/manifest/project/project.conf
Changes:
| ... | ... | @@ -3,6 +3,7 @@ import sys |
| 3 | 3 |
|
| 4 | 4 |
import click
|
| 5 | 5 |
from .. import _yaml
|
| 6 |
+from .. import _manifest
|
|
| 6 | 7 |
from .._exceptions import BstError, LoadError, AppError
|
| 7 | 8 |
from .._versions import BST_FORMAT_VERSION
|
| 8 | 9 |
from .complete import main_bashcomplete, complete_path, CompleteUnhandled
|
| ... | ... | @@ -289,6 +290,15 @@ def init(app, project_name, format_version, element_path, force): |
| 289 | 290 |
##################################################################
|
| 290 | 291 |
# Build Command #
|
| 291 | 292 |
##################################################################
|
| 293 |
+def _validate_manifest_path(ctx, param, value):
|
|
| 294 |
+ if not value:
|
|
| 295 |
+ return
|
|
| 296 |
+ if value.lower().endswith(".yaml") or value.lower().endswith(".yml"):
|
|
| 297 |
+ return os.path.abspath(value)
|
|
| 298 |
+ else:
|
|
| 299 |
+ raise click.BadParameter("Manifest files are outputted as YAML\n\t" +
|
|
| 300 |
+ "Please provide a path with a valid file extension (yml, yaml)")
|
|
| 301 |
+ |
|
| 292 | 302 |
@cli.command(short_help="Build elements in a pipeline")
|
| 293 | 303 |
@click.option('--all', 'all_', default=False, is_flag=True,
|
| 294 | 304 |
help="Build elements that would not be needed for the current build plan")
|
| ... | ... | @@ -305,10 +315,16 @@ def init(app, project_name, format_version, element_path, force): |
| 305 | 315 |
help="Allow tracking to cross junction boundaries")
|
| 306 | 316 |
@click.option('--track-save', default=False, is_flag=True,
|
| 307 | 317 |
help="Deprecated: This is ignored")
|
| 318 |
+@click.option('--build-manifest', default=False, is_flag=True,
|
|
| 319 |
+ help="Produces a build manifest containing elements and sources.")
|
|
| 320 |
+@click.option('--manifest-path', default=None, type=click.Path(readable=False),
|
|
| 321 |
+ help="Provides a path for a build manifest to be written to.",
|
|
| 322 |
+ callback=_validate_manifest_path)
|
|
| 308 | 323 |
@click.argument('elements', nargs=-1,
|
| 309 | 324 |
type=click.Path(readable=False))
|
| 310 | 325 |
@click.pass_obj
|
| 311 |
-def build(app, elements, all_, track_, track_save, track_all, track_except, track_cross_junctions):
|
|
| 326 |
+def build(app, elements, all_, track_, track_save, build_manifest,
|
|
| 327 |
+ manifest_path, track_all, track_except, track_cross_junctions):
|
|
| 312 | 328 |
"""Build elements in a pipeline"""
|
| 313 | 329 |
|
| 314 | 330 |
if (track_except or track_cross_junctions) and not (track_ or track_all):
|
| ... | ... | @@ -329,6 +345,13 @@ def build(app, elements, all_, track_, track_save, track_all, track_except, trac |
| 329 | 345 |
track_cross_junctions=track_cross_junctions,
|
| 330 | 346 |
build_all=all_)
|
| 331 | 347 |
|
| 348 |
+ if build_manifest and not manifest_path:
|
|
| 349 |
+ manifest_path = os.path.join(app.project.directory,
|
|
| 350 |
+ "build_manifest.yaml")
|
|
| 351 |
+ if manifest_path:
|
|
| 352 |
+ _manifest.generate(app.context, app.stream.total_elements,
|
|
| 353 |
+ app._session_start, manifest_path)
|
|
| 354 |
+ |
|
| 332 | 355 |
|
| 333 | 356 |
##################################################################
|
| 334 | 357 |
# Fetch Command #
|
| 1 |
+#
|
|
| 2 |
+# Copyright (C) 2018 Codethink Limited
|
|
| 3 |
+#
|
|
| 4 |
+# This program is free software; you can redistribute it and/or
|
|
| 5 |
+# modify it under the terms of the GNU Lesser General Public
|
|
| 6 |
+# License as published by the Free Software Foundation; either
|
|
| 7 |
+# version 2 of the License, or (at your option) any later version.
|
|
| 8 |
+#
|
|
| 9 |
+# This library is distributed in the hope that it will be useful,
|
|
| 10 |
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
| 11 |
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
| 12 |
+# Lesser General Public License for more details.
|
|
| 13 |
+#
|
|
| 14 |
+# You should have received a copy of the GNU Lesser General Public
|
|
| 15 |
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
|
| 16 |
+#
|
|
| 17 |
+# Authors:
|
|
| 18 |
+# Josh Smith <josh smith codethink co uk>
|
|
| 19 |
+ |
|
| 20 |
+from ruamel import yaml
|
|
| 21 |
+from . import __version__ as bst_version
|
|
| 22 |
+from . import _yaml
|
|
| 23 |
+from ._message import MessageType, Message
|
|
| 24 |
+ |
|
| 25 |
+# generate():
|
|
| 26 |
+#
|
|
| 27 |
+# Generates a manfiest file from the collection of elements.
|
|
| 28 |
+#
|
|
| 29 |
+# Args:
|
|
| 30 |
+# context (Context): Application context
|
|
| 31 |
+# elements (list of Element): Collection of elements to build the manifest from
|
|
| 32 |
+# build_start_datetime (datetime): The start of the build assosciated with this manifest
|
|
| 33 |
+# manifest_path (str): Absolute path to write the manifest file to.
|
|
| 34 |
+#
|
|
| 35 |
+def generate(context, elements, build_start_datetime, manifest_path):
|
|
| 36 |
+ with context.timed_activity("Building Manifest"):
|
|
| 37 |
+ manifest = _build(elements, build_start_datetime)
|
|
| 38 |
+ |
|
| 39 |
+ _yaml.dump(manifest, manifest_path)
|
|
| 40 |
+ context.message(Message(None, MessageType.STATUS,
|
|
| 41 |
+ "Manifest saved to {}".format(manifest_path)))
|
|
| 42 |
+ |
|
| 43 |
+# _build():
|
|
| 44 |
+#
|
|
| 45 |
+# Builds a manifest (dictionary) using the provided elements to be stored.
|
|
| 46 |
+#
|
|
| 47 |
+# Args:
|
|
| 48 |
+# elements (list of Element): Collection of elements to build the manifest from
|
|
| 49 |
+# build_start_datetime (datetime): The start of the build assosciated with this manifest
|
|
| 50 |
+#
|
|
| 51 |
+# Returns:
|
|
| 52 |
+# (CommentedMap): A dictionary containing the entire
|
|
| 53 |
+# manifest produced from the provided elements.
|
|
| 54 |
+#
|
|
| 55 |
+def _build(elements, build_start_datetime):
|
|
| 56 |
+ manifest = yaml.comments.CommentedMap()
|
|
| 57 |
+ |
|
| 58 |
+ # Add BuildStream Version
|
|
| 59 |
+ manifest['BuildStream_Version'] = "{}".format(bst_version)
|
|
| 60 |
+ # Add Build Date
|
|
| 61 |
+ manifest['Build_Date'] = build_start_datetime.isoformat()
|
|
| 62 |
+ manifest['Elements'] = yaml.comments.CommentedMap()
|
|
| 63 |
+ |
|
| 64 |
+ # Sort elements
|
|
| 65 |
+ elements = sorted(elements, key=lambda e: e.normal_name)
|
|
| 66 |
+ |
|
| 67 |
+ # Add Elements
|
|
| 68 |
+ for elem in elements:
|
|
| 69 |
+ manifest['Elements'][elem.normal_name] = _build_element(elem)
|
|
| 70 |
+ |
|
| 71 |
+ return manifest
|
|
| 72 |
+ |
|
| 73 |
+# _build_element():
|
|
| 74 |
+#
|
|
| 75 |
+# Builds a manifest segment for an individual element.
|
|
| 76 |
+#
|
|
| 77 |
+# Args:
|
|
| 78 |
+# element (Element): Element to extract information from
|
|
| 79 |
+#
|
|
| 80 |
+# Returns:
|
|
| 81 |
+# (CommentedMap): A dictionary containing the information
|
|
| 82 |
+# extracted from the provided element
|
|
| 83 |
+#
|
|
| 84 |
+def _build_element(element):
|
|
| 85 |
+ element_dict = yaml.comments.CommentedMap()
|
|
| 86 |
+ sources = yaml.comments.CommentedMap()
|
|
| 87 |
+ # Add Cache Key
|
|
| 88 |
+ cache_key = element._get_cache_key()
|
|
| 89 |
+ if cache_key:
|
|
| 90 |
+ element_dict["Cache_Key"] = cache_key
|
|
| 91 |
+ |
|
| 92 |
+ # Add sources
|
|
| 93 |
+ for source in element.sources():
|
|
| 94 |
+ src = _build_source(source)
|
|
| 95 |
+ if src:
|
|
| 96 |
+ source_desc = "{}({})".format(source._get_element_index(), type(source).__name__)
|
|
| 97 |
+ sources[source_desc] = src
|
|
| 98 |
+ if sources:
|
|
| 99 |
+ element_dict['Sources'] = sources
|
|
| 100 |
+ |
|
| 101 |
+ |
|
| 102 |
+ return element_dict
|
|
| 103 |
+ |
|
| 104 |
+# _build_source():
|
|
| 105 |
+#
|
|
| 106 |
+# Builds a manifest segment for an individual source.
|
|
| 107 |
+#
|
|
| 108 |
+# Args:
|
|
| 109 |
+# source (Source): Source to extract information from
|
|
| 110 |
+#
|
|
| 111 |
+# Returns:
|
|
| 112 |
+# (CommentedMap): A dictionary containing the information
|
|
| 113 |
+# extracted from the provided source
|
|
| 114 |
+#
|
|
| 115 |
+def _build_source(source):
|
|
| 116 |
+ src = yaml.comments.CommentedMap()
|
|
| 117 |
+ if hasattr(source, "url") and source.url:
|
|
| 118 |
+ src["url"] = source.url
|
|
| 119 |
+ if hasattr(source, "ref") and source.ref:
|
|
| 120 |
+ src["ref"] = source.ref
|
|
| 121 |
+ if hasattr(source, "path") and source.path:
|
|
| 122 |
+ src["path"] = source.path
|
|
| 123 |
+ |
|
| 124 |
+ return src if src else None
|
| ... | ... | @@ -786,6 +786,9 @@ class Source(Plugin): |
| 786 | 786 |
else:
|
| 787 | 787 |
return None
|
| 788 | 788 |
|
| 789 |
+ def _get_element_index(self):
|
|
| 790 |
+ return self.__element_index
|
|
| 791 |
+ |
|
| 789 | 792 |
#############################################################
|
| 790 | 793 |
# Local Private Methods #
|
| 791 | 794 |
#############################################################
|
| 1 |
+import pytest
|
|
| 2 |
+import os
|
|
| 3 |
+from ruamel import yaml
|
|
| 4 |
+ |
|
| 5 |
+from tests.testutils import cli
|
|
| 6 |
+ |
|
| 7 |
+# Project directory
|
|
| 8 |
+DATA_DIR = os.path.join(
|
|
| 9 |
+ os.path.dirname(os.path.realpath(__file__)),
|
|
| 10 |
+ "project",
|
|
| 11 |
+)
|
|
| 12 |
+ |
|
| 13 |
+ |
|
| 14 |
+@pytest.mark.datafiles(DATA_DIR)
|
|
| 15 |
+@pytest.mark.parametrize("specify_path, build_manifest", [
|
|
| 16 |
+ (True, True), (True, False), (False, True)
|
|
| 17 |
+])
|
|
| 18 |
+def test_manifest_created(tmpdir, cli, datafiles, specify_path, build_manifest):
|
|
| 19 |
+ project = os.path.join(datafiles.dirname, datafiles.basename)
|
|
| 20 |
+ |
|
| 21 |
+ manifest_path = os.path.join(str(tmpdir), "build_manifest.yaml")
|
|
| 22 |
+ |
|
| 23 |
+ args = ['build', "base.bst"]
|
|
| 24 |
+ |
|
| 25 |
+ if specify_path:
|
|
| 26 |
+ args += ["--manifest-path", manifest_path]
|
|
| 27 |
+ if build_manifest:
|
|
| 28 |
+ args.append("--build-manifest")
|
|
| 29 |
+ |
|
| 30 |
+ result = cli.run(project=project, args=args)
|
|
| 31 |
+ result.assert_success()
|
|
| 32 |
+ |
|
| 33 |
+ with open(manifest_path) as f:
|
|
| 34 |
+ manifest = yaml.load(f, Loader=yaml.loader.RoundTripLoader)
|
|
| 35 |
+ |
|
| 36 |
+ assert len(manifest["Elements"]["base"]["Sources"]) == 1
|
|
| 37 |
+ |
|
| 38 |
+ |
|
| 39 |
+@pytest.mark.datafiles(DATA_DIR)
|
|
| 40 |
+@pytest.mark.parametrize("extension, valid", [
|
|
| 41 |
+ (".yaml", True),
|
|
| 42 |
+ (".yml", True),
|
|
| 43 |
+ (".bst", False),
|
|
| 44 |
+ (".ynl", False),
|
|
| 45 |
+ (".xml", False),
|
|
| 46 |
+ (".mnf", False),
|
|
| 47 |
+ (".txt", False),
|
|
| 48 |
+ (".abc", False),
|
|
| 49 |
+ (".json", False)
|
|
| 50 |
+])
|
|
| 51 |
+def test_manifest_extensions(tmpdir, cli, datafiles, extension, valid):
|
|
| 52 |
+ project = os.path.join(datafiles.dirname, datafiles.basename)
|
|
| 53 |
+ |
|
| 54 |
+ manifest_path = os.path.join(str(tmpdir), "build_manifest{}" + extension)
|
|
| 55 |
+ |
|
| 56 |
+ result = cli.run(project=project, args=['build', "base.bst", "--manifest-path", manifest_path])
|
|
| 57 |
+ |
|
| 58 |
+ if valid:
|
|
| 59 |
+ result.assert_success()
|
|
| 60 |
+ else:
|
|
| 61 |
+ assert result.exit_code == 2
|
| 1 |
+kind: import
|
|
| 2 |
+description: Custom foo element
|
|
| 3 |
+ |
|
| 4 |
+sources:
|
|
| 5 |
+ - kind: local
|
|
| 6 |
+ path: files/hello
|
|
| \ No newline at end of file |
| 1 |
+# Project config for frontend build test
|
|
| 2 |
+name: test
|
|
| 3 |
+ |
|
| 4 |
+element-path: elements
|
