summaryrefslogtreecommitdiff
path: root/rtemsspec
diff options
context:
space:
mode:
authorSebastian Huber <sebastian.huber@embedded-brains.de>2023-11-21 11:13:16 +0100
committerSebastian Huber <sebastian.huber@embedded-brains.de>2023-11-21 11:15:25 +0100
commitcd6cbe8792f038fc7a27d37c0804afa0a244eca9 (patch)
tree57fc96cefdf5b814db447ede3f9b8972c50b3d67 /rtemsspec
parentecb305c6fdf43944a9082870474d95041df7dcf0 (diff)
runactions: New
Diffstat (limited to 'rtemsspec')
-rw-r--r--rtemsspec/packagebuildfactory.py2
-rw-r--r--rtemsspec/runactions.py314
-rw-r--r--rtemsspec/tests/spec-packagebuild/qdp/output/run-actions.yml13
-rw-r--r--rtemsspec/tests/spec-packagebuild/qdp/package-build.yml2
-rw-r--r--rtemsspec/tests/spec-packagebuild/qdp/steps/run-actions.yml282
-rw-r--r--rtemsspec/tests/test_packagebuild.py14
-rw-r--r--rtemsspec/util.py42
7 files changed, 667 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
@@ -10,6 +10,8 @@ links:
- role: build-step
uid: steps/c
- role: build-step
+ uid: steps/run-actions
+- role: build-step
uid: steps/archive
qdp-type: package-build
type: qdp
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. """