diff options
author | Sebastian Huber <sebastian.huber@embedded-brains.de> | 2023-11-21 11:13:16 +0100 |
---|---|---|
committer | Sebastian Huber <sebastian.huber@embedded-brains.de> | 2023-11-21 11:15:25 +0100 |
commit | cd6cbe8792f038fc7a27d37c0804afa0a244eca9 (patch) | |
tree | 57fc96cefdf5b814db447ede3f9b8972c50b3d67 /rtemsspec | |
parent | ecb305c6fdf43944a9082870474d95041df7dcf0 (diff) |
runactions: New
Diffstat (limited to 'rtemsspec')
-rw-r--r-- | rtemsspec/packagebuildfactory.py | 2 | ||||
-rw-r--r-- | rtemsspec/runactions.py | 314 | ||||
-rw-r--r-- | rtemsspec/tests/spec-packagebuild/qdp/output/run-actions.yml | 13 | ||||
-rw-r--r-- | rtemsspec/tests/spec-packagebuild/qdp/package-build.yml | 2 | ||||
-rw-r--r-- | rtemsspec/tests/spec-packagebuild/qdp/steps/run-actions.yml | 282 | ||||
-rw-r--r-- | rtemsspec/tests/test_packagebuild.py | 14 | ||||
-rw-r--r-- | rtemsspec/util.py | 42 |
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. """ |