diff options
author | Jonathan Maw <jonathan.maw@codethink.co.uk> | 2017-06-06 10:40:48 +0000 |
---|---|---|
committer | Tristan Van Berkom <tristan.van.berkom@gmail.com> | 2017-06-06 10:40:48 +0000 |
commit | d39122416192c94fa1deef5053b5e288f8bbf826 (patch) | |
tree | 98281d1d845badc24e7886460ab534d3a193dcc7 /buildstream | |
parent | 0c9291c0b6cd3a2d41468b04fb2cd672c7b1d39f (diff) | |
download | buildstream-d39122416192c94fa1deef5053b5e288f8bbf826.tar.gz |
Jonathan/enhance script element
Diffstat (limited to 'buildstream')
-rw-r--r-- | buildstream/__init__.py | 1 | ||||
-rw-r--r-- | buildstream/element.py | 20 | ||||
-rw-r--r-- | buildstream/plugins/elements/script.py | 134 | ||||
-rw-r--r-- | buildstream/plugins/elements/script.yaml | 53 | ||||
-rw-r--r-- | buildstream/plugins/elements/x86image.py | 69 | ||||
-rw-r--r-- | buildstream/plugins/elements/x86image.yaml | 140 | ||||
-rw-r--r-- | buildstream/scriptelement.py | 246 |
7 files changed, 522 insertions, 141 deletions
diff --git a/buildstream/__init__.py b/buildstream/__init__.py index 714ca1f6d..cda2808d3 100644 --- a/buildstream/__init__.py +++ b/buildstream/__init__.py @@ -32,3 +32,4 @@ from .plugin import Plugin from .source import Source, Consistency from .element import Element, Scope from .buildelement import BuildElement +from .scriptelement import ScriptElement diff --git a/buildstream/element.py b/buildstream/element.py index 34cf44ac2..15ce644cb 100644 --- a/buildstream/element.py +++ b/buildstream/element.py @@ -224,6 +224,26 @@ class Element(Plugin): value = self.node_get_member(node, str, member_name, default_value=default_value) return self.__variables.subst(value) + def node_subst_list(self, node, member_name): + """Fetch a list from a node member, substituting any variables in the list + + Args: + node (dict): A dictionary loaded from YAML + member_name (str): The name of the member to fetch (a list) + + Returns: + The list in *member_name* + + Raises: + :class:`.LoadError` + + This is essentially the same as :func:`~buildstream.plugin.Plugin.node_get_member` + except that it assumes the expected type is a list of strings and will also + perform variable substitutions. + """ + value = self.node_get_member(node, list, member_name) + return [self.__variables.subst(x) for x in value] + def node_subst_list_element(self, node, member_name, indices): """Fetch the value of a list element from a node member, substituting any variables in the loaded value with the element contextual variables. diff --git a/buildstream/plugins/elements/script.py b/buildstream/plugins/elements/script.py index 1f452a86f..0fa06c802 100644 --- a/buildstream/plugins/elements/script.py +++ b/buildstream/plugins/elements/script.py @@ -17,135 +17,43 @@ # # Authors: # Tristan Van Berkom <tristan.vanberkom@codethink.co.uk> +# Jonathan Maw <jonathan.maw@codethink.co.uk> """Script element This element allows one to run some commands to mutate the input and create some output. -As with build elements, the output created by a script element is -collected from the ``%{install-root}`` directory. - The default configuration and possible options are as such: .. literalinclude:: ../../../buildstream/plugins/elements/script.yaml :language: yaml """ -import os -from buildstream import utils -from buildstream import Element, ElementError, Scope -from buildstream import SandboxFlags +import buildstream # Element implementation for the 'script' kind. -class ScriptElement(Element): - +class ScriptElement(buildstream.ScriptElement): def configure(self, node): - self.base_dep = self.node_get_member(node, str, 'base') - self.input_dep = self.node_get_member(node, str, 'input', '') or None - self.stage_mode = self.node_get_member(node, str, 'stage-mode') - self.collect = self.node_subst_member(node, 'collect', '%{install-root}') - - # Assert stage mode is valid - if self.stage_mode and self.stage_mode not in ['build', 'install']: - p = self.node_provenance(node, 'stage-mode') - raise ElementError("{}: Stage mode must be either 'build' or 'install'" - .format(p)) - - # Collect variable substituted commands - self.commands = [] - command_list = self.node_get_member(node, list, 'commands', default_value=[]) - for i in range(len(command_list)): - self.commands.append( - self.node_subst_list_element(node, 'commands', [i]) - ) - - # To be resolved in preflight when the pipeline is built - self.base_elt = None - self.input_elt = None - - def preflight(self): - - # Assert that the user did not list any runtime dependencies - runtime_deps = list(self.dependencies(Scope.RUN, recurse=False)) - if runtime_deps: - raise ElementError("{}: Only build type dependencies supported by script elements" + for n in self.node_get_member(node, list, 'layout', []): + dst = self.node_subst_member(n, 'destination') + elm = self.node_subst_member(n, 'element') + self.layout_add(elm, dst) + + cmds = [] + prefixes = ["pre-", "", "post-"] + if "commands" not in node: + raise ElementError("{}: Unexpectedly missing command group 'commands'" .format(self)) - - # Assert that the user did not specify any sources, as they will - # be ignored by this element type anyway - sources = list(self.sources()) - if sources: - raise ElementError("Script elements may not have sources") - - # Assert that a base and an input were specified - if not self.base_dep: - raise ElementError("{}: No base dependencies were specified".format(self)) - - # Now resolve the base and input elements - self.base_elt = self.search(Scope.BUILD, self.base_dep) - if self.input_dep: - self.input_elt = self.search(Scope.BUILD, self.input_dep) - - if self.base_elt is None: - raise ElementError("{}: Could not find base dependency {}".format(self, self.base_dep)) - - def get_unique_key(self): - return { - 'commands': self.commands, - 'base': self.base_dep, - 'input': self.input_dep, - 'stage-mode': self.stage_mode, - 'collect': self.collect - } - - def assemble(self, sandbox): - - directory = sandbox.get_directory() - environment = self.get_environment() - - # Stage the base in the sandbox root - with self.timed_activity("Staging {} as base".format(self.base_dep), silent_nested=True): - self.base_elt.stage_dependencies(sandbox, Scope.RUN) - - # Run any integration commands on the base - with self.timed_activity("Integrating sandbox", silent_nested=True): - for dep in self.base_elt.dependencies(Scope.RUN): - dep.integrate(sandbox) - - # Ensure some directories we'll need - cmd_dir = '/' - if self.stage_mode: - os.makedirs(os.path.join(directory, - 'buildstream', - 'build'), exist_ok=True) - os.makedirs(os.path.join(directory, - 'buildstream', - 'install'), exist_ok=True) - - # Stage the input - input_dir = os.path.join(os.sep, 'buildstream', self.stage_mode) - cmd_dir = input_dir - with self.timed_activity("Staging {} as input at {}" - .format(self.input_dep, input_dir), silent_nested=True): - self.input_elt.stage_dependencies(sandbox, Scope.RUN, path=input_dir) - - # Run the scripts - with self.timed_activity("Running script commands"): - for cmd in self.commands: - self.status("Running command", detail=cmd) - - # Note the -e switch to 'sh' means to exit with an error - # if any untested command fails. - exitcode = sandbox.run(['sh', '-c', '-e', cmd + '\n'], - 0, - cwd=cmd_dir, - env=environment) - if exitcode != 0: - raise ElementError("Command '{}' failed with exitcode {}".format(cmd, exitcode)) - - # Return the install dir - return self.collect + for prefix in prefixes: + if prefix + "commands" in node: + cmds += self.node_subst_list(node, prefix + "commands") + self.add_commands("commands", cmds) + + self.set_work_dir() + self.set_install_root() + self.set_root_read_only(self.node_get_member(node, bool, + 'root-read-only', False)) # Plugin entry point diff --git a/buildstream/plugins/elements/script.yaml b/buildstream/plugins/elements/script.yaml index 17e0cfa54..56a656cf6 100644 --- a/buildstream/plugins/elements/script.yaml +++ b/buildstream/plugins/elements/script.yaml @@ -1,36 +1,33 @@ +# Common script element variables +variables: + # Defines the directory that output is collected from once commands + # have been run. + install-root: /buildstream/install + # + # Defines the directory commands will be run from. + cwd: / + # + # Not directly used, but expected to be used when staging elements to be + # worked on. + build-root: /buildstream/build # Script element configuration config: - # A dependency of this element to use as the root filesystem - # for scripting, expressed as a project relative element bst - # filename. - # - # This will define what tools are available to use in - # the commands. At minimum, a shell should exist in - # the base dependencies in order to run any commands. - # - # base: foo.bst - - # A dependency of this element to manipulate as input, - # expressed as a project relative element bst filename - # - # input: bar.bst + # Defines whether to run the sandbox with '/' read-only. + # It is recommended to set root as read-only wherever possible. + root-read-only: False - # Defines where to stage the 'input' dependencies, the - # working directory for running commands will also be - # set to this directory. - # - # build: Stage the input in %{build-root} - # - # install: Stage the input directly at %{install-root} - # - stage-mode: build + # Defines where to stage elements which are direct or indirect dependencies. + # By default, all direct dependencies are staged to '/'. + # This is also commonly used to take one element as an environment + # containing the tools used to operate on the other element. + # layout: + # - element: foo-tools.bst + # destination: / + # - element: foo-system.bst + # destination: %{build-root} - # List of commands to run in the sandbox - # + # List of commands to run in the sandbox. commands: [] - # Directory to collect the output artifact from, - # default is %{install-root} - # collect: diff --git a/buildstream/plugins/elements/x86image.py b/buildstream/plugins/elements/x86image.py new file mode 100644 index 000000000..bd6962dcd --- /dev/null +++ b/buildstream/plugins/elements/x86image.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2017 Codethink Limited +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. +# +# Authors: +# Jonathan Maw <jonathan.maw@codethink.co.uk> + +"""x86 image build element + +A :mod:`ScriptElement <buildstream.scriptelement>` implementation for creating +x86 disk images + +The x86-image default configuration: + .. literalinclude:: ../../../buildstream/plugins/elements/x86image.yaml + :language: yaml +""" + +from buildstream import ScriptElement + + +# Element implementation for the 'x86image' kind. +class X86ImageElement(ScriptElement): + def configure(self, node): + prefixes = ["pre-", "", "post-"] + groups = [ + "filesystem-tree-setup-commands", + "filesystem-image-creation-commands", + "partition-commands", + "final-commands" + ] + for group in groups: + cmds = [] + if group not in node: + raise ElementError("{}: Unexpectedly missing command group '{}'" + .format(self, group)) + for prefix in prefixes: + if prefix + group in node: + cmds += self.node_subst_list(node, prefix + group) + self.add_commands(group, cmds) + + self.layout_add(self.node_subst_member(node, 'base'), "/") + self.layout_add(self.node_subst_member(node, 'input'), + self.get_variable('build-root')) + + self.set_work_dir() + self.set_install_root() + self.set_root_read_only(True) + + def preflight(self): + super(X86ImageElement, self).preflight() + self.validate_layout() + + +# Plugin entry point +def setup(): + return X86ImageElement diff --git a/buildstream/plugins/elements/x86image.yaml b/buildstream/plugins/elements/x86image.yaml new file mode 100644 index 000000000..9ca6d0e89 --- /dev/null +++ b/buildstream/plugins/elements/x86image.yaml @@ -0,0 +1,140 @@ +#x86 image default configuration + +variables: + # Size of the disk to create + # + # Should be able to calculate this based on the space + # used, however it must be a multiple of (63 * 512) bytes + # as mtools wants a size that is devisable by sectors (512 bytes) + # per track (63). + boot-size: 252000K + rootfs-size: 4G + swap-size: 1G + sector-size: 512 + +config: + # The element that should be staged into "/". It must contain + # all the tools required to generate the image + # base: image-tools.bst + + # The element that should be staged into %{build-root}. It is expected + # to be the system that you're planning to turn into an image. + # input: foo-system.bst + + root-read-only: True + filesystem-tree-setup-commands: + - | + # XXX Split up the boot directory and the other + # + # This should be changed so that the /boot directory + # is created separately. + + cd /buildstream + mkdir -p /buildstream/sda1 + mkdir -p /buildstream/sda2 + + mv %{build-root}/boot/* /buildstream/sda1 + mv %{build-root}/* /buildstream/sda2 + + - | + # Generate an fstab + cat > /buildstream/sda2/etc/fstab << EOF + /dev/sda2 / ext4 defaults,rw,noatime 0 1 + /dev/sda1 /boot vfat defaults 0 2 + /dev/sda3 none swap defaults 0 0 + EOF + + - | + # Create the syslinux config + mkdir -p /buildstream/sda1/syslinux + cat > /buildstream/sda1/syslinux/syslinux.cfg << EOF + PROMPT 0 + TIMEOUT 5 + + ALLOWOPTIONS 1 + SERIAL 0 115200 + + DEFAULT boot + LABEL boot + + KERNEL /vmlinuz + INITRD /initramfs.gz + + APPEND root=/dev/sda2 rootfstype=ext4 rootdelay=20 init=/usr/lib/systemd/systemd + EOF + filesystem-image-creation-commands: + - | + # Create the vfat image + truncate -s %{boot-size} /buildstream/sda1.img + mkdosfs /buildstream/sda1.img + + - | + # Copy all that stuff into the image + mcopy -D s -i /buildstream/sda1.img -s /buildstream/sda1/* ::/ + + - | + # Install the bootloader on the image, it should get the config file + # from inside the vfat image, I think + syslinux --directory /syslinux/ /buildstream/sda1.img + + - | + # Now create the root filesys on sda2 + truncate -s %{rootfs-size} /buildstream/sda2.img + mkfs.ext4 -F -i 8192 /buildstream/sda2.img -L root -d /buildstream/sda2 + + - | + # Create swap + truncate -s %{swap-size} /buildstream/sda3.img + mkswap -L swap /buildstream/sda3.img + partition-commands: + - | + ######################################## + # Partition the disk # + ######################################## + + # First get the size in bytes + sda1size=$(stat --printf="%s" /buildstream/sda1.img) + sda2size=$(stat --printf="%s" /buildstream/sda2.img) + sda3size=$(stat --printf="%s" /buildstream/sda3.img) + + # Now convert to sectors + sda1sec=$(( ${sda1size} / %{sector-size} )) + sda2sec=$(( ${sda2size} / %{sector-size} )) + sda3sec=$(( ${sda3size} / %{sector-size} )) + + # Now get the offsets in sectors, first sector is MBR + sda1offset=1 + sda2offset=$(( ${sda1offset} + ${sda1sec} )) + sda3offset=$(( ${sda2offset} + ${sda2sec} )) + + # Get total disk size in sectors and bytes + sdasectors=$(( ${sda3offset} + ${sda3sec} )) + sdabytes=$(( ${sdasectors} * %{sector-size} )) + + # Create the main disk and do the partitioning + truncate -s ${sdabytes} /buildstream/sda.img + parted -s /buildstream/sda.img mklabel msdos + parted -s /buildstream/sda.img unit s mkpart primary fat32 ${sda1offset} $(( ${sda1offset} + ${sda1sec} - 1 )) + parted -s /buildstream/sda.img unit s mkpart primary ext2 ${sda2offset} $(( ${sda2offset} + ${sda2sec} - 1 )) + parted -s /buildstream/sda.img unit s mkpart primary linux-swap ${sda3offset} $(( ${sda3offset} + ${sda3sec} - 1 )) + + # Make partition 1 the boot partition + parted -s /buildstream/sda.img set 1 boot on + + # Now splice the existing filesystems directly into the image + dd if=/buildstream/sda1.img of=/buildstream/sda.img \ + ibs=%{sector-size} obs=%{sector-size} conv=notrunc \ + count=${sda1sec} seek=${sda1offset} + + dd if=/buildstream/sda2.img of=/buildstream/sda.img \ + ibs=%{sector-size} obs=%{sector-size} conv=notrunc \ + count=${sda2sec} seek=${sda2offset} + + dd if=/buildstream/sda3.img of=/buildstream/sda.img \ + ibs=%{sector-size} obs=%{sector-size} conv=notrunc \ + count=${sda3sec} seek=${sda3offset} + final-commands: + - | + # Move the image where it will be collected + mv /buildstream/sda.img %{install-root} + chmod 0644 %{install-root}/sda.img diff --git a/buildstream/scriptelement.py b/buildstream/scriptelement.py new file mode 100644 index 000000000..a4144f7af --- /dev/null +++ b/buildstream/scriptelement.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2017 Codethink Limited +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. +# +# Authors: +# Jonathan Maw <jonathan.maw@codethink.co.uk> + +"""The ScriptElement class is a convenience class one can derive for +implementing elements that stage elements and run command-lines on them. + +Any derived classes must write their own configure() implementation, using +the public APIs exposed in this class. + +Derived classes must also chain up to the parent method in their preflight() +implementations. + + +""" + +import os +from collections import OrderedDict + +from . import Element, ElementError, Scope, SandboxFlags + + +class ScriptElement(Element): + __install_root = "/" + __cwd = "/" + __root_read_only = False + __commands = None + __layout = None + + def set_work_dir(self, work_dir=None): + """Sets the working dir + + The working dir (a.k.a. cwd) is the directory which commands will be + called from. + + Args: + work_dir (str): The working directory. If called without this argument + set, it'll default to the value of the variable ``cwd``. + """ + if work_dir is None: + self.__cwd = self.get_variable("cwd") or "/" + else: + self.__cwd = work_dir + + def set_install_root(self, install_root=None): + """Sets the install root + + The install root is the directory which output will be collected from + once the commands have been run. + + Args: + install_root(str): The install root. If called without this argument + set, it'll default to the value of the variable ``install-root``. + """ + if install_root is None: + self.__install_root = self.get_variable("install-root") or "/" + else: + self.__install_root = install_root + + def set_root_read_only(self, root_read_only): + """Sets root read-only + + When commands are run, if root_read_only is true, then the root of the + filesystem will be protected. This is strongly recommended whenever + possible. + + If this variable is not set, the default permission is read-write. + + Args: + root_read_only (bool): Whether to mark the root filesystem as + read-only. + """ + self.__root_read_only = root_read_only + + def layout_add(self, element, destination): + """Adds an element-destination pair to the layout. + + Layout is a way of defining how dependencies should be added to the + staging area for running commands. + + Args: + element (str): The name of the element to stage. This may be any + element found in the dependencies, whether it is a direct or indirect + dependency. + destination (str): The path inside the staging area for where to + stage this element. If it is not "/", then integration commands will + not be run. + + If this function is never called, then the default behavior is to just + stage the Scope.BUILD dependencies of the element in question at the + sandbox root. Otherwise, the Scope.RUN dependencies of each specified + element will be staged in their specified destination directories. + """ + if not self.__layout: + self.__layout = [] + self.__layout.append({"element": element, + "destination": destination}) + + def add_commands(self, group_name, command_list): + """Adds a list of commands under the group-name. + + .. note:: + + Command groups will be run in the order they were added. + + .. note:: + + This does not perform substitutions automatically. They must + be performed beforehand (see + :func:`~buildstream.element.Element.node_subst_list`) + + Args: + group_name (str): The name of the group of commands. + command_list (list): The list of commands to be run. + """ + if not self.__commands: + self.__commands = OrderedDict() + self.__commands[group_name] = command_list + + def __validate_layout(self): + if self.__layout: + # Cannot proceeed if layout is used, but none are for "/" + root_defined = any([(entry['destination'] == '/') for entry in self.__layout]) + if not root_defined: + raise ElementError("{}: Using layout, but none are staged as '/'" + .format(self)) + + # Cannot proceed if layout specifies an element that isn't part + # of the dependencies. + rundeps = set() + for rd in self.dependencies(Scope.BUILD, recurse=False): + rundeps |= set(rd.dependencies(Scope.RUN)) + for item in self.__layout: + element_name = item['element'] + element_found = any([(rd.name == element_name) for rd in rundeps]) + if not element_found: + raise ElementError("{}: '{}' in layout not found in dependencies" + .format(self, element_name)) + + def preflight(self): + # All dependencies on script elements must be BUILD only, otherwise + # the element will get pulled into dependencies + if any(self.dependencies(Scope.RUN, recurse=False)): + raise ElementError("{}: Only build type dependencies supported by script elements" + .format(self)) + + # Script elements have no use for sources, so they should not be present. + if any(self.sources()): + raise ElementError("{}: Script elements should not have sources".format(self)) + + # The layout, if set, must make sense. + self.__validate_layout() + + def get_unique_key(self): + return { + 'commands': self.__commands, + 'cwd': self.__cwd, + 'install-root': self.__install_root, + 'layout': self.__layout, + 'root-read-only': self.__root_read_only + } + + def assemble(self, sandbox): + # Stage the elements, and run integration commands where appropriate. + if not self.__layout: + # if no layout set, stage all dependencies into / + for build_dep in self.dependencies(Scope.BUILD, recurse=False): + with self.timed_activity("Staging {} at /" + .format(build_dep), silent_nested=True): + build_dep.stage_dependencies(sandbox, Scope.RUN, path="/") + + for build_dep in self.dependencies(Scope.BUILD, recurse=False): + with self.timed_activity("Integrating {}".format(build_dep), silent_nested=True): + for dep in build_dep.dependencies(Scope.RUN): + dep.integrate(sandbox) + else: + # If layout, follow its rules. + for item in self.__layout: + for bd in self.dependencies(Scope.BUILD, recurse=False): + element = bd.search(Scope.RUN, item['element']) + if element: + break + if item['destination'] == '/': + with self.timed_activity("Staging {} at /".format(item['element']), + silent_nested=True): + element.stage_dependencies(sandbox, Scope.RUN) + else: + with self.timed_activity("Staging {} at {}" + .format(item['element'], item['destination']), + silent_nested=True): + real_dstdir = os.path.join(sandbox.get_directory(), + item['destination'].lstrip(os.sep)) + os.makedirs(os.path.dirname(real_dstdir), exist_ok=True) + element.stage_dependencies(sandbox, Scope.RUN, path=item['destination']) + + for item in self.__layout: + for bd in self.dependencies(Scope.BUILD, recurse=False): + element = bd.search(Scope.RUN, item['element']) + if element: + break + # Integration commands can only be run for elements staged to / + if item['destination'] == '/': + with self.timed_activity("Integrating {}".format(item['element']), + silent_nested=True): + for dep in element.dependencies(Scope.RUN): + dep.integrate(sandbox) + + os.makedirs(os.path.join(sandbox.get_directory(), self.__install_root.lstrip(os.sep)), + exist_ok=True) + + environment = self.get_environment() + for groupname, commands in self.__commands.items(): + with self.timed_activity("Running '{}'".format(groupname)): + for cmd in commands: + self.status("Running command", detail=cmd) + # Note the -e switch to 'sh' means to exit with an error + # if any untested command fails. + exitcode = sandbox.run(['sh', '-c', '-e', cmd + '\n'], + SandboxFlags.ROOT_READ_ONLY if self.__root_read_only else 0, + cwd=self.__cwd, + env=environment) + if exitcode != 0: + raise ElementError("Command '{}' failed with exitcode {}".format(cmd, exitcode)) + + # Return where the result can be collected from + return self.__install_root + + +def setup(): + return ScriptElement |