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