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