Skip to content
Snippets Groups Projects
populate-depot.py 64.3 KiB
Newer Older
# Copyright © 2019-2021 Collabora Ltd.
#
# SPDX-License-Identifier: MIT
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""
Build the steam-container-runtime (aka SteamLinuxRuntime) depot, either
from just-built files or by downloading a previous build.
"""

import argparse
import json
import logging
import os
import urllib.request
from contextlib import suppress
from typing import (
    Any,
    Dict,
    List,
    Optional,
    Sequence,
from debian.deb822 import (
    Sources,
)


HERE = os.path.dirname(os.path.abspath(__file__))


logger = logging.getLogger('populate-depot')


DEFAULT_PRESSURE_VESSEL_URI = (
    'https://repo.steampowered.com/pressure-vessel/snapshots'
)

DEFAULT_IMAGES_URI = (
    'https://repo.steampowered.com/steamrt-images-SUITE/snapshots'
)


class InvocationError(Exception):
    pass


class PressureVesselRelease:
    def __init__(
        self,
        *,
        cache: str = '.cache',
        ssh_host: str = '',
        ssh_path: str = '',
        uri: str = DEFAULT_PRESSURE_VESSEL_URI,
        version: str = ''
    ) -> None:
        self.cache = cache
        self.pinned_version = None      # type: Optional[str]
        self.ssh_host = ssh_host
        self.ssh_path = ssh_path
        self.uri = uri
        self.version = version

    def get_uri(
        self,
        filename: str,
        version: Optional[str] = None,
    ) -> str:
        uri = self.uri
        v = version or self.pinned_version or self.version or 'latest'
        return f'{uri}/{v}/{filename}'

    def get_ssh_path(
        self,
        filename: str,
        version: Optional[str] = None,
    ) -> str:
        ssh_host = self.ssh_host
        ssh_path = self.ssh_path
        v = version or self.pinned_version or self.version or 'latest'

        if not ssh_host or not ssh_path:
            raise RuntimeError('ssh host/path not configured')

        return f'{ssh_path}/{v}/{filename}'

    def fetch(
        self,
        filename: str,
        opener: urllib.request.OpenerDirector,
        version: Optional[str] = None,
    ) -> str:
        dest = os.path.join(self.cache, filename)

        if self.ssh_host and self.ssh_path:
            path = self.get_ssh_path(filename)
            logger.info('Downloading %r...', path)
            subprocess.run([
                'rsync',
                '--archive',
                '--partial',
                '--progress',
                self.ssh_host + ':' + path,
                dest,
            ], check=True)
        else:
            uri = self.get_uri(filename)
            logger.info('Downloading %r...', uri)

            with opener.open(uri) as response:
                with open(dest + '.new', 'wb') as writer:
                    shutil.copyfileobj(response, writer)

                os.rename(dest + '.new', dest)

        return dest

    def pin_version(
        self,
        opener: urllib.request.OpenerDirector,
    ) -> str:
        pinned = self.pinned_version

        if pinned is None:
            if self.ssh_host and self.ssh_path:
                path = self.get_ssh_path(filename='VERSION.txt')
                logger.info('Determining version number from %r...', path)
                pinned = subprocess.run([
                    'ssh', self.ssh_host,
                    'cat {}'.format(shlex.quote(path)),
                ], stdout=subprocess.PIPE).stdout.decode('utf-8').strip()
            else:
                uri = self.get_uri(filename='VERSION.txt')
                logger.info('Determining version number from %r...', uri)
                with opener.open(uri) as response:
                    pinned = response.read().decode('utf-8').strip()

            self.pinned_version = pinned

        return pinned


class Runtime:
    def __init__(
        self,
        name,
        *,
        suite: str,

        architecture: str = 'amd64,i386',
        cache: str = '.cache',
        images_uri: str = DEFAULT_IMAGES_URI,
        path: Optional[str] = None,
        ssh_host: str = '',
        ssh_path: str = '',
        version: str = '',
    ) -> None:
        self.architecture = architecture
        self.cache = cache
        self.images_uri = images_uri
        self.name = name
        self.path = path
        self.suite = suite
        self.ssh_host = ssh_host
        self.ssh_path = ssh_path
        self.version = version
        self.pinned_version = None      # type: Optional[str]
        self.sha256 = {}                # type: Dict[str, str]

        os.makedirs(self.cache, exist_ok=True)

        self.prefix = 'com.valvesoftware.SteamRuntime'
        self.platform = self.prefix + '.Platform'
        self.sdk = self.prefix + '.Sdk'
        self.tarball = '{}-{}-{}-runtime.tar.gz'.format(
            self.platform,
            self.architecture,
            self.suite,
        )
        self.dockerfile = '{}-{}-{}-sysroot.Dockerfile'.format(
            self.sdk,
            self.architecture,
            self.suite,
        )
        self.sdk_tarball = '{}-{}-{}-runtime.tar.gz'.format(
            self.sdk,
            self.architecture,
            self.suite,
        )
        self.debug_tarball = '{}-{}-{}-debug.tar.gz'.format(
            self.sdk,
            self.architecture,
            self.suite,
        )
        self.sysroot_tarball = '{}-{}-{}-sysroot.tar.gz'.format(
            self.sdk,
            self.architecture,
            self.suite,
        )
        self.build_id_file = '{}-{}-{}-buildid.txt'.format(
            self.platform,
            self.architecture,
            self.suite,
        )
        self.sdk_build_id_file = '{}-{}-{}-buildid.txt'.format(
            self.sdk,
            self.architecture,
            self.suite,
        )
        self.sources = '{}-{}-{}-sources.deb822.gz'.format(
            self.sdk,
            self.architecture,
            self.suite,
        )
    def get_archives(
        self,
        include_sdk_debug=False,
        include_sdk_runtime=False,
        include_sdk_sysroot=False,
    ):
            archives.append(self.debug_tarball)
            archives.append(self.dockerfile)
            archives.append(self.sysroot_tarball)

        if include_sdk_runtime:
            archives.append(self.sdk_tarball)

    def __str__(self) -> str:
        return self.name

    @classmethod
    def from_details(
        cls,
        name: str,
        details: Dict[str, Any],
        cache: str = '.cache',
        default_architecture: str = 'amd64,i386',
        default_suite: str = '',
        default_version: str = '',
        images_uri: str = DEFAULT_IMAGES_URI,
        ssh_host: str = '',
        ssh_path: str = '',
    ):
        return cls(
            name,
            architecture=details.get(
                'architecture', default_architecture,
            ),
            images_uri=images_uri,
            path=details.get('path', None),
            ssh_host=ssh_host,
            ssh_path=ssh_path,
            suite=details.get('suite', default_suite or name),
            version=details.get('version', default_version),
        )

    def get_uri(
        self,
        filename: str,
        version: Optional[str] = None,
    ) -> str:
        suite = self.suite
        uri = self.images_uri.replace('SUITE', suite)
        v = version or self.pinned_version or self.version or 'latest'
        return f'{uri}/{v}/{filename}'

    def get_ssh_path(
        self,
        filename: str,
        version: Optional[str] = None,
    ) -> str:
        ssh_host = self.ssh_host
        ssh_path = self.ssh_path.replace('SUITE', suite)
        v = version or self.pinned_version or self.version or 'latest'

        if not ssh_host or not ssh_path:
            raise RuntimeError('ssh host/path not configured')

        return f'{ssh_path}/{v}/{filename}'

    def fetch(
        self,
        filename: str,
        opener: urllib.request.OpenerDirector,
        version: Optional[str] = None,
    ) -> str:
        dest = os.path.join(self.cache, filename)

        if filename in self.sha256:
            try:
                with open(dest, 'rb') as reader:
                    hasher = hashlib.sha256()

                    while True:
                        blob = reader.read(4096)

                        if not blob:
                            break

                        hasher.update(blob)

                    digest = hasher.hexdigest()
            except OSError:
                pass
            else:
                if digest == self.sha256[filename]:
                    logger.info('Using cached %r', dest)
                    return dest

        if self.ssh_host and self.ssh_path:
            path = self.get_ssh_path(filename)
            logger.info('Downloading %r...', path)
            subprocess.run([
                'rsync',
                '--archive',
                '--partial',
                '--progress',
                self.ssh_host + ':' + path,
            ], check=True)
        else:
            uri = self.get_uri(filename)
            logger.info('Downloading %r...', uri)

            with opener.open(uri) as response:
                with open(dest + '.new', 'wb') as writer:
                    shutil.copyfileobj(response, writer)

                os.rename(dest + '.new', dest)

        return dest

    def pin_version(
        self,
        opener: urllib.request.OpenerDirector,
    ) -> str:
        pinned = self.pinned_version
        sha256 = {}     # type: Dict[str, str]
            if self.ssh_host and self.ssh_path:
                path = self.get_ssh_path(filename='VERSION.txt')
                logger.info('Determining version number from %r...', path)
                pinned = subprocess.run([
                    'cat {}'.format(shlex.quote(path)),
                ], stdout=subprocess.PIPE).stdout.decode('utf-8').strip()

                path = self.get_ssh_path(filename='SHA256SUMS')

                sha256sums = subprocess.run([
                    'ssh', self.ssh_host,
                    'cat {}'.format(shlex.quote(path)),
                ], stdout=subprocess.PIPE).stdout
                assert sha256sums is not None

            else:
                uri = self.get_uri(filename='VERSION.txt')
                logger.info('Determining version number from %r...', uri)
                with opener.open(uri) as response:
                    pinned = response.read().decode('utf-8').strip()

                uri = self.get_uri(filename='SHA256SUMS')

                with opener.open(uri) as response:
                    sha256sums = response.read()

            for line in sha256sums.splitlines():
                sha256_bytes, name_bytes = line.split(maxsplit=1)
                name = name_bytes.decode('utf-8')

                if name.startswith('*'):
                    name = name[1:]

                sha256[name] = sha256_bytes.decode('ascii')

            self.sha256 = sha256
RUN_IN_DIR_SOURCE = '''\
#!/bin/sh
# {source_for_generated_file}

set -eu

me="$(readlink -f "$0")"
here="${{me%/*}}"
me="${{me##*/}}"

pressure_vessel="${{PRESSURE_VESSEL_PREFIX:-"${{here}}/pressure-vessel"}}"

export PRESSURE_VESSEL_COPY_RUNTIME=1
export PRESSURE_VESSEL_GC_LEGACY_RUNTIMES=1
export PRESSURE_VESSEL_RUNTIME="${{dir}}"
unset PRESSURE_VESSEL_RUNTIME_ARCHIVE
export PRESSURE_VESSEL_RUNTIME_BASE="${{here}}"

if [ -z "${{PRESSURE_VESSEL_VARIABLE_DIR-}}" ]; then
    export PRESSURE_VESSEL_VARIABLE_DIR="${{here}}/var"
fi

exec "${{pressure_vessel}}/bin/pressure-vessel-unruntime" "$@"
'''

RUN_IN_ARCHIVE_SOURCE = '''\
#!/bin/sh
# {source_for_generated_file}

set -eu

me="$(readlink -f "$0")"
here="${{me%/*}}"
me="${{me##*/}}"

archive={escaped_runtime}-{escaped_arch}-{escaped_suite}-runtime.tar.gz
pressure_vessel="${{PRESSURE_VESSEL_PREFIX:-"${{here}}/pressure-vessel"}}"

export PRESSURE_VESSEL_COPY_RUNTIME=1
export PRESSURE_VESSEL_GC_LEGACY_RUNTIMES=1
unset PRESSURE_VESSEL_RUNTIME
export PRESSURE_VESSEL_RUNTIME_ARCHIVE="${{archive}}"
export PRESSURE_VESSEL_RUNTIME_BASE="${{here}}"

if [ -z "${{PRESSURE_VESSEL_VARIABLE_DIR-}}" ]; then
    export PRESSURE_VESSEL_VARIABLE_DIR="${{here}}/var"
fi

exec "${{pressure_vessel}}/bin/pressure-vessel-unruntime" "$@"
class ComponentVersion:
    def __init__(
        self,
        name: str = '',
        sort_weight: int = 0,
    ) -> None:
        self.name = name
        self.version = ''
        self.runtime = ''
        self.runtime_version = ''
        self.comment = ''

    def __str__(self) -> str:
        ret = '{} version {!r}'.format(self.name, self.version)

        if self.runtime or self.runtime_version:
            ret = ret + ' (from {} version {})'.format(
                self.runtime or '(unknown runtime)',
                self.runtime_version or '(unknown)',
            )

        return ret

    def to_sort_key(self) -> Tuple[int, str]:
        return (self.sort_weight, self.to_tsv())

    def to_tsv(self) -> str:
        if self.comment:
            comment = '# ' + self.comment
        else:
            comment = ''

        return '\t'.join((
            self.name, self.version,
            self.runtime, self.runtime_version,
            comment,
        )) + '\n'


class Main:
    def __init__(
        self,
        architecture: str = 'amd64,i386',
        cache: str = '.cache',
        credential_envs: Sequence[str] = (),
        credential_hosts: Sequence[str] = (),
        images_uri: str = DEFAULT_IMAGES_URI,
        include_sdk_debug: bool = False,
        include_sdk_runtime: bool = False,
        include_sdk_sysroot: bool = False,
        layered: bool = False,
        pressure_vessel_archive: str = '',
        pressure_vessel_from_runtime: str = '',
        pressure_vessel_from_runtime_json: str = '',
        pressure_vessel_guess: str = '',
        pressure_vessel_ssh_host: str = '',
        pressure_vessel_ssh_path: str = '',
        pressure_vessel_uri: str = DEFAULT_PRESSURE_VESSEL_URI,
        pressure_vessel_version: str = '',
        ssh_host: str = '',
        ssh_path: str = '',
        toolmanifest: bool = False,
        unpack_ld_library_path: str = '',
        unpack_sources: Sequence[str] = (),
        unpack_sources_into: str = '.',
        version: str = '',
        versioned_directories: bool = False,
        **kwargs: Dict[str, Any],
    ) -> None:
        openers: List[urllib.request.BaseHandler] = []

        if not credential_hosts:
            credential_hosts = []
            host = urllib.parse.urlparse(images_uri).hostname

            if host is not None:
                credential_hosts.append(host)

        if credential_envs:
            password_manager = urllib.request.HTTPPasswordMgrWithDefaultRealm()

            for cred in credential_envs:
                if ':' in cred:
                    username_env, password_env = cred.split(':', 1)
                    logger.info(
                        'Using username from $%s and password from $%s',
                        username_env, password_env)
                    username = os.environ[username_env]
                    password = os.environ[password_env]
                else:
                    logger.info(
                        'Using username and password from $%s', cred)
                    username, password = os.environ[cred].split(':', 1)

                for host in credential_hosts:
                    password_manager.add_password(
                        None,       # type: ignore
                        host,
                        username,
                        password,
                    )

            openers.append(
                urllib.request.HTTPBasicAuthHandler(password_manager)
            )

        self.opener = urllib.request.build_opener(*openers)

        self.default_architecture = architecture
        self.default_suite = suite
        self.default_version = version
        self.depot = os.path.abspath(depot)
        self.images_uri = images_uri
        self.include_archives = include_archives
        self.include_sdk_debug = include_sdk_debug
        self.include_sdk_runtime = include_sdk_runtime
        self.include_sdk_sysroot = include_sdk_sysroot
        self.layered = layered
        self.pressure_vessel_ssh_host = pressure_vessel_ssh_host or ssh_host
        self.pressure_vessel_ssh_path = pressure_vessel_ssh_path
        self.pressure_vessel_uri = pressure_vessel_uri
        self.source_dir = source_dir
        self.ssh_host = ssh_host
        self.ssh_path = ssh_path
        self.toolmanifest = toolmanifest
        self.unpack_ld_library_path = unpack_ld_library_path
        self.unpack_runtime = unpack_runtime
        self.unpack_sources = unpack_sources
        self.unpack_sources_into = unpack_sources_into
        self.versioned_directories = versioned_directories
        n_sources = 0

        for source in (
            pressure_vessel_archive,
            pressure_vessel_from_runtime,
            pressure_vessel_from_runtime_json,
            pressure_vessel_guess,
            pressure_vessel_version = 'latest'
        elif n_sources > 1:
            raise RuntimeError(
                'Cannot combine more than one of '
                '--pressure-vessel, '
                '--pressure-vessel-archive, '
                '--pressure-vessel-from-runtime, '
                '--pressure-vessel-from-runtime-json and '
                '--pressure-vessel-version'
        os.makedirs(self.cache, exist_ok=True)
        if not (self.include_archives or self.unpack_runtime):
            raise RuntimeError(
                'Cannot use both --no-include-archives and '
        if '=' in runtime:
            name, rhs = runtime.split('=', 1)
            if rhs.startswith('{'):
                details = json.loads(rhs)
                with open(rhs, 'rb') as reader:
                    details = json.load(reader)
        else:
            name = runtime
            details = {}
        self.runtime = self.new_runtime(name, details)
        self.versions = []      # type: List[ComponentVersion]
        if pressure_vessel_guess:
            if self.runtime.name == pressure_vessel_guess:
                pressure_vessel_from_runtime = pressure_vessel_guess
            elif pressure_vessel_guess.startswith('{'):
                pressure_vessel_from_runtime_json = pressure_vessel_guess
            elif os.path.isdir(pressure_vessel_guess):
                pressure_vessel_archive = os.path.join(
                    pressure_vessel_guess, 'pressure-vessel-bin.tar.gz',
                )
            elif (
                os.path.isfile(pressure_vessel_guess)
                and pressure_vessel_guess.endswith('.tar.gz')
            ):
                pressure_vessel_archive = pressure_vessel_guess
            else:
                pressure_vessel_from_runtime = pressure_vessel_guess

        self.pressure_vessel_runtime = None     # type: Optional[Runtime]
        self.pressure_vessel_version = ''

        if pressure_vessel_version:
            self.pressure_vessel_version = pressure_vessel_version
        elif pressure_vessel_archive:
            self.pressure_vessel_runtime = self.new_runtime(
                'scout',
                {'path': pressure_vessel_archive},
                default_suite='local',
            )
        elif self.runtime.name == pressure_vessel_from_runtime:
            self.pressure_vessel_runtime = self.runtime
        elif pressure_vessel_from_runtime:
            self.pressure_vessel_runtime = self.new_runtime(
                pressure_vessel_from_runtime, {},
                default_suite=pressure_vessel_from_runtime,
            )
        elif pressure_vessel_from_runtime_json:
            self.pressure_vessel_runtime = self.new_runtime(
                'scout',
                json.loads(pressure_vessel_from_runtime_json),
                default_suite='scout',
            )

    def new_runtime(
        self,
        name: str,
        details: Dict[str, Any],
        default_suite: str = '',
    ) -> Runtime:
        return Runtime.from_details(
            name,
            details,
            default_architecture=self.default_architecture,
            default_suite=default_suite or self.default_suite,
            default_version=self.default_version,
            images_uri=self.images_uri,
            ssh_host=self.ssh_host,
            ssh_path=self.ssh_path,
    def merge_dir_into_depot(
        self,
        source_root: str,
    ):
        for (dirpath, dirnames, filenames) in os.walk(source_root):
            relative_path = os.path.relpath(dirpath, source_root)

            for member in dirnames:
                os.makedirs(
                    os.path.join(self.depot, relative_path, member),
                    exist_ok=True,
                )

            for member in filenames:
                source = os.path.join(dirpath, member)
                merged = os.path.join(self.depot, relative_path, member)

                with suppress(FileNotFoundError):
                    os.unlink(merged)

                os.makedirs(os.path.dirname(merged), exist_ok=True)
                shutil.copy(source, merged)

        if self.layered:
            self.do_layered_runtime()
        else:
            self.do_container_runtime()

    def do_layered_runtime(self) -> None:
        if self.runtime.name != 'scout':
            raise InvocationError('Can only layer scout onto soldier')

        if self.unpack_ld_library_path:
            raise InvocationError(
                'Cannot use --unpack-ld-library-path with --layered'
            )

        if self.include_archives:
            raise InvocationError(
                'Cannot use --include-archives with --layered'
            )

        if (
            self.include_sdk_debug
            or self.include_sdk_runtime
            or self.include_sdk_sysroot
        ):
            raise InvocationError(
                'Cannot use --include-sdk-* with --layered'
            )

        if self.unpack_sources:
            raise InvocationError(
                'Cannot use --unpack-source with --layered'
            )

        self.merge_dir_into_depot(
            os.path.join(self.source_dir, 'runtimes', 'scout-on-soldier')
        )

        if self.runtime.version:
            self.unpack_ld_library_path = self.depot
            self.download_scout_tarball(self.runtime)
            local_version = ComponentVersion('LD_LIBRARY_PATH')
            version = self.runtime.pinned_version
            assert version is not None
            local_version.version = version
            local_version.runtime = 'scout'
            local_version.runtime_version = version
            local_version.comment = 'steam-runtime/'
            self.versions.append(local_version)
        else:
            unspecified_version = ComponentVersion('LD_LIBRARY_PATH')
            unspecified_version.version = '-'
            unspecified_version.runtime = 'scout'
            unspecified_version.runtime_version = '-'
            unspecified_version.comment = (
                'see ~/.steam/root/ubuntu12_32/steam-runtime/version.txt'
            )
            self.versions.append(unspecified_version)

        self.write_component_versions()

    def ensure_ref(self, path: str) -> None:
        '''
        Create $path/files/.ref as an empty regular file.

        This is useful because pressure-vessel would create this file
        during processing. If it gets committed to the depot, then Steampipe
        will remove it when superseded.
        '''
        ref = os.path.join(path, 'files', '.ref')

        try:
            statinfo = os.stat(ref, follow_symlinks=False)
        except FileNotFoundError:
            with open(ref, 'x'):
                pass
        else:
            if statinfo.st_size > 0 or not stat.S_ISREG(statinfo.st_mode):
                raise RuntimeError(
                    'Expected {} to be an empty regular file'.format(path)
                )

    def do_container_runtime(self) -> None:
        pv_version = ComponentVersion('pressure-vessel')
        self.merge_dir_into_depot(os.path.join(self.source_dir, 'common'))

        root = os.path.join(self.source_dir, 'runtimes', self.runtime.name)
        if os.path.exists(root):
            self.merge_dir_into_depot(root)
        pressure_vessel_runtime = self.pressure_vessel_runtime

        if self.pressure_vessel_version:
            logger.info(
                'Downloading standalone pressure-vessel release'
            pv_version.version = self.download_pressure_vessel_standalone(
                self.pressure_vessel_version
            )
        else:
            assert pressure_vessel_runtime is not None

            if pressure_vessel_runtime.path:
                self.use_local_pressure_vessel(pressure_vessel_runtime.path)
                pv_version.comment = 'from local file'
            else:
                pv_version.comment = (
                    self.download_pressure_vessel_from_runtime(
                        pressure_vessel_runtime
                    )
                )

        for path in ('metadata/VERSION.txt', 'sources/VERSION.txt'):
            full = os.path.join(self.depot, 'pressure-vessel', path)
            if os.path.exists(full):
                with open(full) as text_reader:
                    v = text_reader.read().rstrip('\n')
                    if pv_version.version:
                        if pv_version.version != v:
                            raise RuntimeError(
                                'Inconsistent version! '
                                '{} says {}, but expected {}'.format(
                                    path, v, pv_version.version,
                                )
                            )
                    else:
                        pv_version.version = v
        if pressure_vessel_runtime is not None:
            pv_version.runtime = pressure_vessel_runtime.suite or ''
            pv_version.runtime_version = (
                pressure_vessel_runtime.pinned_version or ''
            )

        self.versions.append(pv_version)
        if self.unpack_ld_library_path and pressure_vessel_runtime is None:
            if self.runtime.name == 'scout':
                scout = self.runtime
            else:
                scout = self.new_runtime(
                    'scout',
                    dict(version='latest'),
                    default_suite='scout',
                )
            logger.info(
                'Downloading LD_LIBRARY_PATH Steam Runtime from scout into %r',
                self.unpack_ld_library_path)
            self.download_scout_tarball(scout)
        elif self.unpack_ld_library_path:
            assert pressure_vessel_runtime is not None
            logger.info(
                'Downloading LD_LIBRARY_PATH Steam Runtime from same place '
                'as pressure-vessel into %r',
                self.unpack_ld_library_path)
            self.download_scout_tarball(pressure_vessel_runtime)
        if self.unpack_sources:
            logger.info(
                'Will download %s source code into %r',
                ', '.join(self.unpack_sources), self.unpack_sources_into)
            os.makedirs(self.unpack_sources_into, exist_ok=True)

            os.makedirs(
                os.path.join(self.unpack_sources_into, self.runtime.name),
                exist_ok=True,
            )
        for runtime in (self.runtime,):     # too much to reindent right now
                    'Using runtime from local directory %r',
                self.use_local_runtime(runtime)
                    'Downloading runtime from %s',
                self.download_runtime(runtime)
            component_version = ComponentVersion(runtime.name)

            if runtime.path:
                with open(
                    os.path.join(runtime.path, runtime.build_id_file), 'r',
                ) as text_reader:
                    version = text_reader.read().strip()
            else:
                version = runtime.pinned_version or ''
                assert version
                    runtime.get_archives(
                        include_sdk_debug=self.include_sdk_debug,
                        include_sdk_runtime=self.include_sdk_runtime,
                        include_sdk_sysroot=self.include_sdk_sysroot,
                    )
                    subdir = '{}_platform_{}'.format(runtime.name, version)
                dest = os.path.join(self.depot, subdir)
                runtime_files.add(subdir + '/')

                with suppress(FileNotFoundError):
                    shutil.rmtree(dest)

                os.makedirs(dest, exist_ok=True)
                argv = [
                    'tar',
                    '-C', dest,
                    os.path.join(self.cache, runtime.tarball),
                ]
                logger.info('%r', argv)
                subprocess.run(argv, check=True)
                    os.path.join(self.cache, runtime.tarball),

                if self.minimize:
                    self.minimize_runtime(dest)

                        sdk_subdir = '{}_sdk_{}'.format(runtime.name, version)
                        sdk_subdir = '{}_sdk'.format(runtime.name)

                    dest = os.path.join(self.depot, sdk_subdir)
                    runtime_files.add(sdk_subdir + '/')