[rtems-central commit] runactions: New

Sebastian Huber sebh at rtems.org
Tue Nov 21 13:35:41 UTC 2023


Module:    rtems-central
Branch:    master
Commit:    cd6cbe8792f038fc7a27d37c0804afa0a244eca9
Changeset: http://git.rtems.org/rtems-central/commit/?id=cd6cbe8792f038fc7a27d37c0804afa0a244eca9

Author:    Sebastian Huber <sebastian.huber at embedded-brains.de>
Date:      Tue Nov 21 11:13:16 2023 +0100

runactions: New

---

 rtemsspec/packagebuildfactory.py                   |   2 +
 rtemsspec/runactions.py                            | 314 +++++++++++++++++++++
 .../spec-packagebuild/qdp/output/run-actions.yml   |  13 +
 .../tests/spec-packagebuild/qdp/package-build.yml  |   2 +
 .../spec-packagebuild/qdp/steps/run-actions.yml    | 282 ++++++++++++++++++
 rtemsspec/tests/test_packagebuild.py               |  14 +
 rtemsspec/util.py                                  |  42 ++-
 spec-qdp/spec/qdp-action-copy-and-substitute.yml   |  42 +++
 spec-qdp/spec/qdp-action-create-ini-file.yml       |  35 +++
 .../spec/qdp-action-directory-state-add-files.yml  |  34 +++
 ...-action-directory-state-add-tarfile-members.yml |  43 +++
 .../spec/qdp-action-directory-state-add-tree.yml   |  49 ++++
 spec-qdp/spec/qdp-action-directory-state-clear.yml |  26 ++
 .../spec/qdp-action-directory-state-copy-tree.yml  |  49 ++++
 .../spec/qdp-action-directory-state-move-tree.yml  |  49 ++++
 spec-qdp/spec/qdp-action-list.yml                  |  16 ++
 spec-qdp/spec/qdp-action-mkdir.yml                 |  35 +++
 .../spec/qdp-action-remove-empty-directories.yml   |  28 ++
 spec-qdp/spec/qdp-action-remove-glob.yml           |  38 +++
 spec-qdp/spec/qdp-action-remove-tree.yml           |  30 ++
 spec-qdp/spec/qdp-action-remove.yml                |  31 ++
 spec-qdp/spec/qdp-action-subprocess-env-list.yml   |  16 ++
 spec-qdp/spec/qdp-action-subprocess-env.yml        |  33 +++
 spec-qdp/spec/qdp-action-subprocess.yml            |  42 +++
 spec-qdp/spec/qdp-action-touch.yml                 |  30 ++
 spec-qdp/spec/qdp-action.yml                       |  26 ++
 spec-qdp/spec/qdp-ini-file-key-value-pair-list.yml |  16 ++
 spec-qdp/spec/qdp-ini-file-key-value-pair.yml      |  31 ++
 spec-qdp/spec/qdp-ini-file-section-list.yml        |  16 ++
 spec-qdp/spec/qdp-ini-file-section.yml             |  29 ++
 spec-qdp/spec/qdp-run-actions.yml                  |  31 ++
 31 files changed, 1442 insertions(+), 2 deletions(-)

diff --git a/rtemsspec/packagebuildfactory.py b/rtemsspec/packagebuildfactory.py
index ef4a950b..600c58de 100644
--- a/rtemsspec/packagebuildfactory.py
+++ b/rtemsspec/packagebuildfactory.py
@@ -27,12 +27,14 @@
 from rtemsspec.archiver import Archiver
 from rtemsspec.directorystate import DirectoryState
 from rtemsspec.packagebuild import BuildItemFactory, PackageVariant
+from rtemsspec.runactions import RunActions
 
 
 def create_build_item_factory() -> BuildItemFactory:
     """ Creates the default build item factory. """
     factory = BuildItemFactory()
     factory.add_constructor("qdp/build-step/archive", Archiver)
+    factory.add_constructor("qdp/build-step/run-actions", RunActions)
     factory.add_constructor("qdp/directory-state/generic", DirectoryState)
     factory.add_constructor("qdp/directory-state/repository", DirectoryState)
     factory.add_constructor("qdp/directory-state/unpacked-archive",
diff --git a/rtemsspec/runactions.py b/rtemsspec/runactions.py
new file mode 100644
index 00000000..1da507cd
--- /dev/null
+++ b/rtemsspec/runactions.py
@@ -0,0 +1,314 @@
+# SPDX-License-Identifier: BSD-2-Clause
+""" This module provides a build step to run actions. """
+
+# Copyright (C) 2022, 2023 embedded brains GmbH (http://www.embedded-brains.de)
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in the
+#    documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import copy
+import os
+import logging
+from pathlib import Path
+import shutil
+import subprocess
+from typing import Any, Dict, List, Optional, Union
+
+from rtemsspec.directorystate import DirectoryState
+from rtemsspec.items import Item, ItemGetValueContext, is_enabled
+from rtemsspec.packagebuild import BuildItem, PackageBuildDirector
+from rtemsspec.util import copy_and_substitute, remove_empty_directories
+
+
+def _env_clear(item: "RunActions", env: Dict, _action: Dict[str, str]) -> None:
+    logging.info("%s: env: clear", item.uid)
+    env.clear()
+
+
+def _env_path_append(item: "RunActions", env: Dict, action: Dict[str,
+                                                                 str]) -> None:
+    name = action["name"]
+    value = action["value"]
+    logging.info("%s: env: append '%s' to %s", item.uid, value, name)
+    env[name] = f"{env[name]}:{value}"
+
+
+def _env_path_prepend(item: "RunActions", env: Dict,
+                      action: Dict[str, str]) -> None:
+    name = action["name"]
+    value = action["value"]
+    logging.info("%s: env: prepend '%s' to %s", item.uid, value, name)
+    env[name] = f"{value}:{env[name]}"
+
+
+def _env_set(item: "RunActions", env: Dict, action: Dict[str, str]) -> None:
+    name = action["name"]
+    value = action["value"]
+    logging.info("%s: env: %s = '%s'", item.uid, name, value)
+    env[name] = value
+
+
+def _env_unset(item: "RunActions", env: Dict, action: Dict[str, str]) -> None:
+    name = action["name"]
+    logging.info("%s: env: unset %s", item.uid, name)
+    del env[name]
+
+
+_ENV_ACTIONS = {
+    "clear": _env_clear,
+    "path-append": _env_path_append,
+    "path-prepend": _env_path_prepend,
+    "set": _env_set,
+    "unset": _env_unset
+}
+
+
+def _get_host_processor_count(_ctx: ItemGetValueContext) -> str:
+    count = os.cpu_count()
+    return str(count if count is not None else 1)
+
+
+class RunActions(BuildItem):
+    """ Runs actions. """
+
+    def __init__(self, director: PackageBuildDirector, item: Item):
+        super().__init__(director, item)
+        self.mapper.add_get_value(f"{self.item.type}:/host-processor-count",
+                                  _get_host_processor_count)
+
+    def run(self):
+        for index, action in enumerate(self["actions"]):
+            action_type = action["action"]
+            logging.info("%s: run action %i: %s", self.uid, index, action_type)
+            if is_enabled(self.enabled_set, action["enabled-by"]):
+                output_name = action.get("output-name", None)
+                if output_name is None:
+                    output = None
+                else:
+                    try:
+                        output = self.output(output_name)
+                    except ValueError:
+                        continue
+                RunActions._ACTIONS[action_type](self, action, output)
+
+    def _copy_and_substitute(self, action: Dict,
+                             output: Optional[DirectoryState]) -> None:
+        assert isinstance(output, DirectoryState)
+        input_state = self.input(action["input-name"])
+        assert isinstance(input_state, DirectoryState)
+        source = action["source"]
+        source_base = input_state.directory
+        target_base = output.directory
+        if source is None:
+            prefix = action["target"]
+            if prefix is None:
+                prefix = "."
+            targets: List[str] = []
+            for source_file in input_state:
+                tail = os.path.relpath(source_file, source_base)
+                target_file = os.path.join(target_base, prefix, tail)
+                targets.append(tail)
+                copy_and_substitute(source_file, target_file, self.mapper,
+                                    self.uid)
+            output.add_files(targets)
+        else:
+            source_file = os.path.join(source_base, source)
+            target = action["target"]
+            if target is None:
+                target_file = output.file
+            else:
+                output.add_files([target])
+                target_file = os.path.join(target_base, target)
+            copy_and_substitute(source_file, target_file, self.mapper,
+                                self.uid)
+
+    def _create_ini_file(self, action: Dict,
+                         output: Optional[DirectoryState]) -> None:
+        assert isinstance(output, DirectoryState)
+        target = action["target"]
+        if target is None:
+            target = output.file
+        else:
+            output.add_files([target])
+            target = os.path.join(output.directory, target)
+        logging.info("%s: create: %s", self.uid, target)
+        os.makedirs(os.path.dirname(target), exist_ok=True)
+        with open(target, "w", encoding="utf-8") as dst:
+            for section in action["sections"]:
+                if not is_enabled(self.enabled_set, section["enabled-by"]):
+                    continue
+                dst.write(f"[{section['name']}]\n")
+                for key_value in section["key-value-pairs"]:
+                    if not is_enabled(self.enabled_set,
+                                      key_value["enabled-by"]):
+                        continue
+                    dst.write(f"{key_value['key']} = {key_value['value']}\n")
+
+    def _directory_state_clear(self, _action: Dict,
+                               output: Optional[DirectoryState]) -> None:
+        assert isinstance(output, DirectoryState)
+        output.clear()
+
+    def _directory_state_add_files(self, action: Dict,
+                                   output: Optional[DirectoryState]) -> None:
+        assert isinstance(output, DirectoryState)
+        root = Path(action["path"]).absolute()
+        pattern = action["pattern"]
+        logging.info("%s: add files matching '%s' in: %s", self.uid, pattern,
+                     root)
+        base = output.directory
+        output.add_files(
+            [os.path.relpath(path, base) for path in root.glob(pattern)])
+
+    def _directory_state_add_tarfile_members(
+            self, action: Dict, output: Optional[DirectoryState]) -> None:
+        assert isinstance(output, DirectoryState)
+        root = Path(action["search-path"])
+        pattern = action["pattern"]
+        logging.info("%s: search for tarfiles matching '%s' in: %s", self.uid,
+                     pattern, root)
+        for path in root.glob(pattern):
+            output.add_tarfile_members(path, action["prefix-path"],
+                                       action["extract"])
+
+    def _directory_state_tree_op(self, action: Dict,
+                                 output: Optional[DirectoryState],
+                                 tree_op: Any) -> None:
+        assert isinstance(output, DirectoryState)
+        root = Path(action["root"]).absolute()
+        prefix = action["prefix"]
+        if prefix is None:
+            prefix = "."
+        tree_op(output, root, prefix, action["excludes"])
+
+    def _directory_state_add_tree(self, action: Dict,
+                                  output: Optional[DirectoryState]) -> None:
+        self._directory_state_tree_op(action, output, DirectoryState.add_tree)
+
+    def _directory_state_copy_tree(self, action: Dict,
+                                   output: Optional[DirectoryState]) -> None:
+        self._directory_state_tree_op(action, output, DirectoryState.copy_tree)
+
+    def _directory_state_move_tree(self, action: Dict,
+                                   output: Optional[DirectoryState]) -> None:
+        self._directory_state_tree_op(action, output, DirectoryState.move_tree)
+
+    def _process(self, action: Dict,
+                 _output: Optional[DirectoryState]) -> None:
+        env: Union[Dict, None] = None
+        env_actions = action["env"]
+        if env_actions:
+            logging.info("%s: env: modify", self.uid)
+            env = copy.deepcopy(os.environ.copy())
+            for env_action in env_actions:
+                _ENV_ACTIONS[env_action["action"]](self, env, env_action)
+        cmd = action["command"]
+        cwd = action["working-directory"]
+        logging.info("%s: run in '%s': %s", self.uid, cwd,
+                     " ".join(f"'{i}'" for i in cmd))
+        status = subprocess.run(cmd, env=env, check=False, cwd=cwd)
+        expected_return_code = action["expected-return-code"]
+        if expected_return_code is not None:
+            assert status.returncode == expected_return_code
+
+    def _mkdir(self, action: Dict, _output: Optional[DirectoryState]) -> None:
+        path = Path(action["path"])
+        logging.info("%s: make directory: %s", self.uid, path)
+        path.mkdir(parents=action["parents"], exist_ok=action["exist-ok"])
+
+    def _remove_path(self, path: Path) -> None:
+        if path.is_dir():
+            logging.info("%s: remove directory: %s", self.uid, path)
+            path.rmdir()
+        else:
+            logging.info("%s: unlink file: %s", self.uid, path)
+            path.unlink()
+
+    def _remove(self, action: Dict, _output: Optional[DirectoryState]) -> None:
+        path = Path(action["path"])
+        if action["missing-ok"]:
+            try:
+                self._remove_path(path)
+            except FileNotFoundError:
+                pass
+        else:
+            self._remove_path(path)
+
+    def _remove_empty_directories(self, action: Dict,
+                                  _output: Optional[DirectoryState]) -> None:
+        remove_empty_directories(self.uid, action["path"])
+
+    def _remove_glob(self, action: Dict,
+                     _output: Optional[DirectoryState]) -> None:
+        root = Path(action["path"])
+        for pattern in action["patterns"]:
+            logging.info(
+                "%s: remove files and directories matching with '%s' in: %s",
+                self.uid, pattern, root)
+            for path in root.glob(pattern):
+                if path.is_dir():
+                    if action["remove-tree"]:
+                        logging.info("%s: remove directory tree: %s", self.uid,
+                                     path)
+                        shutil.rmtree(path)
+                    else:
+                        logging.info("%s: remove directory: %s", self.uid,
+                                     path)
+                        path.rmdir()
+                else:
+                    logging.info("%s: remove file: %s", self.uid, path)
+                    path.unlink()
+
+    def _remove_tree(self, action: Dict,
+                     _output: Optional[DirectoryState]) -> None:
+        path = action["path"]
+        logging.info("%s: remove directory tree: %s", self.uid, path)
+        if action["missing-ok"]:
+            try:
+                shutil.rmtree(path)
+            except FileNotFoundError:
+                pass
+        else:
+            shutil.rmtree(path)
+
+    def _touch(self, action: Dict, _output: Optional[DirectoryState]) -> None:
+        path = Path(action["path"])
+        logging.info("%s: touch file: %s", self.uid, path)
+        path.touch(exist_ok=action["exist-ok"])
+
+    _ACTIONS = {
+        "copy-and-substitute": _copy_and_substitute,
+        "create-ini-file": _create_ini_file,
+        "directory-state-add-files": _directory_state_add_files,
+        "directory-state-add-tarfile-members":
+        _directory_state_add_tarfile_members,
+        "directory-state-add-tree": _directory_state_add_tree,
+        "directory-state-clear": _directory_state_clear,
+        "directory-state-copy-tree": _directory_state_copy_tree,
+        "directory-state-move-tree": _directory_state_move_tree,
+        "mkdir": _mkdir,
+        "remove": _remove,
+        "remove-empty-directories": _remove_empty_directories,
+        "remove-glob": _remove_glob,
+        "remove-tree": _remove_tree,
+        "subprocess": _process,
+        "touch": _touch
+    }
diff --git a/rtemsspec/tests/spec-packagebuild/qdp/output/run-actions.yml b/rtemsspec/tests/spec-packagebuild/qdp/output/run-actions.yml
new file mode 100644
index 00000000..b5dd5ddb
--- /dev/null
+++ b/rtemsspec/tests/spec-packagebuild/qdp/output/run-actions.yml
@@ -0,0 +1,13 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2023 embedded brains GmbH & Co. KG
+copyrights-by-license: {}
+directory: ${../variant:/prefix-directory}
+directory-state-type: generic
+enabled-by: true
+files: []
+hash: null
+links: []
+patterns: []
+qdp-type: directory-state
+type: qdp
diff --git a/rtemsspec/tests/spec-packagebuild/qdp/package-build.yml b/rtemsspec/tests/spec-packagebuild/qdp/package-build.yml
index 4e72be8b..1d7a18c8 100644
--- a/rtemsspec/tests/spec-packagebuild/qdp/package-build.yml
+++ b/rtemsspec/tests/spec-packagebuild/qdp/package-build.yml
@@ -9,6 +9,8 @@ links:
   uid: steps/b
 - role: build-step
   uid: steps/c
+- role: build-step
+  uid: steps/run-actions
 - role: build-step
   uid: steps/archive
 qdp-type: package-build
diff --git a/rtemsspec/tests/spec-packagebuild/qdp/steps/run-actions.yml b/rtemsspec/tests/spec-packagebuild/qdp/steps/run-actions.yml
new file mode 100644
index 00000000..505acbdd
--- /dev/null
+++ b/rtemsspec/tests/spec-packagebuild/qdp/steps/run-actions.yml
@@ -0,0 +1,282 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+actions:
+- action: mkdir
+  enabled-by: true
+  exist-ok: false
+  parents: true
+  path: ${../variant:/build-directory}/some/more/dirs
+- action: touch
+  enabled-by: true
+  exist-ok: false
+  path: ${../variant:/build-directory}/some/more/dirs/file
+- action: touch
+  enabled-by: false
+  exist-ok: false
+  path: ${../variant:/build-directory}/some/more/dirs/file
+- action: touch
+  enabled-by: true
+  exist-ok: true
+  path: ${../variant:/build-directory}/some/more/dirs/file
+- action: remove
+  enabled-by: true
+  missing-ok: false
+  path: ${../variant:/build-directory}/some/more/dirs/file
+- action: remove
+  enabled-by: true
+  missing-ok: true
+  path: ${../variant:/build-directory}/some/more/dirs/file
+- action: remove
+  enabled-by: true
+  missing-ok: false
+  path: ${../variant:/build-directory}/some/more/dirs
+- action: remove
+  enabled-by: true
+  missing-ok: true
+  path: ${../variant:/build-directory}/some/more/dirs
+- action: mkdir
+  enabled-by: true
+  exist-ok: false
+  parents: false
+  path: ${../variant:/build-directory}/some/more/dirs
+- action: touch
+  enabled-by: true
+  exist-ok: false
+  path: ${../variant:/build-directory}/some/more/dirs/file
+- action: remove-empty-directories
+  enabled-by: true
+  path: ${../variant:/build-directory}/some
+- action: remove
+  enabled-by: true
+  missing-ok: false
+  path: ${../variant:/build-directory}/some/more/dirs/file
+- action: remove-empty-directories
+  enabled-by: true
+  path: ${../variant:/build-directory}/some
+- action: mkdir
+  enabled-by: true
+  exist-ok: false
+  parents: true
+  path: ${../variant:/build-directory}/some/more/dirs
+- action: touch
+  enabled-by: true
+  exist-ok: false
+  path: ${../variant:/build-directory}/some/more/dirs/file
+- action: remove-tree
+  enabled-by: true
+  missing-ok: true
+  path: ${../variant:/build-directory}/some/more
+- action: remove-tree
+  enabled-by: true
+  missing-ok: true
+  path: ${../variant:/build-directory}/some/more
+- action: remove-tree
+  enabled-by: true
+  missing-ok: false
+  path: ${../variant:/build-directory}/some
+- action: mkdir
+  enabled-by: true
+  exist-ok: false
+  parents: true
+  path: ${../variant:/build-directory}/some/more/dirs
+- action: touch
+  enabled-by: true
+  exist-ok: false
+  path: ${../variant:/build-directory}/some/more/dirs/file
+- action: remove-glob
+  enabled-by: true
+  remove-tree: false
+  path: ${../variant:/build-directory}/some/more/dirs
+  patterns:
+  - foobar
+- action: remove-glob
+  enabled-by: true
+  remove-tree: false
+  path: ${../variant:/build-directory}/some/more/dirs
+  patterns:
+  - file
+- action: touch
+  enabled-by: true
+  exist-ok: false
+  path: ${../variant:/build-directory}/some/more/dirs/file
+- action: remove-glob
+  enabled-by: true
+  remove-tree: true
+  path: ${../variant:/build-directory}/some
+  patterns:
+  - more
+- action: remove-glob
+  enabled-by: true
+  remove-tree: false
+  path: ${../variant:/build-directory}
+  patterns:
+  - some
+- action: directory-state-clear
+  enabled-by: true
+  output-name: destination
+- action: directory-state-clear
+  enabled-by: true
+  output-name: disabled
+- action: directory-state-add-tarfile-members
+  enabled-by: true
+  extract: true
+  output-name: destination
+  prefix-path: ${../variant:/deployment-directory}
+  search-path: ${../variant:/prefix-directory}
+  pattern: archive.tar.xz
+- action: directory-state-clear
+  enabled-by: true
+  output-name: destination
+- action: mkdir
+  enabled-by: true
+  exist-ok: false
+  parents: true
+  path: ${../variant:/build-directory}/some/more/dirs
+- action: touch
+  enabled-by: true
+  exist-ok: false
+  path: ${../variant:/build-directory}/some/more/dirs/file
+- action: directory-state-add-files
+  enabled-by: true
+  output-name: destination
+  path: ${../variant:/prefix-directory}
+  pattern: archive.tar.xz
+- action: directory-state-add-tree
+  enabled-by: true
+  excludes: []
+  output-name: destination
+  prefix: null
+  root: ${../variant:/build-directory}/some/more
+- action: mkdir
+  enabled-by: true
+  exist-ok: false
+  parents: true
+  path: ${../output/run-actions:/directory}/dirs
+- action: touch
+  enabled-by: true
+  exist-ok: false
+  path: ${../output/run-actions:/directory}/dirs/file
+- action: directory-state-copy-tree
+  enabled-by: true
+  excludes: []
+  output-name: destination
+  prefix: "u"
+  root: ${../variant:/build-directory}/some/more
+- action: directory-state-move-tree
+  enabled-by: true
+  excludes: []
+  output-name: destination
+  prefix: "v"
+  root: ${../output/run-actions:/directory}/u/dirs
+- action: touch
+  enabled-by: true
+  exist-ok: false
+  path: ${../output/run-actions:/directory}/u/dirs/file
+- action: create-ini-file
+  enabled-by: true
+  output-name: destination
+  sections:
+  - enabled-by: true
+    key-value-pairs:
+    - enabled-by: true
+      key: KA
+      value: VA
+    - enabled-by: false
+      key: KB
+      value: VB
+    name: AA
+  - enabled-by: false
+    key-value-pairs: []
+    name: SB
+  target: null
+- action: create-ini-file
+  enabled-by: true
+  output-name: destination
+  sections: []
+  target: foo.ini
+- action: copy-and-substitute
+  enabled-by: true
+  input-name: source
+  output-name: destination
+  source: null
+  target: null
+- action: copy-and-substitute
+  enabled-by: true
+  input-name: source
+  output-name: destination
+  source: dir/subdir/c.txt
+  target: null
+- action: copy-and-substitute
+  enabled-by: true
+  input-name: source
+  output-name: destination
+  source: null
+  target: some/other
+- action: copy-and-substitute
+  enabled-by: true
+  input-name: source
+  output-name: destination
+  source: dir/subdir/c.txt
+  target: some/other/file.txt
+- action: subprocess
+  command:
+  - git
+  - foobar
+  enabled-by: true
+  env: []
+  expected-return-code: null
+  working-directory: ${../variant:/build-directory}
+- action: subprocess
+  command:
+  - git
+  - status
+  enabled-by: true
+  env:
+  - action: clear
+    name: PATH
+    value: null
+  - action: set
+    name: FOOBAR
+    value: foo
+  - action: path-append
+    name: FOOBAR
+    value: bar
+  - action: path-prepend
+    name: FOOBAR
+    value: ${.:/host-processor-count}
+  - action: unset
+    name: FOOBAR
+    value: null
+  expected-return-code: null
+  working-directory: ${../variant:/build-directory}
+- action: subprocess
+  command:
+  - git
+  - status
+  enabled-by: true
+  env: []
+  expected-return-code: 0
+  working-directory: ${../variant:/build-directory}
+build-step-type: run-actions
+copyrights:
+- Copyright (C) 2023 embedded brains GmbH & Co. KG
+description: Description.
+enabled-by: run-actions
+links:
+- hash: null
+  name: variant
+  role: input
+  uid: ../variant
+- hash: null
+  name: source
+  role: input
+  uid: ../source/a
+- name: destination
+  role: output
+  uid: ../output/run-actions
+- name: disabled
+  role: output
+  uid: ../output/b
+params: {}
+qdp-type: build-step
+type: qdp
+
diff --git a/rtemsspec/tests/test_packagebuild.py b/rtemsspec/tests/test_packagebuild.py
index 436c6f29..fc4ae327 100644
--- a/rtemsspec/tests/test_packagebuild.py
+++ b/rtemsspec/tests/test_packagebuild.py
@@ -101,6 +101,8 @@ def test_packagebuild(caplog, tmpdir):
     director.clear()
     variant = director["/qdp/variant"]
     prefix_dir = Path(variant["prefix-directory"])
+    status = run_command(["git", "init"], str(prefix_dir))
+    assert status == 0
 
     director.build_package(None, None)
     log = get_and_clear_log(caplog)
@@ -199,3 +201,15 @@ def test_packagebuild(caplog, tmpdir):
         "dir/subdir/c.txt\t663049a20dfea6b8da28b2eb90eddd10ccf28ef2519563310b9bde25b7268444014c48c4384ee5c5a54e7830e45fcd87df7910a7fda77b68c2efdd75f8de25e8",
         "dir/subdir/d.txt\t48fb10b15f3d44a09dc82d02b06581e0c0c69478c9fd2cf8f9093659019a1687baecdbb38c9e72b12169dc4148690f87467f9154f5931c5df665c6496cbfd5f5"
     ]
+
+    # Test RunActions
+    variant["enabled"] = ["run-actions"]
+    director.build_package(None, None)
+    log = get_and_clear_log(caplog)
+    assert f"/qdp/steps/run-actions: make directory: {tmp_dir}/pkg/build/some/more/dirs" in log
+    assert f"/qdp/steps/run-actions: remove empty directory: {tmp_dir}/pkg/build/some/more/dirs" in log
+    assert f"/qdp/steps/run-actions: remove empty directory: {tmp_dir}/pkg/build/some/more" in log
+    assert f"/qdp/steps/run-actions: remove empty directory: {tmp_dir}/pkg/build/some" in log
+    assert f"/qdp/steps/run-actions: remove directory tree: {tmp_dir}/pkg/build/some" in log
+    assert f"/qdp/steps/run-actions: run in '{tmp_dir}/pkg/build': 'git' 'foobar'" in log
+    assert f"/qdp/steps/run-actions: run in '{tmp_dir}/pkg/build': 'git' 'status'" in log
diff --git a/rtemsspec/util.py b/rtemsspec/util.py
index 2e491244..a1f46372 100644
--- a/rtemsspec/util.py
+++ b/rtemsspec/util.py
@@ -1,7 +1,7 @@
 # SPDX-License-Identifier: BSD-2-Clause
 """ This module provides utility functions. """
 
-# Copyright (C) 2020, 2021 embedded brains GmbH & Co. KG
+# Copyright (C) 2020, 2023 embedded brains GmbH & Co. KG
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions
@@ -33,9 +33,11 @@ import os
 from pathlib import Path
 import shutil
 import subprocess
-from typing import Any, List, Optional, Union
+from typing import Any, List, Optional, Set, Union
 import yaml
 
+from rtemsspec.items import ItemMapper
+
 
 def base64_to_hex(data: str) -> str:
     """ Converts the data from base64 to hex. """
@@ -79,6 +81,42 @@ def copy_files(src_dir: str, dst_dir: str, files: List[str],
         shutil.copy2(src_file, dst_file)
 
 
+def copy_and_substitute(src_file: str, dst_file: str, mapper: ItemMapper,
+                        log_context: str) -> None:
+    """
+    Copies the file from the source to the destination path and performs a
+    variable substitution on the file content using the item mapper.
+    """
+    logging.info("%s: read: %s", log_context, src_file)
+    with open(src_file, "r", encoding="utf-8") as src:
+        logging.info("%s: substitute using mapper of item %s", log_context,
+                     mapper.item.uid)
+        content = mapper.substitute(src.read())
+        logging.info("%s: write: %s", log_context, dst_file)
+        os.makedirs(os.path.dirname(dst_file), exist_ok=True)
+        with open(dst_file, "w+", encoding="utf-8") as dst:
+            dst.write(content)
+
+
+def remove_empty_directories(scope: str, base: str) -> None:
+    """
+    Recursively removes all empty subdirectories of base and base itself if it
+    gets empty.
+
+    The scope is used for log messages.
+    """
+    removed: Set[str] = set()
+    for root, subdirs, files in os.walk(base, topdown=False):
+        if files:
+            continue
+        if any(subdir for subdir in subdirs
+               if os.path.join(root, subdir) not in removed):
+            continue
+        logging.info("%s: remove empty directory: %s", scope, root)
+        os.rmdir(root)
+        removed.add(root)
+
+
 def load_config(config_filename: str) -> Any:
     """ Loads the configuration file with recursive includes. """
 
diff --git a/spec-qdp/spec/qdp-action-copy-and-substitute.yml b/spec-qdp/spec/qdp-action-copy-and-substitute.yml
new file mode 100644
index 00000000..9b1d1527
--- /dev/null
+++ b/spec-qdp/spec/qdp-action-copy-and-substitute.yml
@@ -0,0 +1,42 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2023 embedded brains GmbH & Co. KG
+enabled-by: true
+links:
+- role: spec-member
+  uid: root
+- role: spec-refinement
+  spec-key: action
+  spec-value: copy-and-substitute
+  uid: qdp-action
+spec-description: null
+spec-example: null
+spec-info:
+  dict:
+    attributes:
+      input-name:
+        description: |
+          It shall be name of the input directory state.
+        spec-type: str
+      output-name:
+        description: |
+          It shall be name of the output directory state.
+        spec-type: str
+      source:
+        description: |
+          If the value is present, then it shall be the source file relative to
+          the base directory of the input directory state, otherwise the
+          source file is the first file of the input directory state.
+        spec-type: optional-str
+      target:
+        description: |
+          If the value is present, then it shall be the target file relative to
+          the base directory of the output directory state, otherwise the
+          target file is the first file of the output directory state.
+        spec-type: optional-str
+    description: |
+      This set of attributes specifies a copy and substitute action.
+    mandatory-attributes: all
+spec-name: Copy and Substitute Action
+spec-type: qdp-action-copy-and-substitute
+type: spec
diff --git a/spec-qdp/spec/qdp-action-create-ini-file.yml b/spec-qdp/spec/qdp-action-create-ini-file.yml
new file mode 100644
index 00000000..269a22b8
--- /dev/null
+++ b/spec-qdp/spec/qdp-action-create-ini-file.yml
@@ -0,0 +1,35 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2023 embedded brains GmbH & Co. KG
+enabled-by: true
+links:
+- role: spec-member
+  uid: root
+- role: spec-refinement
+  spec-key: action
+  spec-value: create-ini-file
+  uid: qdp-action
+spec-description: null
+spec-example: null
+spec-info:
+  dict:
+    attributes:
+      output-name:
+        description: |
+          It shall be name of the output directory state.
+        spec-type: str
+      sections:
+        description: null
+        spec-type: qdp-ini-file-section-list
+      target:
+        description: |
+          If the value is present, then it shall be the target file relative to
+          the base directory of the output directory state, otherwise the
+          target file is the first file of the output directory state.
+        spec-type: optional-str
+    description: |
+      This set of attributes specifies a create INI file action.
+    mandatory-attributes: all
+spec-name: Create INI File Action
+spec-type: qdp-action-create-ini-file
+type: spec
diff --git a/spec-qdp/spec/qdp-action-directory-state-add-files.yml b/spec-qdp/spec/qdp-action-directory-state-add-files.yml
new file mode 100644
index 00000000..ef6dd05d
--- /dev/null
+++ b/spec-qdp/spec/qdp-action-directory-state-add-files.yml
@@ -0,0 +1,34 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2023 embedded brains GmbH & Co. KG
+enabled-by: true
+links:
+- role: spec-member
+  uid: root
+- role: spec-refinement
+  spec-key: action
+  spec-value: directory-state-add-files
+  uid: qdp-action
+spec-description: null
+spec-example: null
+spec-info:
+  dict:
+    attributes:
+      output-name:
+        description: |
+          It shall be name of the output directory state.
+        spec-type: str
+      pattern:
+        description: |
+          It shall be the pattern to match the files.
+        spec-type: str
+      path:
+        description: |
+          It shall be the path to start the search for files.
+        spec-type: str
+    description: |
+      This set of attributes specifies a directory state add files action.
+    mandatory-attributes: all
+spec-name: Directory State Add Files Action
+spec-type: qdp-action-directory-state-add-files
+type: spec
diff --git a/spec-qdp/spec/qdp-action-directory-state-add-tarfile-members.yml b/spec-qdp/spec/qdp-action-directory-state-add-tarfile-members.yml
new file mode 100644
index 00000000..27b8b69d
--- /dev/null
+++ b/spec-qdp/spec/qdp-action-directory-state-add-tarfile-members.yml
@@ -0,0 +1,43 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2023 embedded brains GmbH & Co. KG
+enabled-by: true
+links:
+- role: spec-member
+  uid: root
+- role: spec-refinement
+  spec-key: action
+  spec-value: directory-state-add-tarfile-members
+  uid: qdp-action
+spec-description: null
+spec-example: null
+spec-info:
+  dict:
+    attributes:
+      extract:
+        description: |
+          It shall be true, to extract the archive file.
+        spec-type: bool
+      output-name:
+        description: |
+          It shall be name of the output directory state.
+        spec-type: str
+      pattern:
+        description: |
+          It shall be the pattern to match the tarfiles.
+        spec-type: str
+      prefix-path:
+        description: |
+          It shall be prefix path for tarfile members.
+        spec-type: str
+      search-path:
+        description: |
+          It shall be the path to start the search for tarfiles.
+        spec-type: str
+    description: |
+      This set of attributes specifies a directory state add tarfile members
+      action.
+    mandatory-attributes: all
+spec-name: Directory State Add Tarfile Members Action
+spec-type: qdp-action-directory-state-add-tarfile-members
+type: spec
diff --git a/spec-qdp/spec/qdp-action-directory-state-add-tree.yml b/spec-qdp/spec/qdp-action-directory-state-add-tree.yml
new file mode 100644
index 00000000..6a237d6c
--- /dev/null
+++ b/spec-qdp/spec/qdp-action-directory-state-add-tree.yml
@@ -0,0 +1,49 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2023 embedded brains GmbH & Co. KG
+enabled-by: true
+links:
+- role: spec-member
+  uid: root
+- role: spec-refinement
+  spec-key: action
+  spec-value: directory-state-add-tree
+  uid: qdp-action
+spec-description: null
+spec-example: null
+spec-info:
+  dict:
+    attributes:
+      excludes:
+        description: |
+          It shall be a list of Python ``fnmatch`` patterns.  If a file path
+          matches with the pattern, then the file is not added to the directory
+          state.  The file paths are absolute paths relative to the base
+          directory of the directory state.  For example, if the directory
+          state has a base of ``/base`` and a file to include has the path
+          ``/base/abc``, then the file path ``/abc`` is used to match with the
+          exclude patterns.
+        spec-type: list-str
+      output-name:
+        description: |
+          It shall be the name of the output directory state.
+        spec-type: str
+      root:
+        description: |
+          It shall be the root directory of the directory tree to add.
+        spec-type: str
+      prefix:
+        description: |
+          If the value is present, then it shall be the prefix path.
+        spec-type: optional-str
+    description: |
+      This set of attributes specifies a directory state add tree action.  The
+      files of the directory tree starting at the root directory are added to
+      the file set of the directory state.  The added file path is relative to
+      the root directory.  The prefix is prepended to the file path for each
+      file before it is added to the directory state.  The files are not copied
+      or moved.
+    mandatory-attributes: all
+spec-name: Directory State Add Tree Action
+spec-type: qdp-action-directory-state-add-tree
+type: spec
diff --git a/spec-qdp/spec/qdp-action-directory-state-clear.yml b/spec-qdp/spec/qdp-action-directory-state-clear.yml
new file mode 100644
index 00000000..289e10cc
--- /dev/null
+++ b/spec-qdp/spec/qdp-action-directory-state-clear.yml
@@ -0,0 +1,26 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2022 embedded brains GmbH & Co. KG
+enabled-by: true
+links:
+- role: spec-member
+  uid: root
+- role: spec-refinement
+  spec-key: action
+  spec-value: directory-state-clear
+  uid: qdp-action
+spec-description: null
+spec-example: null
+spec-info:
+  dict:
+    attributes:
+      output-name:
+        description: |
+          It shall be name of the output directory state.
+        spec-type: str
+    description: |
+      This set of attributes specifies a directory state clear action.
+    mandatory-attributes: all
+spec-name: Directory State Clear Action
+spec-type: qdp-action-directory-state-clear
+type: spec
diff --git a/spec-qdp/spec/qdp-action-directory-state-copy-tree.yml b/spec-qdp/spec/qdp-action-directory-state-copy-tree.yml
new file mode 100644
index 00000000..b2876579
--- /dev/null
+++ b/spec-qdp/spec/qdp-action-directory-state-copy-tree.yml
@@ -0,0 +1,49 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2023 embedded brains GmbH & Co. KG
+enabled-by: true
+links:
+- role: spec-member
+  uid: root
+- role: spec-refinement
+  spec-key: action
+  spec-value: directory-state-copy-tree
+  uid: qdp-action
+spec-description: null
+spec-example: null
+spec-info:
+  dict:
+    attributes:
+      excludes:
+        description: |
+          It shall be a list of Python ``fnmatch`` patterns.  If a file path
+          matches with the pattern, then the file is not added to the directory
+          state.  The file paths are absolute paths relative to the base
+          directory of the directory state.  For example, if the directory
+          state has a base of ``/base`` and a file to include has the path
+          ``/base/abc``, then the file path ``/abc`` is used to match with the
+          exclude patterns.
+        spec-type: list-str
+      output-name:
+        description: |
+          It shall be the name of the output directory state.
+        spec-type: str
+      root:
+        description: |
+          It shall be the root directory of the directory tree to add and copy.
+        spec-type: str
+      prefix:
+        description: |
+          If the value is present, then it shall be the prefix path.
+        spec-type: optional-str
+    description: |
+      This set of attributes specifies a directory state add and copy tree
+      action.  The files of the directory tree starting at the root directory
+      are added to the file set of the directory state.  The added file path is
+      relative to the root directory.  The prefix is prepended to the file path
+      for each file before it is added to the directory state.  The files are
+      copied.
+    mandatory-attributes: all
+spec-name: Directory State Add and Copy Tree Action
+spec-type: qdp-action-directory-state-copy-tree
+type: spec
diff --git a/spec-qdp/spec/qdp-action-directory-state-move-tree.yml b/spec-qdp/spec/qdp-action-directory-state-move-tree.yml
new file mode 100644
index 00000000..250b6a78
--- /dev/null
+++ b/spec-qdp/spec/qdp-action-directory-state-move-tree.yml
@@ -0,0 +1,49 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2023 embedded brains GmbH & Co. KG
+enabled-by: true
+links:
+- role: spec-member
+  uid: root
+- role: spec-refinement
+  spec-key: action
+  spec-value: directory-state-move-tree
+  uid: qdp-action
+spec-description: null
+spec-example: null
+spec-info:
+  dict:
+    attributes:
+      excludes:
+        description: |
+          It shall be a list of Python ``fnmatch`` patterns.  If a file path
+          matches with the pattern, then the file is not added to the directory
+          state.  The file paths are absolute paths relative to the base
+          directory of the directory state.  For example, if the directory
+          state has a base of ``/base`` and a file to include has the path
+          ``/base/abc``, then the file path ``/abc`` is used to match with the
+          exclude patterns.
+        spec-type: list-str
+      output-name:
+        description: |
+          It shall be the name of the output directory state.
+        spec-type: str
+      root:
+        description: |
+          It shall be the root directory of the directory tree to add and move.
+        spec-type: str
+      prefix:
+        description: |
+          If the value is present, then it shall be the prefix path.
+        spec-type: optional-str
+    description: |
+      This set of attributes specifies a directory state add and move tree
+      action.  The files of the directory tree starting at the root directory
+      are added to the file set of the directory state.  The added file path is
+      relative to the root directory.  The prefix is prepended to the file path
+      for each file before it is added to the directory state.  The files are
+      moved.
+    mandatory-attributes: all
+spec-name: Directory State Add and Move Tree Action
+spec-type: qdp-action-directory-state-move-tree
+type: spec
diff --git a/spec-qdp/spec/qdp-action-list.yml b/spec-qdp/spec/qdp-action-list.yml
new file mode 100644
index 00000000..95398756
--- /dev/null
+++ b/spec-qdp/spec/qdp-action-list.yml
@@ -0,0 +1,16 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2022 embedded brains GmbH & Co. KG
+enabled-by: true
+links:
+- role: spec-member
+  uid: root
+spec-description: null
+spec-example: null
+spec-info:
+  list:
+    description: null
+    spec-type: qdp-action
+spec-name: Action List
+spec-type: qdp-action-list
+type: spec
diff --git a/spec-qdp/spec/qdp-action-mkdir.yml b/spec-qdp/spec/qdp-action-mkdir.yml
new file mode 100644
index 00000000..624b9985
--- /dev/null
+++ b/spec-qdp/spec/qdp-action-mkdir.yml
@@ -0,0 +1,35 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2022 embedded brains GmbH & Co. KG
+enabled-by: true
+links:
+- role: spec-member
+  uid: root
+- role: spec-refinement
+  spec-key: action
+  spec-value: mkdir
+  uid: qdp-action
+spec-description: null
+spec-example: null
+spec-info:
+  dict:
+    attributes:
+      exist-ok:
+        description: |
+          It shall be true, if the directory may already exist, otherwise false.
+        spec-type: bool
+      parents:
+        description: |
+          It shall be true, if missing parent directories shall be created,
+          otherwise false.
+        spec-type: bool
+      path:
+        description: |
+          It shall be the path to the directory to make.
+        spec-type: str
+    description: |
+      This set of attributes specifies a make directory action.
+    mandatory-attributes: all
+spec-name: Make Directory Action
+spec-type: qdp-action-mkdir
+type: spec
diff --git a/spec-qdp/spec/qdp-action-remove-empty-directories.yml b/spec-qdp/spec/qdp-action-remove-empty-directories.yml
new file mode 100644
index 00000000..dd6afa6f
--- /dev/null
+++ b/spec-qdp/spec/qdp-action-remove-empty-directories.yml
@@ -0,0 +1,28 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2022 embedded brains GmbH & Co. KG
+enabled-by: true
+links:
+- role: spec-member
+  uid: root
+- role: spec-refinement
+  spec-key: action
+  spec-value: remove-empty-directories
+  uid: qdp-action
+spec-description: null
+spec-example: null
+spec-info:
+  dict:
+    attributes:
+      path:
+        description: |
+          It shall be the path to the base directory of the removal action.
+        spec-type: str
+    description: |
+      This set of attributes specifies a removal of empty directories action.
+      The action recursively removes all empty subdirectories of the base
+      directory and the base directory itself if it gets empty.
+    mandatory-attributes: all
+spec-name: Remove Empty Directories Action
+spec-type: qdp-action-remove-empty-directories
+type: spec
diff --git a/spec-qdp/spec/qdp-action-remove-glob.yml b/spec-qdp/spec/qdp-action-remove-glob.yml
new file mode 100644
index 00000000..b1ba517c
--- /dev/null
+++ b/spec-qdp/spec/qdp-action-remove-glob.yml
@@ -0,0 +1,38 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2023 embedded brains GmbH & Co. KG
+enabled-by: true
+links:
+- role: spec-member
+  uid: root
+- role: spec-refinement
+  spec-key: action
+  spec-value: remove-glob
+  uid: qdp-action
+spec-description: null
+spec-example: null
+spec-info:
+  dict:
+    attributes:
+      path:
+        description: |
+          It shall be the path to start the search for files and directories to
+          remove.
+        spec-type: str
+      patterns:
+        description: |
+          It shall be the list of patterns to match the files and directories
+          to remove.
+        spec-type: list-str
+      remove-tree:
+        description: |
+          It shall be true, if matching directories shall be removed as a
+          directory tree, otherwise false.
+        spec-type: bool
+    description: |
+      This set of attributes specifies a pattern based removal of files and
+      directories action.
+    mandatory-attributes: all
+spec-name: Remove Matching Files and Directories Action
+spec-type: qdp-action-remove-glob
+type: spec
diff --git a/spec-qdp/spec/qdp-action-remove-tree.yml b/spec-qdp/spec/qdp-action-remove-tree.yml
new file mode 100644
index 00000000..4a659e8a
--- /dev/null
+++ b/spec-qdp/spec/qdp-action-remove-tree.yml
@@ -0,0 +1,30 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2022 embedded brains GmbH & Co. KG
+enabled-by: true
+links:
+- role: spec-member
+  uid: root
+- role: spec-refinement
+  spec-key: action
+  spec-value: remove-tree
+  uid: qdp-action
+spec-description: null
+spec-example: null
+spec-info:
+  dict:
+    attributes:
+      missing-ok:
+        description: |
+          It shall be true, if the directory may be missing, otherwise false.
+        spec-type: bool
+      path:
+        description: |
+          It shall be the path to the directory tree to remove.
+        spec-type: str
+    description: |
+      This set of attributes specifies a removal of a directory tree action.
+    mandatory-attributes: all
+spec-name: Remove Directory Tree Action
+spec-type: qdp-action-remove-tree
+type: spec
diff --git a/spec-qdp/spec/qdp-action-remove.yml b/spec-qdp/spec/qdp-action-remove.yml
new file mode 100644
index 00000000..6722af9f
--- /dev/null
+++ b/spec-qdp/spec/qdp-action-remove.yml
@@ -0,0 +1,31 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2023 embedded brains GmbH & Co. KG
+enabled-by: true
+links:
+- role: spec-member
+  uid: root
+- role: spec-refinement
+  spec-key: action
+  spec-value: remove
+  uid: qdp-action
+spec-description: null
+spec-example: null
+spec-info:
+  dict:
+    attributes:
+      missing-ok:
+        description: |
+          It shall be true, if the file or directory may be missing, otherwise
+          false.
+        spec-type: bool
+      path:
+        description: |
+          It shall be the path to the file or directory to remove.
+        spec-type: str
+    description: |
+      This set of attributes specifies a file or directory remove action.
+    mandatory-attributes: all
+spec-name: Remove File or Directory Action
+spec-type: qdp-action-remove
+type: spec
diff --git a/spec-qdp/spec/qdp-action-subprocess-env-list.yml b/spec-qdp/spec/qdp-action-subprocess-env-list.yml
new file mode 100644
index 00000000..cfb3370e
--- /dev/null
+++ b/spec-qdp/spec/qdp-action-subprocess-env-list.yml
@@ -0,0 +1,16 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2022 embedded brains GmbH & Co. KG
+enabled-by: true
+links:
+- role: spec-member
+  uid: root
+spec-description: null
+spec-example: null
+spec-info:
+  list:
+    description: null
+    spec-type: qdp-action-subprocess-env
+spec-name: Action Subprocess Environment List
+spec-type: qdp-action-subprocess-env-list
+type: spec
diff --git a/spec-qdp/spec/qdp-action-subprocess-env.yml b/spec-qdp/spec/qdp-action-subprocess-env.yml
new file mode 100644
index 00000000..1e24c20c
--- /dev/null
+++ b/spec-qdp/spec/qdp-action-subprocess-env.yml
@@ -0,0 +1,33 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2022 embedded brains GmbH & Co. KG
+enabled-by: true
+links:
+- role: spec-member
+  uid: root
+spec-description: null
+spec-example: null
+spec-info:
+  dict:
+    attributes:
+      action:
+        description: |
+          It shall be the action execution environment modification action.
+        spec-type: str
+      name:
+        description: |
+          If the value is present, then it shall be the environment variable
+          name.
+        spec-type: optional-str
+      value:
+        description: |
+          If the value is present, then it shall be the environment variable
+          value.
+        spec-type: optional-str
+    description: |
+      This set of attributes specifies an action to alter the action execution
+      environment.
+    mandatory-attributes: all
+spec-name: Subprocess Action Environment Action
+spec-type: qdp-action-subprocess-env
+type: spec
diff --git a/spec-qdp/spec/qdp-action-subprocess.yml b/spec-qdp/spec/qdp-action-subprocess.yml
new file mode 100644
index 00000000..3b19a570
--- /dev/null
+++ b/spec-qdp/spec/qdp-action-subprocess.yml
@@ -0,0 +1,42 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2022 embedded brains GmbH & Co. KG
+enabled-by: true
+links:
+- role: spec-member
+  uid: root
+- role: spec-refinement
+  spec-key: action
+  spec-value: subprocess
+  uid: qdp-action
+spec-description: null
+spec-example: null
+spec-info:
+  dict:
+    attributes:
+      command:
+        description: |
+          It shall be the command and argument list to run as a subprocess.  A
+          variable substitution is performed on the list elements.  For
+          example, you can use
+          ``$${/variant:/build-directory}/some/path/to/build/file`` or
+          ``$${/variant:/deployment-directory}/some/path/to/deployment/file``.
+        spec-type: list-str
+      env:
+        description: null
+        spec-type: qdp-action-subprocess-env-list
+      expected-return-code:
+        description: |
+          If the value is present, then it shall be the expected return code of
+          the command.
+        spec-type: optional-int
+      working-directory:
+        description: |
+          It shall be the working directory to run the command.
+        spec-type: str
+    description: |
+      This set of attributes specifies a subprocess action.
+    mandatory-attributes: all
+spec-name: Subprocess Action
+spec-type: qdp-action-subprocess
+type: spec
diff --git a/spec-qdp/spec/qdp-action-touch.yml b/spec-qdp/spec/qdp-action-touch.yml
new file mode 100644
index 00000000..f6aa8fdf
--- /dev/null
+++ b/spec-qdp/spec/qdp-action-touch.yml
@@ -0,0 +1,30 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2023 embedded brains GmbH & Co. KG
+enabled-by: true
+links:
+- role: spec-member
+  uid: root
+- role: spec-refinement
+  spec-key: action
+  spec-value: touch
+  uid: qdp-action
+spec-description: null
+spec-example: null
+spec-info:
+  dict:
+    attributes:
+      exist-ok:
+        description: |
+          It shall be true, if the file may already exist, otherwise false.
+        spec-type: bool
+      path:
+        description: |
+          It shall be the path to the file to touch.
+        spec-type: str
+    description: |
+      This set of attributes specifies a touch file action.
+    mandatory-attributes: all
+spec-name: Touch File Action
+spec-type: qdp-action-touch
+type: spec
diff --git a/spec-qdp/spec/qdp-action.yml b/spec-qdp/spec/qdp-action.yml
new file mode 100644
index 00000000..52e5344a
--- /dev/null
+++ b/spec-qdp/spec/qdp-action.yml
@@ -0,0 +1,26 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2022 embedded brains GmbH & Co. KG
+enabled-by: true
+links:
+- role: spec-member
+  uid: root
+spec-description: null
+spec-example: null
+spec-info:
+  dict:
+    attributes:
+      action:
+        description: |
+          It shall be the action type.
+        spec-type: name
+      enabled-by:
+        description: |
+          It shall define the conditions under which the action is enabled.
+        spec-type: enabled-by
+    description: |
+      This set of attributes specifies a action.
+    mandatory-attributes: all
+spec-name: Action
+spec-type: qdp-action
+type: spec
diff --git a/spec-qdp/spec/qdp-ini-file-key-value-pair-list.yml b/spec-qdp/spec/qdp-ini-file-key-value-pair-list.yml
new file mode 100644
index 00000000..34891e96
--- /dev/null
+++ b/spec-qdp/spec/qdp-ini-file-key-value-pair-list.yml
@@ -0,0 +1,16 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2023 embedded brains GmbH & Co. KG
+enabled-by: true
+links:
+- role: spec-member
+  uid: root
+spec-description: null
+spec-example: null
+spec-info:
+  list:
+    description: null
+    spec-type: qdp-ini-file-key-value-pair
+spec-name: INI File Key Value Pair List
+spec-type: qdp-ini-file-key-value-pair-list
+type: spec
diff --git a/spec-qdp/spec/qdp-ini-file-key-value-pair.yml b/spec-qdp/spec/qdp-ini-file-key-value-pair.yml
new file mode 100644
index 00000000..8cf815a6
--- /dev/null
+++ b/spec-qdp/spec/qdp-ini-file-key-value-pair.yml
@@ -0,0 +1,31 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2023 embedded brains GmbH & Co. KG
+enabled-by: true
+links:
+- role: spec-member
+  uid: root
+spec-description: null
+spec-example: null
+spec-info:
+  dict:
+    attributes:
+      enabled-by:
+        description: |
+          It shall define the conditions under which the key-value pair is
+          enabled.
+        spec-type: enabled-by
+      key:
+        description: |
+          It shall be the name of the key.
+        spec-type: str
+      value:
+        description: |
+          It shall be the value associated with the key.
+        spec-type: str
+    description: |
+      This set of attributes specifies an INI file key-value pair.
+    mandatory-attributes: all
+spec-name: INI File Key-Value Pair
+spec-type: qdp-ini-file-key-value-pair
+type: spec
diff --git a/spec-qdp/spec/qdp-ini-file-section-list.yml b/spec-qdp/spec/qdp-ini-file-section-list.yml
new file mode 100644
index 00000000..422a6df2
--- /dev/null
+++ b/spec-qdp/spec/qdp-ini-file-section-list.yml
@@ -0,0 +1,16 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2023 embedded brains GmbH & Co. KG
+enabled-by: true
+links:
+- role: spec-member
+  uid: root
+spec-description: null
+spec-example: null
+spec-info:
+  list:
+    description: null
+    spec-type: qdp-ini-file-section
+spec-name: INI File Section List
+spec-type: qdp-ini-file-section-list
+type: spec
diff --git a/spec-qdp/spec/qdp-ini-file-section.yml b/spec-qdp/spec/qdp-ini-file-section.yml
new file mode 100644
index 00000000..0772b6da
--- /dev/null
+++ b/spec-qdp/spec/qdp-ini-file-section.yml
@@ -0,0 +1,29 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2023 embedded brains GmbH & Co. KG
+enabled-by: true
+links:
+- role: spec-member
+  uid: root
+spec-description: null
+spec-example: null
+spec-info:
+  dict:
+    attributes:
+      enabled-by:
+        description: |
+          It shall define the conditions under which the section is enabled.
+        spec-type: enabled-by
+      key-value-pairs:
+        description: null
+        spec-type: qdp-ini-file-key-value-pair-list
+      name:
+        description: |
+          It shall be the name of the section.
+        spec-type: str
+    description: |
+      This set of attributes specifies an INI file section.
+    mandatory-attributes: all
+spec-name: INI File Section
+spec-type: qdp-ini-file-section
+type: spec
diff --git a/spec-qdp/spec/qdp-run-actions.yml b/spec-qdp/spec/qdp-run-actions.yml
new file mode 100644
index 00000000..f72ae596
--- /dev/null
+++ b/spec-qdp/spec/qdp-run-actions.yml
@@ -0,0 +1,31 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2022 embedded brains GmbH & Co. KG
+enabled-by: true
+links:
+- role: spec-member
+  uid: root
+- role: spec-refinement
+  spec-key: build-step-type
+  spec-value: run-actions
+  uid: qdp-build-step
+spec-description: null
+spec-example: null
+spec-info:
+  dict:
+    attributes:
+      actions:
+        description: |
+          It shall be the list of actions to run as a subprocess.
+        spec-type: qdp-action-list
+      params:
+        description: |
+          It shall be an optional set of parameters which may be used for
+          variable subsitution.
+        spec-type: any
+    description: |
+      This set of attributes specifies actions to run as a subprocess.
+    mandatory-attributes: all
+spec-name: Run Actions Item Type
+spec-type: qdp-run-actions
+type: spec



More information about the vc mailing list