Skip to content
Snippets Groups Projects
set-build-id 7 KiB
Newer Older
#!/usr/bin/python3

# flatdeb — build Flatpak runtimes from Debian packages
#
# Copyright © 2016-2017 Simon McVittie
# Copyright © 2017-2019 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.

import argparse
import logging
import os
import subprocess
import sys

try:
    import typing
except ImportError:
    pass
else:
    typing  # silence "unused" warnings


logger = logging.getLogger('flatdeb.set-build-id')


def quote(s):
    # We can't use shlex.quote() because it relies on concatenated
    # strings, which are valid for sh but not for /etc/os-release
    quoted = s.replace(
        '\\', '\\\\',
    ).replace(
        '$', '\\$',
    ).replace(
        '`', '\\`',
    ).replace(
        '"', '\\"',
    )

    for c in quoted:
        if c >= '\x7f' or not c.isalnum():
            quoted = '"{}"'.format(quoted)
            break

    return quoted


def main():
    # type: (...) -> None

    parser = argparse.ArgumentParser(
        description='Set build ID in chroot'
    )
    parser.add_argument('--build-id', default='')
    parser.add_argument('--variant', default='')
    parser.add_argument('--variant-id', default='')
    parser.add_argument('--test-quoting', action='store_true')
    parser.add_argument('sysroot')

    args = parser.parse_args()
    lines = []

    if args.test_quoting:
        for x in (
            'debian',
            '10',
        ):
            assert quote(x) == x, x

        for x in (
            'Debian GNU/Linux 10 (buster)',
            '3.141592654',
        ):
            assert quote(x) == '"' + x + '"', x

        for orig, quoted in (
            ("My 'great' distro", '"My \'great\' distro"'),
            ('My "great" distro', '"My \\"great\\" distro"'),
            ('$PATH', '"\\$PATH"'),
            ('C:\\Windows', '"C:\\\\Windows"'),
            ('Shell `injection`', '"Shell \\`injection\\`"'),
        ):
            assert quote(orig) == quoted
            unquoted = subprocess.check_output(
                'printf "%s" ' + quoted,
                universal_newlines=True,
                shell=True,
            )
            assert unquoted == orig, (unquoted, orig)

        return

    os_id = ''
    version_codename = ''
    version_id = ''

    with open(
        os.path.join(args.sysroot, 'usr', 'lib', 'os-release'),
        'r',
    ) as reader:
        for line in reader:
            if line.startswith(('VARIANT=', 'VARIANT_ID=', 'BUILD_ID=')):
                logger.info('# Ignoring: %s', line.strip())
            else:
                logger.info('%s', line.strip())
                lines.append(line)
                value = line.split('=', 1)[1].strip()

                if value[0] == '"':
                    assert value.endswith('"'), value
                    value = value[1:-1]

                # Unescape \\, \", \$, \`. The string can't contain \n
                # so we can use \n as a placeholder for a literal backslash.
                value = value.replace('\\\\', '\n')
                value = value.replace('\\', '')
                value = value.replace('\n', '\\')

                if line.startswith('ID='):
                    os_id = value
                elif line.startswith('VERSION_CODENAME='):
                    version_codename = value
                elif line.startswith('VERSION_ID='):
                    version_id = value

    if args.build_id:
        logger.info('Adding BUILD_ID=%s', quote(args.build_id))
        lines.append('BUILD_ID={}\n'.format(quote(args.build_id)))

    if args.variant:
        logger.info('Adding VARIANT=%s', quote(args.variant))
        lines.append('VARIANT={}\n'.format(quote(args.variant)))

    if args.variant_id:
        logger.info('Adding VARIANT_ID=%s', quote(args.variant_id))
        lines.append('VARIANT_ID={}\n'.format(quote(args.variant_id)))

    with open(
        os.path.join(args.sysroot, 'usr', 'lib', 'os-release.new'),
        'w',
    ) as writer:
        writer.writelines(lines)

    os.rename(
        os.path.join(args.sysroot, 'usr', 'lib', 'os-release.new'),
        os.path.join(args.sysroot, 'usr', 'lib', 'os-release'),
    )

    if version_codename:
        # e.g. buster, leading to (buster) user@host:~$
        chroot_name = version_codename
    elif version_id:
        # e.g. 10, leading to (10) user@host:~$
        chroot_name = version_id
    else:
        chroot_name = ''

    # We use the OS ID instead of the human-readable NAME because we want
    # something short.
    if os_id:
        if chroot_name:
            # e.g. leading to (debian buster) user@host:~$
            # or (debian 10) user@host:~$
            chroot_name = '{} {}'.format(os_id, chroot_name)
        else:
            # e.g. (debian) user@host:~$
            chroot_name = os_id

    if args.build_id:
        if chroot_name:
            # e.g. (debian buster 20190603) user@host:~$
            chroot_name = '{} {}'.format(chroot_name, args.build_id)
        else:
            # e.g. (20190603) user@host:~$
            chroot_name = args.build_id

    if chroot_name:
        logger.info('Setting debian_chroot to %r', chroot_name)

        with open(
            os.path.join(args.sysroot, 'etc', 'debian_chroot.new'),
            'w',
        ) as writer:
            writer.write(chroot_name + '\n')
    else:
        logger.info('Not setting debian_chroot')

    os.rename(
        os.path.join(args.sysroot, 'etc', 'debian_chroot.new'),
        os.path.join(args.sysroot, 'etc', 'debian_chroot'),
    )


if __name__ == '__main__':
    if sys.stderr.isatty():
        try:
            import colorlog
        except ImportError:
            pass
        else:
            formatter = colorlog.ColoredFormatter(
                '%(log_color)s%(levelname)s:%(name)s:%(reset)s %(message)s')
            handler = logging.StreamHandler()
            handler.setFormatter(formatter)
            logging.getLogger().addHandler(handler)
    else:
        logging.basicConfig()

    logging.getLogger().setLevel(logging.DEBUG)

    try:
        main()
    except KeyboardInterrupt:
        raise SystemExit(130)