clickable/commands/docker/theme_support.py
class ThemeSupport(DockerSupport):
    config = None
    package_name = ''

    def __init__(self, config):
        self.config = config

    def update(self, docker_config: DockerConfig):
        package_name = self.config.install_files.find_package_name()

        config_path = makedirs(os.path.join(Constants.desktop_device_home, '.config/ubuntu-ui-toolkit'))

        theme = 'Ubuntu.Components.Themes.Ambiance'
        if self.config.dark_mode:
            theme = 'Ubuntu.Components.Themes.SuruDark'

        with open(os.path.join(config_path, 'theme.ini'), 'w') as f:
            f.write('[General]\ntheme=' + theme)
clickable/commands/docker/docker_config.py
class DockerConfig(object):
    docker_executable = 'docker'
    volumes = {}
    environment = {}
    extra_options = {}
    extra_flags = []

    execute = None
    pseudo_tty = False

    working_directory = ''
    docker_image = ''

    uid = ''

    use_nvidia = False

    def add_volume_mappings(self, volume_mapping_dict):
        self.volumes.update(volume_mapping_dict)

    def add_environment_variables(self, environment_variables_dict):
        self.environment.update(environment_variables_dict)

    def add_extra_flags(self, extra_flags_list):
        self.extra_flags += extra_flags_list

    def add_extra_options(self, extra_options_dict):
        self.extra_options.update(extra_options_dict)

    def render_command(self):
        volume_mapping_string = self.render_volume_mapping_string()
        environment_string = self.render_environment_string()
        extra_options_string = self.render_extra_options_string()
        extra_flags_string = self.render_extra_flags_string()

        return self.render_command_string(
            volume_mapping_string,
            environment_string,
            extra_options_string,
            extra_flags_string,
        )

    def render_volume_mapping_string(self):
        # e.g. "-v /host/path:/container/path:Z -v /other/path:/other/path:Z
        return self.render_dictionary_as_mapping(self.volumes, '-v ', ':Z')

    def render_dictionary_as_mapping(self, dict, prefix='', suffix=''):
        return ' '.join(self.join_dictionary_items(dict, ':', prefix, suffix))

    def render_environment_string(self):
        # e.g. "-e VAR=value -e X=y
        return self.render_dictionary_as_variables(self.environment, '-e ')

    def render_dictionary_as_variables(self, dict, prefix=''):
        return ' '.join(self.join_dictionary_items(dict, '=', prefix))

    def join_dictionary_items(self, dict, glue=':', prefix='', suffix=''):
        return map(lambda key_value_list: '{prefix}{name}{glue}{value}{suffix}'.format(
            prefix=prefix,
            name=key_value_list[0],
            glue=glue,
            value=shlex.quote(key_value_list[1]),
            suffix=suffix
        ), dict.items())

    def render_extra_options_string(self):
        # {'--my-option': 'value', '--my-other-option': 'test'}
        # --my-option=value --my-other-option=test
        return self.render_dictionary_as_variables(self.extra_options)

    def render_extra_flags_string(self):
        # ['--my-flag', '--my-other-flag']
        # --my-flag --my-other-flag
        return ' '.join(self.extra_flags)

    def render_command_string(self, volumes_string, environment_string,
            extra_options_string, extra_flags_string):
        return (
            '{docker} run --privileged --net=host {volumes} {env} {extra_options} '
            '{extra_flags} -w {working_dir} --user={uid} '
            '--rm {tty} -i {docker_image} bash -c "{executable}"'
        ).format(
            docker=self.docker_executable,
            volumes=volumes_string,
            env=environment_string,
            extra_options=extra_options_string,
            extra_flags=extra_flags_string,
            working_dir=self.working_directory,
            uid=self.uid,
            docker_image=self.docker_image,
            executable=self.execute,
            tty="--tty" if self.pseudo_tty else ""
        )
clickable/config/constants.py
class Constants(object):
    PURE_QML_QMAKE = 'pure-qml-qmake'
    QMAKE = 'qmake'
    PURE_QML_CMAKE = 'pure-qml-cmake'
    CMAKE = 'cmake'
    CUSTOM = 'custom'
    CORDOVA = 'cordova'
    PURE = 'pure'
    PYTHON = 'python'
    GO = 'go'
    RUST = 'rust'
    PRECOMPILED = 'precompiled'

    builders = [PURE_QML_QMAKE, QMAKE, PURE_QML_CMAKE, CMAKE, CUSTOM, CORDOVA, PURE, PYTHON, GO, RUST, PRECOMPILED]
    arch_agnostic_builders = [PURE_QML_QMAKE, PURE_QML_CMAKE, PURE]

    container_mapping = {
        "armhf": {
            ('16.04.4', 'armhf'): 'clickable/armhf-16.04-armhf:16.04.4-qt5.9',
            ('16.04.5', 'armhf'): 'clickable/armhf-16.04-armhf:16.04.5',
        },
        "arm64": {
            ('16.04.4', 'arm64'): 'clickable/arm64-16.04-arm64:16.04.4-qt5.9',
            ('16.04.5', 'arm64'): 'clickable/arm64-16.04-arm64:16.04.5',
        },
        "amd64": {
            ('16.04.4', 'armhf'): 'clickable/amd64-16.04-armhf:16.04.4-qt5.9',
            ('16.04.4', 'arm64'): 'clickable/amd64-16.04-arm64:16.04.4-qt5.9',
            ('16.04.4', 'amd64'): 'clickable/amd64-16.04-amd64:16.04.4-qt5.9',
            ('16.04.4', 'amd64-nvidia'): 'clickable/amd64-16.04-amd64-nvidia:16.04.4-qt5.9',
            ('16.04.4', 'amd64-ide'): 'clickable/amd64-16.04-amd64-ide:16.04.4-qt5.9',
            ('16.04.4', 'amd64-nvidia-ide'): 'clickable/amd64-16.04-amd64-nvidia-ide:16.04.4-qt5.9',
            ('16.04.5', 'armhf'): 'clickable/amd64-16.04-armhf:16.04.5',
            ('16.04.5', 'arm64'): 'clickable/amd64-16.04-arm64:16.04.5',
            ('16.04.5', 'amd64'): 'clickable/amd64-16.04-amd64:16.04.5',
            ('16.04.5', 'amd64-nvidia'): 'clickable/amd64-16.04-amd64-nvidia:16.04.5',
            ('16.04.5', 'amd64-ide'): 'clickable/amd64-16.04-amd64-ide:16.04.5',
            ('16.04.5', 'amd64-nvidia-ide'): 'clickable/amd64-16.04-amd64-nvidia-ide:16.04.5',
        }
    }

    framework_image_mapping = {
        "ubuntu-sdk-16.04": "16.04.4",
        "ubuntu-sdk-16.04.1": "16.04.4",
        "ubuntu-sdk-16.04.2": "16.04.4",
        "ubuntu-sdk-16.04.3": "16.04.4",
        "ubuntu-sdk-16.04.4": "16.04.4",
        "ubuntu-sdk-16.04.5": "16.04.5",
    }

    framework_fallback = "16.04.5"

    default_qt_framework_mapping = {
        '5.9': 'ubuntu-sdk-16.04.4',
        '5.12': 'ubuntu-sdk-16.04.5',
    }

    default_qt = '5.12'

    arch_triplet_mapping = {
        'armhf': 'arm-linux-gnueabihf',
        'arm64': 'aarch64-linux-gnu',
        'amd64': 'x86_64-linux-gnu',
        'all': 'all'
    }

    host_arch_mapping = {
        'x86_64': 'amd64',
        'aarch64': 'arm64',
        'armv7l': 'armhf',
    }

    desktop_device_home = os.path.expanduser('~/.clickable/home')
    device_home = '/home/phablet'
clickable/commands/docker/debug_valgrind_support.py
class DebugValgrindSupport(DockerSupport):
    config = None

    def __init__(self, config: ProjectConfig):
        self.config = config

    def update(self, docker_config: DockerConfig):
        if self.config.debug_valgrind:
            docker_config.execute = 'valgrind {}'.format(docker_config.execute)
clickable/commands/docker/debug_gdb_support.py
class DebugGdbSupport(DockerSupport):
    config = None

    def __init__(self, config: ProjectConfig):
        self.config = config

    def update(self, docker_config: DockerConfig):
        if self.config.debug_gdb:
            if self.config.debug_gdb_port:
                port = self.config.debug_gdb_port
                docker_config.execute = 'gdbserver localhost:{} {}'.format(port, docker_config.execute)
                docker_config.add_extra_options({
                    '--publish': '{port}:{port}'.format(port=port)
                })
            else:
                docker_config.execute = 'gdb --args {}'.format(docker_config.execute)
                docker_config.add_extra_options({
                    '--cap-add': 'SYS_PTRACE',
                    '--security-opt seccomp': 'unconfined'
                })
clickable/logger.py
class Colors:
class ColorFormatter(logging.Formatter):
    DEBUG = '\033[94m'
    INFO = '\033[92m'
    WARNING = '\033[93m'
    ERROR = '\033[91m'
    CRITICAL = '\033[91m'
    CLEAR = '\033[0m'
    def format(self, record):
clickable/exceptions.py
class ClickableException(Exception):
    pass
clickable/commands/base.py
class Command(object):
    aliases = []
    name = None
    help = ''

    def __init__(self, config):
        self.config = config
        self.device = Device(config)

    def run(self, path_arg=None):
        raise NotImplementedError('run is not yet implemeneted')

    def preprocess(self, path_arg=None):
        pass  # Implemented in subclasses
clickable/commands/clean.py
class CleanCommand(Command):
    aliases = []
    name = 'clean'
    help = 'Clean the build directory'

    def run(self, path_arg=None):
        if os.path.exists(self.config.build_dir):
            try:
                shutil.rmtree(self.config.build_dir)
            except Exception:
                cls, value, traceback = sys.exc_info()
                if cls == OSError and 'No such file or directory' in str(value):  # TODO see if there is a proper way to do this
                    pass  # Nothing to do here, the directory didn't exist
                else:
                    logger.warning('Failed to clean the build directory: {}: {}'.format(type, value))

        if os.path.exists(self.config.install_dir):
            try:
                shutil.rmtree(self.config.install_dir)
            except Exception:
                cls, value, traceback = sys.exc_info()
                if cls == OSError and 'No such file or directory' in str(value):  # TODO see if there is a proper way to do this
                    pass  # Nothing to do here, the directory didn't exist
                else:
                    logger.warning('Failed to clean the temp directory: {}: {}'.format(type, value))
clickable/commands/docker/multimedia_support.py
class MultimediaSupport(DockerSupport):
    config = None

    def __init__(self, config: ProjectConfig):
        self.config = config

    def update(self, docker_config: DockerConfig):
        uid = os.getuid()
        user = getpass.getuser()
        
        docker_config.volumes.update({
            '/dev/shm': '/dev/shm',
            '/etc/machine-id': '/etc/machine-id',
            '/run/{}/pulse'.format(uid): '/run/user/1000/pulse',
            '/var/lib/dbus': '/var/lib/dbus',
            '/home/{}/.pulse'.format(user): '/home/phablet/.pulse',
            '/dev/snd': '/dev/snd',
        })
clickable/commands/docker/go_support.py
class GoSupport(DockerSupport):
    config = None

    def __init__(self, config: ProjectConfig):
        self.config = config

    def update(self, docker_config: DockerConfig):
        builder = self.config.builder

        if builder == Constants.GO:
            go_paths = list(map(
                lambda gopath:
                '/gopath/path{}'.format(gopath),
                self.config.gopath.split(':')
            ))

            for path in go_paths:
                docker_config.add_volume_mappings({
                    path: path
                })

            docker_config.add_environment_variables({
                'GOPATH': ':'.join(list(go_paths))
            })
clickable/commands/docker/rust_support.py
class RustSupport(DockerSupport):
    config = None

    def __init__(self, config: ProjectConfig):
        self.config = config

    def update(self, docker_config: DockerConfig):
        builder = self.config.builder

        if builder == Constants.RUST:
            cargo_home = self.config.cargo_home
            cargo_registry = os.path.join(cargo_home, 'registry')
            cargo_git = os.path.join(cargo_home, 'git')

            os.makedirs(cargo_registry, exist_ok=True)
            os.makedirs(cargo_git, exist_ok=True)

            docker_config.add_volume_mappings({
                cargo_registry: '/opt/rust/cargo/registry',
                cargo_git: '/opt/rust/cargo/git'
            })
clickable/commands/build.py
class BuildCommand(Command):
    aliases = []
    name = 'build'
    help = 'Compile the app'

    def run(self, path_arg=None):
        try:
            os.makedirs(self.config.build_dir, exist_ok=True)
        except Exception:
            logger.warning('Failed to create the build directory: {}'.format(str(sys.exc_info()[0])))

        try:
            os.makedirs(self.config.build_home, exist_ok=True)
        except Exception:
            logger.warning('Failed to create the build home directory: {}'.format(str(sys.exc_info()[0])))

        self.config.container.setup()

        if self.config.prebuild:
            run_subprocess_check_call(self.config.prebuild, cwd=self.config.cwd, shell=True)

        self.build()

        self.install_additional_files()

        if self.config.postbuild:
            run_subprocess_check_call(self.config.postbuild, cwd=self.config.build_dir, shell=True)

        self.click_build()

        if not self.config.skip_review:
            review = ReviewCommand(self.config)
            review.check(self.click_path, raise_on_error=False)

    def build(self):
        builder_classes = get_builders()
        builder = builder_classes[self.config.builder](self.config, self.device)
        builder.build()

    def install_files(self, pattern, dest_dir):
        if not is_sub_dir(dest_dir, self.config.install_dir):
            dest_dir = os.path.abspath(self.config.install_dir + "/" + dest_dir)

        makedirs(dest_dir)
        if '"' in pattern:
            # Make sure one cannot run random bash code through the "ls" command
            raise ClickableException("install_* patterns must not contain any '\"' quotation character.")

        command = 'ls -d "{}"'.format(pattern)
        files = self.config.container.run_command(command, get_output=True).split()

        logger.info("Installing {}".format(", ".join(files)))
        self.config.container.pull_files(files, dest_dir)

    def install_qml_files(self, pattern, dest_dir):
        if '*' in pattern:
            self.install_files(pattern, dest_dir)
        else:
            command = 'cat {}'.format(os.path.join(pattern, 'qmldir'))
            qmldir = self.config.container.run_command(command, get_output=True)
            module = None
            for line in qmldir.split('\n'):
                if line.startswith('module'):
                    module = line.split(' ')[1]

            if module:
                self.install_files(pattern, os.path.join(
                    dest_dir, *module.split('.')[:-1])
                )
            else:
                self.install_files(pattern, dest_dir)

    def install_additional_files(self):
        for p in self.config.install_lib:
            self.install_files(p, os.path.join(self.config.install_dir,
                                               self.config.app_lib_dir))
        for p in self.config.install_bin:
            self.install_files(p, os.path.join(self.config.install_dir,
                                               self.config.app_bin_dir))
        for p in self.config.install_qml:
            self.install_qml_files(p, os.path.join(self.config.install_dir,
                                               self.config.app_qml_dir))
        for p, dest in self.config.install_data.items():
            self.install_files(p, dest)

    def set_arch(self, manifest):
        arch = manifest.get('architecture', None)

        if arch == '@CLICK_ARCH@' or arch == '':
            manifest['architecture'] = self.config.arch
            return True

        if arch != self.config.arch:
            raise ClickableException('Clickable is building for architecture "{}", but "{}" is specified in the manifest. You can set the architecture field to @CLICK_ARCH@ to let Clickable set the architecture field automatically.'.format(
                self.config.arch, arch))

        return False

    def set_framework(self, manifest):
        framework = manifest.get('framework', None)

        if framework == '@CLICK_FRAMEWORK@' or framework == '':
            manifest['framework'] = self.config.framework
            return True

        if framework != self.config.framework:
            logger.warning('Framework in manifest is "{}", Clickable expected "{}".'.format(
                framework, self.config.framework))

        return False

    def manipulate_manifest(self):
        manifest = self.config.install_files.get_manifest()
        has_changed = False

        if self.set_arch(manifest):
            has_changed = True

        if self.set_framework(manifest):
            has_changed = True

        if has_changed:
            self.config.install_files.write_manifest(manifest)

    def click_build(self):
        self.manipulate_manifest()

        command = 'click build {} --no-validate'.format(self.config.install_dir)
        self.config.container.run_command(command)

        click = self.config.install_files.get_click_filename()
        self.click_path = os.path.join(self.config.build_dir, click)

        if self.config.click_output:
            output_file = os.path.join(self.config.click_output, click)

            if not os.path.exists(self.config.click_output):
                os.makedirs(self.config.click_output)

            shutil.copyfile(self.click_path, output_file)
            self.click_path = output_file

        logger.debug('Click outputted to {}'.format(self.click_path))
clickable/utils.py
def run_subprocess_check_output(cmd, shell=False, **args):
    return subprocess.check_output(prepare_command(cmd, shell), shell=shell, **args).decode()
clickable/commands/docker/nvidia_support.py
class NvidiaSupport(DockerSupport):
    def update(self, docker_config: DockerConfig):
        if docker_config.use_nvidia:
            docker_config.add_volume_mappings({
                '/dev/snd/pcmC2D0c': '/dev/snd/pcmC2D0c',
                '/dev/snd/controlC2': '/dev/snd/controlC2'
            })
            docker_config.add_extra_options({
                '--device': '/dev/snd',
            })

            if LegacyDockerVersion().is_met():
                LegacyNvidiaSupport().update(docker_config)
                return

            NvidiaSupportSinceDockerVersion1903().update(docker_config)
clickable/commands/docker/webapp_support.py
class WebappSupport(DockerSupport):
    package_name = ''

    def __init__(self, package_name):
        self.package_name = package_name

    def update(self, docker_config: DockerConfig):
        # changes docker config if Exec=webapp-container
        if self.is_executable_webapp_container(docker_config):
            docker_config.add_environment_variables({'APP_ID': self.package_name})

    def is_executable_webapp_container(self, docker_config):
        return docker_config.execute.startswith('webapp-container')
clickable/utils.py
def check_command(command):
    if not is_command(command):
        raise ClickableException('The command "{}" does not exist on this system, please install it for clickable to work properly"'.format(command))
clickable/utils.py
def makedirs(path):
    os.makedirs(path, 0o777, True)
    return path
clickable/utils.py
def is_command(command):
    error_code = run_subprocess_call(shlex.split('which {}'.format(command)), stdout=subprocess.PIPE, stderr=subprocess.PIPE)

    return error_code == 0


Question: import os
import shlex
import subprocess
from pathlib import Path
from clickable.utils import (
    check_command,
    is_command,
    makedirs,
    run_subprocess_check_output,
)
from clickable.config.constants import Constants
from clickable.logger import logger
from clickable.exceptions import ClickableException
from .base import Command
from .build import BuildCommand
from .clean import CleanCommand
from .docker.debug_gdb_support import DebugGdbSupport
from .docker.debug_valgrind_support import DebugValgrindSupport
from .docker.docker_config import DockerConfig
from .docker.go_support import GoSupport
from .docker.nvidia_support import NvidiaSupport
from .docker.rust_support import RustSupport
from .docker.webapp_support import WebappSupport
from .docker.theme_support import ThemeSupport
from .docker.multimedia_support import MultimediaSupport



class DesktopCommand(Command):
    aliases = []
    name = 'desktop'
    help = 'Run the app on your desktop'

    def __init__(self, config):
        super().__init__(config)
        self.command = None
        self.custom_mode = False
        self.ide_delegate = None

    def run(self, path_arg=None):
        self.prepare_run()
        self.run_app()

    def prepare_run(self):
        if self.config.desktop_skip_build or self.custom_mode:
            self.config.container.setup()
        else:
            if not self.config.dirty:
                CleanCommand(self.config).run()
            BuildCommand(self.config).run()

    def setup_docker(self):
        self.config.container.check_docker()
        if is_command('xhost'):
            subprocess.check_call(shlex.split('xhost +local:docker'))
        else:
            logger.warning('xhost not installed, desktop mode may fail')

        return self.setup_docker_config()

    def setup_docker_config(self):
        docker_config = DockerConfig()

        docker_config.uid = os.getuid()
        docker_config.docker_image = self.config.container.docker_image
        docker_config.use_nvidia = self.config.use_nvidia

        if self.custom_mode:
            docker_config.execute = self.command
            docker_config.working_directory = self.config.root_dir
            docker_config.pseudo_tty = True

            executable = self.config.project_files.find_any_executable()
            exec_args = self.config.project_files.find_any_exec_args()
            if executable:
                docker_config.add_environment_variables(
                    {
                        "CLICK_EXEC": executable,
                        "CLICK_EXEC_PARAMS": " ".join(exec_args),
                    })
        else:
            docker_config.pseudo_tty = self.config.debug_gdb
            docker_config.execute = self.determine_executable(
                self.find_desktop_file()
            )
            docker_config.working_directory = self.config.install_dir


Answer:             WebappSupport(self.config.install_files.find_package_name()).update(docker_config)