1#!/usr/bin/env python3 2 3import argparse 4import os 5import subprocess 6import sys 7import time 8 9SRC_MOUNT = "/root/src" 10STAGING_MOUNT = "/root/.floss" 11 12 13class FlossContainerRunner: 14 """Runs Floss build inside container.""" 15 16 # Commands to run for build 17 BUILD_COMMANDS = [ 18 # First run bootstrap to get latest code + create symlinks 19 [f'{SRC_MOUNT}/build.py', '--run-bootstrap', '--partial-staging'], 20 21 # Clean up any previous artifacts inside the volume 22 [f'{SRC_MOUNT}/build.py', '--target', 'clean'], 23 24 # Run normal code builder 25 [f'{SRC_MOUNT}/build.py', '--target', 'all'], 26 27 # Run tests 28 [f'{SRC_MOUNT}/build.py', '--target', 'test'], 29 ] 30 31 def __init__(self, workdir, rootdir, image_tag, volume_name, container_name, staging_dir, use_docker, 32 use_pseudo_tty): 33 """ Constructor. 34 35 Args: 36 workdir: Current working directory (should be the script path). 37 rootdir: Root directory for Bluetooth. 38 image_tag: Tag for container image used for building. 39 volume_name: Volume name used for storing artifacts. 40 container_name: Name for running container instance. 41 staging_dir: Directory to mount for artifacts instead of using volume. 42 use_docker: Use docker binary if True (or podman when False). 43 use_pseudo_tty: Run container with pseudo tty if true. 44 """ 45 self.workdir = workdir 46 self.rootdir = rootdir 47 self.image_tag = image_tag 48 self.container_binary = 'docker' if use_docker else 'podman' 49 self.env = os.environ.copy() 50 51 # Flags used by container exec: 52 # -i: interactive mode keeps STDIN open even if not attached 53 # -t: Allocate a pseudo-TTY (better user experience) 54 # Only set if use_pseudo_tty is true. 55 self.container_exec_flags = '-it' if use_pseudo_tty else '-i' 56 57 # Name of running container 58 self.container_name = container_name 59 60 # Name of volume to write output. 61 self.volume_name = volume_name 62 # Staging dir where we send output instead of the volume. 63 self.staging_dir = staging_dir 64 65 def run_command(self, target, args, cwd=None, env=None, ignore_rc=False): 66 """ Run command and stream the output. 67 """ 68 # Set some defaults 69 if not cwd: 70 cwd = self.workdir 71 if not env: 72 env = self.env 73 74 rc = 0 75 process = subprocess.Popen(args, cwd=cwd, env=env, stdout=subprocess.PIPE) 76 while True: 77 line = process.stdout.readline() 78 print(line.decode('utf-8'), end="") 79 if not line: 80 rc = process.poll() 81 if rc is not None: 82 break 83 84 time.sleep(0.1) 85 86 if rc != 0 and not ignore_rc: 87 raise Exception("{} failed. Return code is {}".format(target, rc)) 88 89 def _create_volume_if_needed(self): 90 # Check if the volume exists. Otherwise create it. 91 try: 92 subprocess.check_output([self.container_binary, 'volume', 'inspect', self.volume_name]) 93 except: 94 self.run_command(self.container_binary + ' volume create', 95 [self.container_binary, 'volume', 'create', self.volume_name]) 96 97 def start_container(self): 98 """Starts the container with correct mounts.""" 99 # Stop any previously started container. 100 self.stop_container(ignore_error=True) 101 102 # Create volume and create mount string 103 if self.staging_dir: 104 mount_output_volume = 'type=bind,src={},dst={}'.format(self.staging_dir, STAGING_MOUNT) 105 else: 106 # If not using staging dir, use the volume instead 107 self._create_volume_if_needed() 108 mount_output_volume = 'type=volume,src={},dst={}'.format(self.volume_name, STAGING_MOUNT) 109 110 # Mount the source directory 111 mount_src_dir = 'type=bind,src={},dst={}'.format(self.rootdir, SRC_MOUNT) 112 113 # Run the container image. It will run `tail` indefinitely so the container 114 # doesn't close and we can run `<container_binary> exec` on it. 115 self.run_command(self.container_binary + ' run', [ 116 self.container_binary, 'run', '--name', self.container_name, '--mount', mount_output_volume, '--mount', 117 mount_src_dir, '-d', self.image_tag, 'tail', '-f', '/dev/null' 118 ]) 119 120 def stop_container(self, ignore_error=False): 121 """Stops the container for build.""" 122 self.run_command(self.container_binary + ' stop', 123 [self.container_binary, 'stop', '-t', '1', self.container_name], 124 ignore_rc=ignore_error) 125 self.run_command(self.container_binary + ' rm', [self.container_binary, 'rm', self.container_name], 126 ignore_rc=ignore_error) 127 128 def do_build(self): 129 """Runs the basic build commands.""" 130 # Start container before building 131 self.start_container() 132 133 try: 134 # Run all commands 135 for i, cmd in enumerate(self.BUILD_COMMANDS): 136 self.run_command(self.container_binary + ' exec #{}'.format(i), 137 [self.container_binary, 'exec', self.container_exec_flags, self.container_name] + cmd) 138 finally: 139 # Always stop container before exiting 140 self.stop_container() 141 142 def print_do_build(self): 143 """Prints the commands for building.""" 144 container_exec = [self.container_binary, 'exec', self.container_exec_flags, self.container_name] 145 print('Normally, build would run the following commands: \n') 146 for cmd in self.BUILD_COMMANDS: 147 print(' '.join(container_exec + cmd)) 148 149 def check_container_runnable(self): 150 try: 151 subprocess.check_output([self.container_binary, 'ps'], stderr=subprocess.STDOUT) 152 except subprocess.CalledProcessError as err: 153 if 'denied' in err.output.decode('utf-8'): 154 print('Run script as sudo') 155 else: 156 print('Unexpected error: {}'.format(err.output.decode('utf-8'))) 157 158 return False 159 160 # No exception means container is ok 161 return True 162 163 164if __name__ == "__main__": 165 parser = argparse.ArgumentParser('Builder Floss inside container image.') 166 parser.add_argument('--only-start', 167 action='store_true', 168 default=False, 169 help='Only start the container. Prints the commands it would have ran.') 170 parser.add_argument('--only-stop', action='store_true', default=False, help='Only stop the container and exit.') 171 parser.add_argument('--image-tag', default='floss:latest', help='Container image to use to build.') 172 parser.add_argument('--volume-tag', 173 default='floss-out', 174 help='Name of volume to use. This is where build artifacts will be stored by default.') 175 parser.add_argument('--staging-dir', 176 default=None, 177 help='Staging directory to use instead of volume. Build artifacts will be written here.') 178 parser.add_argument('--container-name', 179 default='floss-container-runner', 180 help='What to name the started container.') 181 parser.add_argument('--use-docker', 182 action='store_true', 183 default=False, 184 help='Use flag to use Docker to build Floss. Defaults to using podman.') 185 parser.add_argument('--no-tty', 186 action='store_true', 187 default=False, 188 help='Use flag to disable pseudo tty for docker container.') 189 args = parser.parse_args() 190 191 # cwd should be set to same directory as this script (that's where 192 # Dockerfile is kept). 193 workdir = os.path.dirname(os.path.abspath(sys.argv[0])) 194 rootdir = os.path.abspath(os.path.join(workdir, '../..')) 195 196 # Determine staging directory absolute path 197 staging = os.path.abspath(args.staging_dir) if args.staging_dir else None 198 199 fdr = FlossContainerRunner(workdir, rootdir, args.image_tag, args.volume_tag, args.container_name, staging, 200 args.use_docker, not args.no_tty) 201 202 # Make sure container is runnable before continuing 203 if fdr.check_container_runnable(): 204 # Handle some flags 205 if args.only_start: 206 fdr.start_container() 207 fdr.print_do_build() 208 elif args.only_stop: 209 fdr.stop_container() 210 else: 211 fdr.do_build() 212