[rtems-central commit] archiver: New

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


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

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

archiver: New

---

 rtemsspec/archiver.py                              | 252 +++++++++++++++++++++
 rtemsspec/packagebuildfactory.py                   |   2 +
 .../spec-packagebuild/qdp/deployment/archive.yml   |  13 ++
 .../qdp/deployment/verify-package.yml              |  13 ++
 .../tests/spec-packagebuild/qdp/package-build.yml  |   2 +
 rtemsspec/tests/spec-packagebuild/qdp/source/a.yml |  19 ++
 rtemsspec/tests/spec-packagebuild/qdp/source/b.yml |  23 ++
 rtemsspec/tests/spec-packagebuild/qdp/source/e.yml |  19 ++
 .../tests/spec-packagebuild/qdp/steps/archive.yml  |  23 ++
 rtemsspec/tests/test-files/dir/a.txt               |   1 +
 rtemsspec/tests/test-files/dir/b.txt               |   1 +
 rtemsspec/tests/test-files/dir/e.txt               |   1 +
 rtemsspec/tests/test-files/dir/subdir/c.txt        |   1 +
 rtemsspec/tests/test-files/dir/subdir/d.txt        |   1 +
 rtemsspec/tests/test_packagebuild.py               |  45 +++-
 spec-qdp/spec/qdp-archive.yml                      |  58 +++++
 16 files changed, 472 insertions(+), 2 deletions(-)

diff --git a/rtemsspec/archiver.py b/rtemsspec/archiver.py
new file mode 100644
index 00000000..b6aa8f2f
--- /dev/null
+++ b/rtemsspec/archiver.py
@@ -0,0 +1,252 @@
+# SPDX-License-Identifier: BSD-2-Clause
+""" Build step to package deployed components into archive. """
+
+# Copyright (C) 2021 EDISOFT (https://www.edisoft.pt/)
+# Copyright (C) 2020, 2021 embedded brains GmbH & Co. KG
+#
+# 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 logging
+import os
+import stat
+import tarfile
+from typing import cast, Dict, List
+
+from rtemsspec.packagebuild import BuildItem
+from rtemsspec.directorystate import DirectoryState
+
+
+def _check_for_duplicates(uid: str, members: List[DirectoryState]) -> None:
+    logging.info("%s: check for duplicate files", uid)
+    for index, dir_state in enumerate(members):
+        logging.debug("%s: get files of: %s", uid, dir_state.uid)
+        files = dict(dir_state.files_and_hashes())
+        paths = set(files.keys())
+        for file_path in files:
+            assert os.path.isfile(file_path) or os.path.islink(file_path)
+        for dir_state_2 in members[index + 1:]:
+            logging.debug("%s: compare with files of: %s", uid,
+                          dir_state_2.uid)
+            files_2 = dict(dir_state_2.files_and_hashes())
+            paths_2 = set(files_2.keys())
+            duplicates = paths.intersection(paths_2)
+            if duplicates:
+                logging.info(
+                    "%s: duplicate files in directory states "
+                    "%s and %s", uid, dir_state.uid, dir_state_2.uid)
+                for file_path in duplicates:
+                    logging.info("%s: duplicate file: %s", uid, file_path)
+                    value = files[file_path]
+                    value_2 = files_2[file_path]
+                    if value == value_2:
+                        continue
+                    logging.error(
+                        "%s: inconsistent file hashes for '%s': %s != %s", uid,
+                        file_path, value, value_2)
+
+
+_SCRIPT_HEAD = """#!/usr/bin/env python3
+# SPDX-License-Identifier: BSD-2-Clause
+\"\"\" Verifies the files of the package. \"\"\"
+
+# Copyright (C) 2021, 2022 embedded brains GmbH & Co. KG
+#
+# 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 base64
+import binascii
+import argparse
+import hashlib
+import logging
+import os
+import sys
+from typing import Dict, List
+
+
+_FILES = {
+"""
+
+_SCRIPT_TAIL = """}
+
+
+def _hash_file(path: str) -> str:
+    file_hash = hashlib.sha512()
+    if os.path.islink(path):
+        file_hash.update(os.readlink(path).encode("utf-8"))
+    else:
+        buf = bytearray(65536)
+        memview = memoryview(buf)
+        with open(path, "rb", buffering=0) as src:
+            for size in iter(lambda: src.readinto(memview), 0):  # type: ignore
+                file_hash.update(memview[:size])
+    return base64.urlsafe_b64encode(file_hash.digest()).decode("ascii")
+
+
+def _hex(digest: str) -> str:
+    binary = base64.urlsafe_b64decode(digest)
+    return binascii.hexlify(binary).decode('ascii')
+
+
+def _check_file(file_path: str, expected_files: Dict[str, str]) -> int:
+    expected_hash = expected_files[file_path]
+    actual_hash = _hash_file(file_path)
+    if expected_hash != actual_hash:
+        logging.error(
+            "expected hash is %s, actual hash is %s for file: %s",
+            _hex(expected_hash), _hex(actual_hash), file_path)
+        return 1
+    return 0
+
+
+def _verify_files(script: str, expected_files: Dict[str, str]) -> int:
+    status = 0
+    script = os.path.normpath(script)
+    for path, dirs, files in os.walk("."):
+        dirs.sort()
+        for name in sorted(files):
+            file_path = os.path.normpath(os.path.join(path, name))
+            if file_path in expected_files:
+                status = _check_file(file_path, expected_files)
+                del expected_files[file_path]
+            elif file_path != script:
+                logging.warning("unexpected file: %s", file_path)
+    for maybe_missing in expected_files.keys():
+        if os.path.islink(maybe_missing):
+            status = _check_file(maybe_missing, expected_files)
+            continue
+        logging.error("missing file: %s", maybe_missing)
+        status = 1
+    return status
+
+
+def main(script: str, argv: List[str]) -> int:
+    \"\"\" Verifies the files of the package. \"\"\"
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "--log-level",
+        choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
+        type=str.upper,
+        default="WARNING",
+        help="log level")
+    parser.add_argument("--log-file",
+                        type=str,
+                        default=None,
+                        help="log to this file")
+    parser.add_argument("--list-files",
+                        action="store_true",
+                        help="list the files of the package")
+    parser.add_argument("--list-files-and-hashes",
+                        action="store_true",
+                        help="list the files of the package "
+                        "with the SHA512 digest of each file")
+    args = parser.parse_args(argv)
+    logging.basicConfig(filename=args.log_file, level=args.log_level)
+    expected_files = dict(
+        zip(map(lambda x: os.path.normpath(x), _FILES.keys()),
+            _FILES.values()))
+    status = 0
+    if args.list_files_and_hashes:
+        for file_path, hash_value in expected_files.items():
+            print(f"{file_path}\t{_hex(hash_value)}")
+    elif args.list_files:
+        for file_path in expected_files.keys():
+            print(file_path)
+    else:
+        status = _verify_files(script, expected_files)
+    return status
+
+
+
+if __name__ == "__main__":
+    status = main(sys.argv[0], sys.argv[1:])
+    sys.exit(status)
+"""
+
+
+def _create_verification_script(script: str, archive_files: Dict[str,
+                                                                 str]) -> None:
+    with open(script, "w", encoding="utf-8") as out:
+        out.write(_SCRIPT_HEAD)
+        for file_path, hash_value in sorted(archive_files.items()):
+            print(f"    \"{file_path}\": \"{hash_value}\",", file=out)
+        out.write(_SCRIPT_TAIL)
+    os.chmod(script, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
+
+
+class Archiver(BuildItem):
+    """
+    The archiver adds the file of its directory state dependencies to an
+    archive file.
+    """
+
+    def run(self) -> None:
+        archive_file = self["archive-file"]
+        archive_state = self.output("archive")
+        assert isinstance(archive_state, DirectoryState)
+        archive_state.set_files([archive_file])
+        archive_file = os.path.join(archive_state.directory, archive_file)
+        script_file = self["verification-script"]
+        script_state = self.output("verify-package")
+        assert isinstance(script_state, DirectoryState)
+        script_state.set_files([script_file])
+        script_file = os.path.join(script_state.directory, script_file)
+        script_dir = os.path.dirname(script_file)
+        logging.info("%s: create archive: %s", self.uid, archive_file)
+        os.makedirs(os.path.dirname(archive_file), exist_ok=True)
+        with tarfile.open(archive_file, "w:xz") as tar_file:
+            members = cast(List[DirectoryState], list(self.inputs("member")))
+            _check_for_duplicates(self.uid, members)
+            strip_prefix = self["archive-strip-prefix"]
+            archive_files: Dict[str, str] = {}
+            for dir_state in members:
+                logging.info("%s: add files of directory state: %s", self.uid,
+                             dir_state.uid)
+                for file_path, hash_value in dir_state.files_and_hashes():
+                    verify_path = os.path.relpath(file_path, script_dir)
+                    assert hash_value
+                    archive_files[verify_path] = hash_value
+                    tar_file.add(file_path,
+                                 os.path.relpath(file_path, strip_prefix))
+            _create_verification_script(script_file, archive_files)
+            tar_file.add(script_file, os.path.relpath(script_file,
+                                                      strip_prefix))
+        logging.info("%s: finished to create archive: %s", self.uid,
+                     archive_file)
diff --git a/rtemsspec/packagebuildfactory.py b/rtemsspec/packagebuildfactory.py
index 8cd430ab..ef4a950b 100644
--- a/rtemsspec/packagebuildfactory.py
+++ b/rtemsspec/packagebuildfactory.py
@@ -24,6 +24,7 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
+from rtemsspec.archiver import Archiver
 from rtemsspec.directorystate import DirectoryState
 from rtemsspec.packagebuild import BuildItemFactory, PackageVariant
 
@@ -31,6 +32,7 @@ from rtemsspec.packagebuild import BuildItemFactory, PackageVariant
 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/directory-state/generic", DirectoryState)
     factory.add_constructor("qdp/directory-state/repository", DirectoryState)
     factory.add_constructor("qdp/directory-state/unpacked-archive",
diff --git a/rtemsspec/tests/spec-packagebuild/qdp/deployment/archive.yml b/rtemsspec/tests/spec-packagebuild/qdp/deployment/archive.yml
new file mode 100644
index 00000000..823071e8
--- /dev/null
+++ b/rtemsspec/tests/spec-packagebuild/qdp/deployment/archive.yml
@@ -0,0 +1,13 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2020 embedded brains GmbH & Co. KG
+copyrights-by-license: {}
+directory: ${../variant:/build-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/deployment/verify-package.yml b/rtemsspec/tests/spec-packagebuild/qdp/deployment/verify-package.yml
new file mode 100644
index 00000000..a6b8c747
--- /dev/null
+++ b/rtemsspec/tests/spec-packagebuild/qdp/deployment/verify-package.yml
@@ -0,0 +1,13 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2021 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 1b451572..4e72be8b 100644
--- a/rtemsspec/tests/spec-packagebuild/qdp/package-build.yml
+++ b/rtemsspec/tests/spec-packagebuild/qdp/package-build.yml
@@ -9,5 +9,7 @@ links:
   uid: steps/b
 - role: build-step
   uid: steps/c
+- role: build-step
+  uid: steps/archive
 qdp-type: package-build
 type: qdp
diff --git a/rtemsspec/tests/spec-packagebuild/qdp/source/a.yml b/rtemsspec/tests/spec-packagebuild/qdp/source/a.yml
new file mode 100644
index 00000000..498a5546
--- /dev/null
+++ b/rtemsspec/tests/spec-packagebuild/qdp/source/a.yml
@@ -0,0 +1,19 @@
+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:
+- file: dir/a.txt
+  hash: null
+- file: dir/subdir/c.txt
+  hash: null
+- file: dir/subdir/d.txt
+  hash: null
+hash: null
+links: []
+patterns: []
+qdp-type: directory-state
+type: qdp
diff --git a/rtemsspec/tests/spec-packagebuild/qdp/source/b.yml b/rtemsspec/tests/spec-packagebuild/qdp/source/b.yml
new file mode 100644
index 00000000..f2836d4e
--- /dev/null
+++ b/rtemsspec/tests/spec-packagebuild/qdp/source/b.yml
@@ -0,0 +1,23 @@
+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:
+- file: dir/b.txt
+  hash: null
+- file: dir/subdir/c.txt
+  hash: null
+- file: dir/subdir/d.txt
+  hash: null
+hash: null
+links:
+- hash: null
+  name: member
+  role: input-to
+  uid: ../steps/archive
+patterns: []
+qdp-type: directory-state
+type: qdp
diff --git a/rtemsspec/tests/spec-packagebuild/qdp/source/e.yml b/rtemsspec/tests/spec-packagebuild/qdp/source/e.yml
new file mode 100644
index 00000000..bcae7c2a
--- /dev/null
+++ b/rtemsspec/tests/spec-packagebuild/qdp/source/e.yml
@@ -0,0 +1,19 @@
+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:
+- file: dir/e.txt
+  hash: null
+hash: null
+links:
+- hash: null
+  name: member
+  role: input-to
+  uid: ../steps/archive
+patterns: []
+qdp-type: directory-state
+type: qdp
diff --git a/rtemsspec/tests/spec-packagebuild/qdp/steps/archive.yml b/rtemsspec/tests/spec-packagebuild/qdp/steps/archive.yml
new file mode 100644
index 00000000..e7f31cf8
--- /dev/null
+++ b/rtemsspec/tests/spec-packagebuild/qdp/steps/archive.yml
@@ -0,0 +1,23 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+archive-file: archive.tar.xz
+archive-strip-prefix: ${../variant:/prefix-directory}/
+build-step-type: archive
+copyrights:
+- Copyright (C) 2023 embedded brains GmbH & Co. KG
+description: |
+  Description.
+enabled-by: archive
+links:
+- hash: null
+  name: member
+  role: input
+  uid: ../source/a
+- name: archive
+  role: output
+  uid: ../deployment/archive
+- name: verify-package
+  role: output
+  uid: ../deployment/verify-package
+qdp-type: build-step
+type: qdp
+verification-script: verify_package.py
diff --git a/rtemsspec/tests/test-files/dir/a.txt b/rtemsspec/tests/test-files/dir/a.txt
new file mode 100644
index 00000000..f70f10e4
--- /dev/null
+++ b/rtemsspec/tests/test-files/dir/a.txt
@@ -0,0 +1 @@
+A
diff --git a/rtemsspec/tests/test-files/dir/b.txt b/rtemsspec/tests/test-files/dir/b.txt
new file mode 100644
index 00000000..223b7836
--- /dev/null
+++ b/rtemsspec/tests/test-files/dir/b.txt
@@ -0,0 +1 @@
+B
diff --git a/rtemsspec/tests/test-files/dir/e.txt b/rtemsspec/tests/test-files/dir/e.txt
new file mode 100644
index 00000000..1c507261
--- /dev/null
+++ b/rtemsspec/tests/test-files/dir/e.txt
@@ -0,0 +1 @@
+E
diff --git a/rtemsspec/tests/test-files/dir/subdir/c.txt b/rtemsspec/tests/test-files/dir/subdir/c.txt
new file mode 100644
index 00000000..3cc58df8
--- /dev/null
+++ b/rtemsspec/tests/test-files/dir/subdir/c.txt
@@ -0,0 +1 @@
+C
diff --git a/rtemsspec/tests/test-files/dir/subdir/d.txt b/rtemsspec/tests/test-files/dir/subdir/d.txt
new file mode 100644
index 00000000..17848105
--- /dev/null
+++ b/rtemsspec/tests/test-files/dir/subdir/d.txt
@@ -0,0 +1 @@
+D
diff --git a/rtemsspec/tests/test_packagebuild.py b/rtemsspec/tests/test_packagebuild.py
index 48ee2c84..436c6f29 100644
--- a/rtemsspec/tests/test_packagebuild.py
+++ b/rtemsspec/tests/test_packagebuild.py
@@ -29,6 +29,7 @@ import os
 import pytest
 from pathlib import Path
 import shutil
+import tarfile
 
 from rtemsspec.items import EmptyItem, Item, ItemCache, ItemGetValueContext
 from rtemsspec.packagebuild import BuildItem, BuildItemMapper, \
@@ -36,6 +37,7 @@ from rtemsspec.packagebuild import BuildItem, BuildItemMapper, \
 from rtemsspec.packagebuildfactory import create_build_item_factory
 from rtemsspec.specverify import verify
 from rtemsspec.tests.util import get_and_clear_log
+from rtemsspec.util import run_command
 
 
 def _copy_dir(src, dst):
@@ -50,9 +52,10 @@ def _copy_dir(src, dst):
 
 
 def _create_item_cache(tmp_dir: Path, spec_dir: Path) -> ItemCache:
-    spec_dst = tmp_dir / Path("pkg/build/spec")
+    spec_dst = tmp_dir / "pkg" / "build" / "spec"
     test_dir = Path(__file__).parent
     _copy_dir(test_dir / spec_dir, spec_dst)
+    _copy_dir(test_dir / "test-files", tmp_dir)
     _copy_dir(test_dir.parent.parent / "spec-spec", spec_dst)
     _copy_dir(test_dir.parent.parent / "spec-qdp" / "spec", spec_dst / "spec")
     cache_dir = os.path.join(tmp_dir, "cache")
@@ -96,7 +99,8 @@ def test_packagebuild(caplog, tmpdir):
     factory.add_get_value("qdp/variant:/tmpdir", get_tmpdir)
     director = PackageBuildDirector(item_cache, factory)
     director.clear()
-    prefix_dir = Path(director["/qdp/variant"]["prefix-directory"])
+    variant = director["/qdp/variant"]
+    prefix_dir = Path(variant["prefix-directory"])
 
     director.build_package(None, None)
     log = get_and_clear_log(caplog)
@@ -158,3 +162,40 @@ def test_packagebuild(caplog, tmpdir):
         c.output("moo")
     assert c["values"]["list"] == ["a", "b1", "b2", ["d", "e"], "c"]
     c.clear()
+
+    # Test Archiver
+    dir_state_a = director["/qdp/source/a"]
+    dir_state_a.load()
+    with open(tmp_dir / "dir/subdir/d.txt", "w", encoding="utf-8") as dst:
+        dst.write("d")
+    dir_state_b = director["/qdp/source/b"]
+    dir_state_b.load()
+    dir_state_e = director["/qdp/source/e"]
+    dir_state_e.load()
+    variant["enabled"] = ["archive"]
+    director.build_package(None, None)
+    log = get_and_clear_log(caplog)
+    assert "/qdp/steps/archive: duplicate files in directory states /qdp/source/a and /qdp/source/b" in log
+    assert f"/qdp/steps/archive: duplicate file: {tmp_dir}/dir/subdir/d.txt" in log
+    assert f"/qdp/steps/archive: inconsistent file hashes for '{tmp_dir}/dir/subdir/d.txt': {list(dir_state_a.files_and_hashes())[2][1]} != {list(dir_state_b.files_and_hashes())[2][1]}" in log
+    assert f"/qdp/steps/archive: duplicate file: {tmp_dir}/dir/subdir/c.txt" in log
+    with tarfile.open(director["/qdp/deployment/archive"].file,
+                      "r:*") as archive:
+        assert archive.getnames() == [
+            'dir/a.txt', 'dir/subdir/c.txt', 'dir/subdir/d.txt', 'dir/b.txt',
+            'dir/subdir/c.txt', 'dir/subdir/d.txt', 'dir/e.txt',
+            'verify_package.py'
+        ]
+
+    verify_package = director["/qdp/deployment/verify-package"]
+    stdout = []
+    status = run_command([verify_package.file, "--list-files-and-hashes"],
+                         str(tmp_dir), stdout)
+    assert status == 0
+    assert stdout == [
+        "dir/a.txt\t7a296fab5364b34ce3e0476d55bf291bd41aa085e5ecf2a96883e593aa1836fed22f7242af48d54af18f55c8d1def13ec9314c926666a0ba63f7663500090565",
+        "dir/b.txt\t480a2ddd53e8db95fc737b670302c7ea0914b52ffdb2e961c2ff90887ec2b25873723374da81ae5adafc47ef7ef1c7c5c91243217d41cb904040279b758da0f7",
+        "dir/e.txt\t61e9f9edbc37b2b5c2fc9633da2d8777916f0e4515a080374acedd14c935f2c6fb5a882c5459b7a06a03f0d057ce4f73f89def713a5824b8769a5917a3bdda93",
+        "dir/subdir/c.txt\t663049a20dfea6b8da28b2eb90eddd10ccf28ef2519563310b9bde25b7268444014c48c4384ee5c5a54e7830e45fcd87df7910a7fda77b68c2efdd75f8de25e8",
+        "dir/subdir/d.txt\t48fb10b15f3d44a09dc82d02b06581e0c0c69478c9fd2cf8f9093659019a1687baecdbb38c9e72b12169dc4148690f87467f9154f5931c5df665c6496cbfd5f5"
+    ]
diff --git a/spec-qdp/spec/qdp-archive.yml b/spec-qdp/spec/qdp-archive.yml
new file mode 100644
index 00000000..e4292d10
--- /dev/null
+++ b/spec-qdp/spec/qdp-archive.yml
@@ -0,0 +1,58 @@
+SPDX-License-Identifier: CC-BY-SA-4.0 OR BSD-2-Clause
+copyrights:
+- Copyright (C) 2020, 2021 embedded brains GmbH & Co. KG
+enabled-by: true
+links:
+- role: spec-member
+  uid: root
+- role: spec-refinement
+  spec-key: build-step-type
+  spec-value: archive
+  uid: qdp-build-step
+spec-description: |
+  Items of this type shall have the following links:
+
+  * There shall be links to a ${qdp-directory-state:/spec-name} item
+    with the ${qdp-input-role:/spec-name} and the name
+    ``"member"``.  The link target directory state item defines the member
+    files of the archive.
+
+  * There shall be exactly one link to a ${qdp-directory-state:/spec-name} item
+    with the ${qdp-output-role:/spec-name} and the name
+    ``"archive"``.  The link target directory state item defines the
+    destination directory of the archive file.
+
+  * There shall be exactly one link to a ${qdp-directory-state:/spec-name} item
+    with the ${qdp-output-role:/spec-name} and the name
+    ``"verify-package"``.  The link target directory state item defines the
+    destination directory of the package verification script.
+spec-example: null
+spec-info:
+  dict:
+    attributes:
+      archive-file:
+        description: |
+          It shall be the path to the archive file relative to the base
+          directory of the ``"archive"`` directory state production.  The
+          method to compress the archive is determined by the archive file name
+          extension.
+        spec-type: str
+      archive-strip-prefix:
+        description: |
+          It shall be the prefix to strip from an archive member file path.
+        spec-type: str
+      verification-script:
+        description: |
+          It shall be the verification script file name.  The verification
+          script is automatically generated and will be included in the
+          archive.  The script can be used to verify that a file of an unpacked
+          archive has the same state as the file packed into the archive.  It
+          shows also missing and additional files inside an unpacked archive
+          base directory.
+        spec-type: str
+    description: |
+      This set of attributes specifies the package archive file and content.
+    mandatory-attributes: all
+spec-name: Archive Item Type
+spec-type: qdp-archive
+type: spec



More information about the vc mailing list