1#!/usr/bin/env python3
2
3import argparse
4import os
5import sys
6import subprocess
7import time
8
9SRC_MOUNT = "/root/src"
10
11
12class ContainerImageBuilder:
13    """Builds the container image for Floss build environment."""
14
15    def __init__(self, workdir, rootdir, tag, use_docker):
16        """ Constructor.
17
18        Args:
19            workdir: Working directory for this script. Containerfile should exist here.
20            rootdir: Root directory for Bluetooth.
21            tag: Label in format |name:version|.
22            use_docker: Use docker binary if True (or podman when False).
23        """
24        self.workdir = workdir
25        self.rootdir = rootdir
26        (self.name, self.version) = tag.split(':')
27        self.build_tag = '{}:{}'.format(self.name, 'buildtemp')
28        self.container_name = 'floss-buildtemp'
29        self.final_tag = tag
30        self.container_binary = 'docker' if use_docker else 'podman'
31        self.env = os.environ.copy()
32
33        # Mark dpkg builders for container
34        self.env['LIBCHROME_DOCKER'] = '1'
35        self.env['MODP_DOCKER'] = '1'
36
37    def run_command(self, target, args, cwd=None, env=None, ignore_rc=False):
38        """ Run command and stream the output.
39        """
40        # Set some defaults
41        if not cwd:
42            cwd = self.workdir
43        if not env:
44            env = self.env
45
46        rc = 0
47        process = subprocess.Popen(args, cwd=cwd, env=env, stdout=subprocess.PIPE)
48        while True:
49            line = process.stdout.readline()
50            print(line.decode('utf-8'), end="")
51            if not line:
52                rc = process.poll()
53                if rc is not None:
54                    break
55
56                time.sleep(0.1)
57
58        if rc != 0 and not ignore_rc:
59            raise Exception("{} failed. Return code is {}".format(target, rc))
60
61    def _container_build(self):
62        self.run_command(self.container_binary + ' build', [self.container_binary, 'build', '-t', self.build_tag, '.'])
63
64    def _build_dpkg_and_commit(self):
65        # Try to remove any previous instance of the container that may be
66        # running if this script didn't complete cleanly last time.
67        self.run_command(self.container_binary + ' stop', [self.container_binary, 'stop', '-t', '1', self.container_name], ignore_rc=True)
68        self.run_command(self.container_binary + ' rm', [self.container_binary, 'rm', self.container_name], ignore_rc=True)
69
70        # Runs never terminating application on the newly built image in detached mode
71        mount_str = 'type=bind,src={},dst={},readonly'.format(self.rootdir, SRC_MOUNT)
72        self.run_command(self.container_binary + ' run', [
73            self.container_binary, 'run', '--name', self.container_name, '--mount', mount_str, '-d', self.build_tag, 'tail', '-f',
74            '/dev/null'
75        ])
76
77        commands = [
78            # Create the output directories
79            ['mkdir', '-p', '/tmp/libchrome', '/tmp/modpb64'],
80
81            # Run the dpkg builder for modp_b64
82            [f'{SRC_MOUNT}/system/build/dpkg/modp_b64/gen-src-pkg.sh', '/tmp/modpb64'],
83
84            # Install modp_b64 since libchrome depends on it
85            ['find', '/tmp/modpb64', '-name', 'modp*.deb', '-exec', 'dpkg', '-i', '{}', '+'],
86
87            # Run the dpkg builder for libchrome
88            [f'{SRC_MOUNT}/system/build/dpkg/libchrome/gen-src-pkg.sh', '/tmp/libchrome'],
89
90            # Install libchrome.
91            ['find', '/tmp/libchrome', '-name', 'libchrome_*.deb', '-exec', 'dpkg', '-i', '{}', '+'],
92
93            # Run the dpkg builder for sysprop
94            [f'{SRC_MOUNT}/system/build/dpkg/sysprop/gen-src-pkg.sh', '/tmp/sysprop'],
95
96            # Install sysprop.
97            ['find', '/tmp/sysprop', '-name', 'sysprop_*.deb', '-exec', 'dpkg', '-i', '{}', '+'],
98
99            # Delete intermediate files
100            ['rm', '-rf', '/tmp/libchrome', '/tmp/modpb64', '/tmp/sysprop'],
101        ]
102
103        try:
104            # Run commands in container first to install everything.
105            for i, cmd in enumerate(commands):
106                self.run_command(self.container_binary + ' exec #{}'.format(i), [self.container_binary, 'exec', '-it', self.container_name] + cmd)
107
108            # Commit changes into the final tag name
109            self.run_command(self.container_binary + ' commit', [self.container_binary, 'commit', self.container_name, self.final_tag])
110        finally:
111            # Stop running the container and remove it
112            self.run_command(self.container_binary + ' stop', [self.container_binary, 'stop', '-t', '1', self.container_name])
113            self.run_command(self.container_binary + ' rm', [self.container_binary, 'rm', self.container_name])
114
115    def _check_container_runnable(self):
116        try:
117            subprocess.check_output([self.container_binary, 'ps'], stderr=subprocess.STDOUT)
118        except subprocess.CalledProcessError as err:
119            if 'denied' in err.output.decode('utf-8'):
120                print('Run script as sudo')
121            else:
122                print('Unexpected error: {}'.format(err.output.decode('utf-8')))
123
124            return False
125
126        # No exception means container is ok
127        return True
128
129    def build(self):
130        if not self._check_container_runnable():
131            return
132
133        # First build the container image
134        self._container_build()
135
136        # Then build libchrome and modp-b64 inside the container image and
137        # install them. Commit those changes to the final label.
138        self._build_dpkg_and_commit()
139
140
141def main():
142    parser = argparse.ArgumentParser(description='Build container image for Floss build environment.')
143    parser.add_argument('--tag', required=True, help='Tag for container image. i.e. floss:latest')
144    parser.add_argument('--use-docker', action='store_true', default=False, help='Use flag to use Docker to build Floss. Defaults to using podman.')
145    args = parser.parse_args()
146
147    # cwd should be set to same directory as this script (that's where
148    # Dockerfile is kept).
149    workdir = os.path.dirname(os.path.abspath(sys.argv[0]))
150    rootdir = os.path.abspath(os.path.join(workdir, '../..'))
151
152    # Build the container image
153    pib = ContainerImageBuilder(workdir, rootdir, args.tag, args.use_docker)
154    pib.build()
155
156
157if __name__ == '__main__':
158    main()
159