# Path: tasks/packaging/model.py
# class DockerImageRecipe:
#     enabled: bool
#     architecture: str
#     flavors: List[str]
#     base_distribution: str
#     base_image: str
#     platform: str
# 
# class PackageSpecification:
#     """
#     Data structure for holding information about individual distribution
#     packages.
#     """
# 
#     # Package flavor, e.g. minimal, standard, full.
#     flavor: str
# 
#     # Package version.
#     version: str
# 
#     # Distribution name, e.g. stretch, buster.
#     distribution: str
# 
#     # Architecture, e.g. amd64, arm64v8, arm32v7.
#     architecture: str
# 
#     # Computed package name, taking `flavor` into account.
#     name: str = None
# 
#     # List of Python `extra` labels, taking `flavor` into account.
#     features: str = None
# 
#     # Map architecture labels to Debian distribution package architecture name.
#     debian_architecture_map: ClassVar = {
#         "arm64v8": "arm64",
#         "arm32v7": "armhf",
#     }
# 
#     def __post_init__(self):
#         self.resolve()
#         self.validate()
# 
#     def validate(self):
#         """
#         Sanity checks. All designated specification fields must be set and not
#         be empty.
#         """
#         for field in dataclasses.fields(self):
#             value = getattr(self, field.name)
#             if value is None:
#                 raise ValueError(
#                     f'Package specification field "{field.name}" is required.'
#                 )
# 
#     def resolve(self):
#         """
#         Resolve the designated package flavor to a list of Python package
#         `extra` labels.
#         """
#         if self.flavor == "minimal":
#             self.name = "kotori-minimal"
#             self.features = "daq"
#         elif self.flavor == "standard":
#             self.name = "kotori-standard"
#             self.features = "daq,daq_geospatial,export"
#         elif self.flavor == "full":
#             self.name = "kotori"
#             self.features = "daq,daq_geospatial,export,plotting,firmware,scientific"
#         else:
#             raise ValueError("Unknown package flavor")
# 
#     @property
#     def debian_architecture(self):
#         """
#         Resolve the architecture label to a Debian package architecture suffix.
#         """
#         return self.debian_architecture_map.get(self.architecture, self.architecture)
# 
#     def deb_name(self):
#         """
#         Compute the full name of the Debian `.deb` package.
#         """
#         return f"{self.name}_{self.version}-1~{self.distribution}_{self.debian_architecture}.deb"
# 
# Path: tasks/util.py
# def print_header(label, char="-"):
#     length = max(len(label), 42)
#     print(char * length)
#     print(label.center(length))
#     print(char * length)
# 
# def run_commands(commands):
#     if isinstance(commands, str):
#         commands = [commands]
#     for command in commands:
#         command = command.strip()
#         print(command)
#         exitcode = os.system(command)
#         if exitcode != 0:
#             raise SystemExit("Command failed")
# 
# def task(f: F) -> F:
#     f.__annotations__ = {}
#     return invoke.task(f)


import dataclasses
import os
import requests
import sh
from datetime import datetime
from pathlib import Path
from typing import List
from invoke import Context
from tasks.packaging.model import DockerImageRecipe, PackageSpecification
from tasks.util import print_header, run_commands, task
        if not self.manifest_tool_path.exists():
            self.manifest_tool_path.parent.mkdir(parents=True, exist_ok=True)
            response = requests.get(self.manifest_tool_url, allow_redirects=True)
            open(self.manifest_tool_path, "wb").write(response.content)
            self.manifest_tool_path.chmod(0o777)
        self.manifest_tool = sh.Command(self.manifest_tool_path)

    @staticmethod
    def check_manifest_tool():
        if "DOCKER_USERNAME" not in os.environ or "DOCKER_PASSWORD" not in os.environ:
            raise ValueError(
                "Unable to build without DOCKER_USERNAME and DOCKER_PASSWORD"
            )

    def run(self, version: str = ""):

        # Sanity checks.
        version = version.strip()
        if not version:
            raise ValueError("Unable to build without version")

        self.check_manifest_tool()

        for recipe in self.recipes:

            if not recipe.enabled:
                continue

            for flavor in recipe.flavors:


# Path: tasks/packaging/model.py
# class DockerImageRecipe:
#     enabled: bool
#     architecture: str
#     flavors: List[str]
#     base_distribution: str
#     base_image: str
#     platform: str
# 
# class PackageSpecification:
#     """
#     Data structure for holding information about individual distribution
#     packages.
#     """
# 
#     # Package flavor, e.g. minimal, standard, full.
#     flavor: str
# 
#     # Package version.
#     version: str
# 
#     # Distribution name, e.g. stretch, buster.
#     distribution: str
# 
#     # Architecture, e.g. amd64, arm64v8, arm32v7.
#     architecture: str
# 
#     # Computed package name, taking `flavor` into account.
#     name: str = None
# 
#     # List of Python `extra` labels, taking `flavor` into account.
#     features: str = None
# 
#     # Map architecture labels to Debian distribution package architecture name.
#     debian_architecture_map: ClassVar = {
#         "arm64v8": "arm64",
#         "arm32v7": "armhf",
#     }
# 
#     def __post_init__(self):
#         self.resolve()
#         self.validate()
# 
#     def validate(self):
#         """
#         Sanity checks. All designated specification fields must be set and not
#         be empty.
#         """
#         for field in dataclasses.fields(self):
#             value = getattr(self, field.name)
#             if value is None:
#                 raise ValueError(
#                     f'Package specification field "{field.name}" is required.'
#                 )
# 
#     def resolve(self):
#         """
#         Resolve the designated package flavor to a list of Python package
#         `extra` labels.
#         """
#         if self.flavor == "minimal":
#             self.name = "kotori-minimal"
#             self.features = "daq"
#         elif self.flavor == "standard":
#             self.name = "kotori-standard"
#             self.features = "daq,daq_geospatial,export"
#         elif self.flavor == "full":
#             self.name = "kotori"
#             self.features = "daq,daq_geospatial,export,plotting,firmware,scientific"
#         else:
#             raise ValueError("Unknown package flavor")
# 
#     @property
#     def debian_architecture(self):
#         """
#         Resolve the architecture label to a Debian package architecture suffix.
#         """
#         return self.debian_architecture_map.get(self.architecture, self.architecture)
# 
#     def deb_name(self):
#         """
#         Compute the full name of the Debian `.deb` package.
#         """
#         return f"{self.name}_{self.version}-1~{self.distribution}_{self.debian_architecture}.deb"
# 
# Path: tasks/util.py
# def print_header(label, char="-"):
#     length = max(len(label), 42)
#     print(char * length)
#     print(label.center(length))
#     print(char * length)
# 
# def run_commands(commands):
#     if isinstance(commands, str):
#         commands = [commands]
#     for command in commands:
#         command = command.strip()
#         print(command)
#         exitcode = os.system(command)
#         if exitcode != 0:
#             raise SystemExit("Command failed")
# 
# def task(f: F) -> F:
#     f.__annotations__ = {}
#     return invoke.task(f)


# Path: tasks/packaging/docker.py
import dataclasses
import os
import requests
import sh
from datetime import datetime
from pathlib import Path
from typing import List
from invoke import Context
from tasks.packaging.model import DockerImageRecipe, PackageSpecification
from tasks.util import print_header, run_commands, task

        if not self.manifest_tool_path.exists():
            self.manifest_tool_path.parent.mkdir(parents=True, exist_ok=True)
            response = requests.get(self.manifest_tool_url, allow_redirects=True)
            open(self.manifest_tool_path, "wb").write(response.content)
            self.manifest_tool_path.chmod(0o777)
        self.manifest_tool = sh.Command(self.manifest_tool_path)

    @staticmethod
    def check_manifest_tool():
        if "DOCKER_USERNAME" not in os.environ or "DOCKER_PASSWORD" not in os.environ:
            raise ValueError(
                "Unable to build without DOCKER_USERNAME and DOCKER_PASSWORD"
            )

    def run(self, version: str = ""):

        # Sanity checks.
        version = version.strip()
        if not version:
            raise ValueError("Unable to build without version")

        self.check_manifest_tool()

        for recipe in self.recipes:

            if not recipe.enabled:
                continue

            for flavor in recipe.flavors:


                spec = PackageSpecification(