blob: 7c7e91cce99a77d61295bef26d1eac1c9a1a9391 [file] [log] [blame]
Brian Silvermana29ebf92014-04-23 13:08:49 -05001#!/usr/bin/python3
2
3import argparse
4import sys
5import subprocess
6import re
7import os
8import os.path
9import string
10import shutil
11import errno
12
Brian Silvermanbe6cfe22014-04-27 08:06:27 -050013def aos_path():
14 return os.path.join(os.path.dirname(__file__), '..')
15
16def get_ip(device):
17 FILENAME = os.path.normpath(os.path.join(aos_path(), '..', 'output', 'ip_base.txt'))
18 if not os.access(FILENAME, os.R_OK):
19 os.makedirs(os.path.dirname(FILENAME), exist_ok=True)
20 with open(FILENAME, 'w') as f:
21 f.write('10.9.71')
22 with open(FILENAME, 'r') as f:
23 base = f.readline()
24 if device == 'prime':
25 return base + '.179'
26 elif device == 'robot':
27 return base + '.2'
28 else:
29 raise Exception('Unknown device %s to get an IP address for.' % device)
30
Brian Silvermana9b1e5c2014-04-30 18:08:04 -070031def user_output(message):
32 print('build.py: ' + message, file=sys.stderr)
33
Brian Silvermana29ebf92014-04-23 13:08:49 -050034class Processor(object):
35 class UnknownPlatform(Exception):
36 def __init__(self, message):
37 self.message = message
38
Brian Silvermanb3d50542014-04-23 14:28:55 -050039 class Platform(object):
40 def outdir(self):
41 return os.path.join(
Brian Silvermanbe6cfe22014-04-27 08:06:27 -050042 aos_path(), '..', 'output', self.outname())
Brian Silvermanb3d50542014-04-23 14:28:55 -050043 def build_ninja(self):
44 return os.path.join(self.outdir(), 'build.ninja')
45
Brian Silvermanbe6cfe22014-04-27 08:06:27 -050046 def do_deploy(self, dry_run, command):
47 real_command = (('echo',) + command) if dry_run else command
48 subprocess.check_call(real_command, stdin=open(os.devnull, 'r'))
Brian Silvermana29ebf92014-04-23 13:08:49 -050049
Brian Silvermanc2d8e5a2014-05-01 18:33:12 -070050 # TODO(brians): Verify that this (and its callers) catch everything from a
51 # fresh install.
52 def do_check_installed(self, other_packages):
53 all_packages = () + other_packages
54 try:
55 result = subprocess.check_output(
56 ('dpkg-query', '--show') + all_packages,
57 stdin=open(os.devnull, 'r'),
58 stderr=subprocess.STDOUT)
59 except subprocess.CalledProcessError as e:
60 user_output('Some packages not installed:\n'
61 + e.output.decode('utf-8').rstrip())
62 exit(1)
63
Brian Silvermana29ebf92014-04-23 13:08:49 -050064class CRIOProcessor(Processor):
Brian Silvermanb3d50542014-04-23 14:28:55 -050065 class Platform(Processor.Platform):
66 def __init__(self, debug):
67 super(CRIOProcessor.Platform, self).__init__()
68
69 self.debug = debug
70
71 def __repr__(self):
72 return 'CRIOProcessor.Platform(debug=%s)' % self.debug
73 def __str__(self):
74 return 'crio%s' % ('-debug' if self.debug else '')
75
76 def outname(self):
77 return 'crio-debug' if self.debug else 'crio'
78 def os(self):
79 return 'vxworks'
80 def gyp_platform(self):
81 return 'crio'
82 def architecture(self):
83 return 'ppc'
84 def compiler(self):
85 return 'gcc'
86
Brian Silvermane48c09a2014-04-30 18:04:58 -070087 # TODO(brians): test this
Brian Silvermanbe6cfe22014-04-27 08:06:27 -050088 def deploy(self, dry_run):
89 self.do_deploy(dry_run,
90 ('ncftpput', get_ip('robot'), '/',
91 os.path.join(self.outdir(), 'lib', 'FRC_UserProgram.out')))
92
Brian Silvermana29ebf92014-04-23 13:08:49 -050093 def __init__(self):
94 super(CRIOProcessor, self).__init__()
95
96 if 'WIND_BASE' in os.environ:
97 self.wind_base = os.environ['WIND_BASE']
98 else:
99 self.wind_base = '/usr/local/powerpc-wrs-vxworks/wind_base'
100
101 def parse_platforms(self, string):
Brian Silvermanb3d50542014-04-23 14:28:55 -0500102 if string is None or string == 'crio':
103 return (CRIOProcessor.Platform(False),)
104 elif string == 'crio-debug':
105 return (CRIOProcessor.Platform(True),)
106 else:
107 raise Processor.UnknownPlatform('Unknown cRIO platform "%s".' % string)
Brian Silvermana29ebf92014-04-23 13:08:49 -0500108
109 def build_env(self):
110 return {'WIND_BASE': self.wind_base}
Brian Silvermanb3d50542014-04-23 14:28:55 -0500111 def extra_gyp_flags(self):
112 return ('-DWIND_BASE=%s' % self.wind_base,)
113
Brian Silvermana29ebf92014-04-23 13:08:49 -0500114 def is_crio(self): return True
115
Brian Silvermanc2d8e5a2014-05-01 18:33:12 -0700116 def check_installed(self):
117 # TODO(brians): Add powerpc-wrs-vxworks (a new enough version too).
118 self.do_check_installed(
119 ('ncftp',))
120
Brian Silvermana29ebf92014-04-23 13:08:49 -0500121class PrimeProcessor(Processor):
Brian Silvermanb3d50542014-04-23 14:28:55 -0500122 class Platform(Processor.Platform):
Brian Silvermana29ebf92014-04-23 13:08:49 -0500123 def __init__(self, architecture, compiler, debug):
Brian Silvermanb3d50542014-04-23 14:28:55 -0500124 super(PrimeProcessor.Platform, self).__init__()
125
Brian Silvermana29ebf92014-04-23 13:08:49 -0500126 self.architecture = architecture
127 self.compiler = compiler
128 self.debug = debug
129
130 def __repr__(self):
131 return 'PrimeProcessor.Platform(architecture=%s, compiler=%s, debug=%s)' \
132 % (self.architecture, self.compiler, self.debug)
133 def __str__(self):
134 return '%s-%s%s' % (self.architecture, self.compiler,
135 '-debug' if self.debug else '')
136
137 def os(self):
138 return 'linux'
139 def gyp_platform(self):
140 return '%s-%s-%s' % (self.os(), self.architecture, self.compiler)
141
Brian Silvermana29ebf92014-04-23 13:08:49 -0500142 def outname(self):
143 return str(self)
Brian Silvermana29ebf92014-04-23 13:08:49 -0500144
Brian Silvermane48c09a2014-04-30 18:04:58 -0700145 # TODO(brians): test this
Brian Silvermanbe6cfe22014-04-27 08:06:27 -0500146 def deploy(self, dry_run):
147 """Downloads code to the prime in a way that avoids clashing too badly with starter
148 """
149 SUM = 'md5sum'
150 TARGET_DIR = '/home/driver/robot_code/bin'
151 TEMP_DIR = '/tmp/aos_downloader'
152 TARGET = 'driver@' + get_ip('prime')
153
154 from_dir = os.path.join(self.outdir(), 'outputs')
155 sums = subprocess.check_output((SUM,) + tuple(os.listdir(from_dir)),
156 stdin=open(os.devnull, 'r'),
157 cwd=from_dir)
158 to_download = subprocess.check_output(
159 ('ssh', TARGET,
160 """rm -rf {TMPDIR} && mkdir {TMPDIR} && cd {TO_DIR}
161 && echo '{SUMS}' | {SUM} --check --quiet
162 |& grep -F FAILED | sed 's/^\\(.*\\): FAILED.*"'$'"/\\1/g'""".format(
163 TMPDIR=TEMP_DIR, TO_DIR=TARGET_DIR, SUMS=sums, SUM=SUM)))
164 if not to_download:
Brian Silvermana9b1e5c2014-04-30 18:08:04 -0700165 user_output("Nothing to download")
Brian Silvermanbe6cfe22014-04-27 08:06:27 -0500166 return
167 self.do_deploy(
168 dry_run,
169 ('scp', '-o', 'Compression yes') + to_download
170 + (('%s:%s' % (TARGET, TEMP_DIR)),))
171 if not dry_run:
172 subprocess.check_call(
173 ('ssh', TARGET,
174 """mv {TMPDIR}/* {TO_DIR}
175 && echo 'Done moving new executables into place'
176 && ionice -c 3 bash -c 'sync && sync && sync'""".format(
177 TMPDIR=TEMP_DIR, TO_DIR=TARGET_DIR)))
178
Brian Silvermana29ebf92014-04-23 13:08:49 -0500179 ARCHITECTURES = ['arm', 'amd64']
180 COMPILERS = ['clang', 'gcc']
181
Brian Silvermanbe6cfe22014-04-27 08:06:27 -0500182 def __init__(self, is_test, is_deploy):
Brian Silvermana29ebf92014-04-23 13:08:49 -0500183 super(Processor, self).__init__()
184
185 platforms = []
186 for architecture in PrimeProcessor.ARCHITECTURES:
187 for compiler in PrimeProcessor.COMPILERS:
188 for debug in [True, False]:
189 platforms.append(
190 PrimeProcessor.Platform(architecture, compiler, debug))
191 self.platforms = frozenset(platforms)
Brian Silvermanbe6cfe22014-04-27 08:06:27 -0500192 if is_test:
Brian Silvermanc2d8e5a2014-05-01 18:33:12 -0700193 self.default_platforms = self.select_platforms(architecture='amd64',
194 debug=True)
Brian Silvermanbe6cfe22014-04-27 08:06:27 -0500195 elif is_deploy:
196 # TODO(brians): Switch to deploying the code built with clang.
Brian Silvermanc2d8e5a2014-05-01 18:33:12 -0700197 self.default_platforms = self.select_platforms(architecture='arm',
198 compiler='gcc',
199 debug=False)
Brian Silvermanbe6cfe22014-04-27 08:06:27 -0500200 else:
201 self.default_platforms = self.select_platforms(debug=False)
Brian Silvermana29ebf92014-04-23 13:08:49 -0500202
203 def build_env(self):
204 return {}
Brian Silvermanb3d50542014-04-23 14:28:55 -0500205 def extra_gyp_flags(self):
206 return ()
Brian Silvermana29ebf92014-04-23 13:08:49 -0500207 def is_crio(self): return False
208
209 def parse_platforms(self, string):
210 if string is None:
211 return self.default_platforms
212 r = self.default_platforms
213 for part in string.split(','):
214 if part[0] == '+':
215 r = r | self.select_platforms_string(part[1:])
216 elif part[0] == '-':
217 r = r - self.select_platforms_string(part[1:])
218 elif part[0] == '=':
219 r = self.select_platforms_string(part[1:])
220 else:
Brian Silverman7cd5ad42014-04-27 08:11:30 -0500221 selected = self.select_platforms_string(part)
222 r = r - (self.platforms - selected)
223 if not r:
224 r = selected
Brian Silvermana29ebf92014-04-23 13:08:49 -0500225 return r
226
227 def select_platforms(self, architecture=None, compiler=None, debug=None):
228 r = []
229 for platform in self.platforms:
230 if architecture is None or platform.architecture == architecture:
231 if compiler is None or platform.compiler == compiler:
232 if debug is None or platform.debug == debug:
233 r.append(platform)
234 return set(r)
235
236 def select_platforms_string(self, string):
237 r = []
238 architecture, compiler, debug = None, None, None
239 for part in string.split('-'):
240 if part in PrimeProcessor.ARCHITECTURES:
241 architecture = part
242 elif part in PrimeProcessor.COMPILERS:
243 compiler = part
244 elif part in ['debug', 'dbg']:
245 debug = True
246 elif part in ['release', 'nodebug', 'ndb']:
247 debug = False
248 else:
249 raise Processor.UnknownPlatform('Unknown platform string component "%s".' % part)
250 return self.select_platforms(
251 architecture=architecture,
252 compiler=compiler,
253 debug=debug)
254
Brian Silvermanc2d8e5a2014-05-01 18:33:12 -0700255 def check_installed(self):
256 self.do_check_installed(
257 ('clang-3.5', 'gcc-4.7-arm-linux-gnueabihf',
258 'g++-4.7-arm-linux-gnueabihf', 'openssh-client'))
259
Brian Silvermana29ebf92014-04-23 13:08:49 -0500260def main():
261 class TryParsingAgain(Exception):
262 pass
263
264 class TryAgainArgumentParser(argparse.ArgumentParser):
265 def __init__(self, **kwargs):
266 super(TryAgainArgumentParser, self).__init__(**kwargs)
267
268 def error(self, message):
269 raise TryParsingAgain
270
271 def SetUpParser(parser, args):
272 def AddBuildArgs(parser):
273 parser.add_argument(
274 'target',
275 help='target to build',
276 nargs='*')
277 def AddCommonArgs(parser):
278 parser.add_argument(
279 'platforms',
280 help='platform(s) to act on',
281 nargs='?')
282
283 parser.add_argument('--processor', required=True, help='prime or crio')
284 parser.add_argument('--main_gyp', required=True, help='main .gyp file')
285 subparsers = parser.add_subparsers(dest='action_name')
286
287 build_parser = subparsers.add_parser(
288 'build',
289 help='build the code (default)')
290 AddCommonArgs(build_parser)
291 AddBuildArgs(build_parser)
292
293 clean_parser = subparsers.add_parser(
294 'clean',
295 help='remove all output directories')
296 AddCommonArgs(clean_parser)
297
298 deploy_parser = subparsers.add_parser(
299 'deploy',
300 help='build and download the code')
301 AddCommonArgs(deploy_parser)
302 AddBuildArgs(deploy_parser)
303 deploy_parser.add_argument(
304 '-n', '--dry-run',
305 help="don't actually download anything",
306 action='store_true')
307
Brian Silvermane48c09a2014-04-30 18:04:58 -0700308 tests_parser = subparsers.add_parser(
309 'tests',
310 help='run tests')
311 AddCommonArgs(tests_parser)
312 AddBuildArgs(tests_parser)
313
Brian Silvermana29ebf92014-04-23 13:08:49 -0500314 return parser.parse_args(args)
315
316 try:
317 parser = TryAgainArgumentParser()
318 args = SetUpParser(parser, sys.argv[1:])
319 except TryParsingAgain:
320 parser = argparse.ArgumentParser()
321 REQUIRED_ARGS_END = 5
322 args = SetUpParser(parser, sys.argv[1:REQUIRED_ARGS_END] + ['build'] +
323 sys.argv[(REQUIRED_ARGS_END):])
324
325 if args.processor == 'crio':
326 processor = CRIOProcessor()
327 elif args.processor == 'prime':
Brian Silvermanbe6cfe22014-04-27 08:06:27 -0500328 processor = PrimeProcessor(args.action_name == 'tests',
329 args.action_name == 'deploy')
Brian Silvermana29ebf92014-04-23 13:08:49 -0500330 else:
331 parser.exit(status=1, message='Unknown processor "%s".' % args.processor)
Brian Silvermanc2d8e5a2014-05-01 18:33:12 -0700332 processor.check_installed()
Brian Silvermana29ebf92014-04-23 13:08:49 -0500333
334 if 'target' in args:
335 targets = args.target[:]
336 else:
337 targets = []
338 unknown_platform_error = None
339 try:
340 platforms = processor.parse_platforms(args.platforms)
341 except Processor.UnknownPlatform as e:
342 unknown_platform_error = e.message
343 targets.append(args.platforms)
344 platforms = processor.parse_platforms(None)
345 if not platforms:
Brian Silvermana9b1e5c2014-04-30 18:08:04 -0700346 user_output("No platforms selected!")
Brian Silvermana29ebf92014-04-23 13:08:49 -0500347 exit(1)
348
349 def download_externals(argument):
350 subprocess.check_call(
Brian Silvermanbe6cfe22014-04-27 08:06:27 -0500351 (os.path.join(aos_path(), 'build', 'download_externals.sh'),
Brian Silvermana29ebf92014-04-23 13:08:49 -0500352 argument),
Brian Silvermanbe6cfe22014-04-27 08:06:27 -0500353 stdin=open(os.devnull, 'r'))
Brian Silvermana29ebf92014-04-23 13:08:49 -0500354
355 if processor.is_crio():
356 download_externals('crio')
357 else:
358 for architecture in PrimeProcessor.ARCHITECTURES:
359 if platforms & processor.select_platforms(architecture=architecture):
360 download_externals(architecture)
361
362 class ToolsConfig(object):
363 def __init__(self):
Brian Silvermanbe6cfe22014-04-27 08:06:27 -0500364 self.variables = {'AOS': aos_path()}
365 with open(os.path.join(aos_path(), 'build', 'tools_config'), 'r') as f:
Brian Silvermana29ebf92014-04-23 13:08:49 -0500366 for line in f:
367 if line[0] == '#':
368 pass
369 elif line.isspace():
370 pass
371 else:
372 new_name, new_value = line.rstrip().split('=')
373 for name, value in self.variables.items():
374 new_value = new_value.replace('${%s}' % name, value)
375 self.variables[new_name] = new_value
376 def __getitem__(self, key):
377 return self.variables[key]
378
379 tools_config = ToolsConfig()
380
381 def handle_clean_error(function, path, excinfo):
382 if issubclass(OSError, excinfo[0]):
383 if excinfo[1].errno == errno.ENOENT:
384 # Who cares if the file we're deleting isn't there?
385 return
386 raise excinfo[1]
387
Brian Silvermanc2d8e5a2014-05-01 18:33:12 -0700388 def need_to_run_gyp_old(platform):
Brian Silvermana29ebf92014-04-23 13:08:49 -0500389 try:
390 build_mtime = os.stat(platform.build_ninja()).st_mtime
391 except OSError as e:
392 if e.errno == errno.ENOENT:
393 return True
394 else:
395 raise e
Brian Silvermane48c09a2014-04-30 18:04:58 -0700396 pattern = re.compile('.*\.gypi?$')
Brian Silvermanbe6cfe22014-04-27 08:06:27 -0500397 for dirname, _, files in os.walk(os.path.join(aos_path(), '..')):
Brian Silvermana29ebf92014-04-23 13:08:49 -0500398 for f in [f for f in files if pattern.match(f)]:
399 if (os.stat(os.path.join(dirname, f)).st_mtime > build_mtime):
400 return True
401 return False
402
Brian Silvermanc2d8e5a2014-05-01 18:33:12 -0700403 def need_to_run_gyp(platform):
404 dirs = os.listdir(os.path.join(aos_path(), '..'))
405 # Looking through output tends to take a long time.
406 dirs.remove('output')
407 return subprocess.call(
408 ('find',) + tuple(os.path.join(aos_path(), '..', d) for d in dirs)
409 + ('-newer', platform.build_ninja(),
410 '(', '-name', '*.gyp', '-or', '-name', '*.gypi', ')'),
411 stdin=open(os.devnull, 'r'), stdout=open(os.devnull, 'w')) != 0
412
413 num = 1
Brian Silvermana29ebf92014-04-23 13:08:49 -0500414 for platform in platforms:
Brian Silvermanc2d8e5a2014-05-01 18:33:12 -0700415 user_output('Building %s (%d/%d)...' % (platform, num, len(platforms)))
Brian Silvermana29ebf92014-04-23 13:08:49 -0500416 if args.action_name == 'clean':
417 shutil.rmtree(platform.outdir(), onerror=handle_clean_error)
418 else:
419 if need_to_run_gyp(platform):
Brian Silvermana9b1e5c2014-04-30 18:08:04 -0700420 user_output('Running gyp...')
Brian Silvermana29ebf92014-04-23 13:08:49 -0500421 gyp = subprocess.Popen(
422 (tools_config['GYP'],
423 '--check',
Brian Silvermanbe6cfe22014-04-27 08:06:27 -0500424 '--depth=%s' % os.path.join(aos_path(), '..'),
Brian Silvermana29ebf92014-04-23 13:08:49 -0500425 '--no-circular-check',
426 '-f', 'ninja',
Brian Silvermanbe6cfe22014-04-27 08:06:27 -0500427 '-I%s' % os.path.join(aos_path(), 'build', 'aos.gypi'),
Brian Silvermana29ebf92014-04-23 13:08:49 -0500428 '-I/dev/stdin', '-Goutput_dir=output',
429 '-DOS=%s' % platform.os(),
430 '-DPLATFORM=%s' % platform.gyp_platform(),
431 '-DARCHITECTURE=%s' % platform.architecture,
432 '-DCOMPILER=%s' % platform.compiler,
Brian Silvermanb3d50542014-04-23 14:28:55 -0500433 '-DDEBUG=%s' % ('yes' if platform.debug else 'no')) +
434 processor.extra_gyp_flags() + (args.main_gyp,),
Brian Silvermana29ebf92014-04-23 13:08:49 -0500435 stdin=subprocess.PIPE)
436 gyp.communicate(("""
437{
438 'target_defaults': {
439 'configurations': {
440 '%s': {}
441 }
442 }
443}""" % platform.outname()).encode())
444 if gyp.returncode:
Brian Silvermana9b1e5c2014-04-30 18:08:04 -0700445 user_output("Running gyp failed!")
Brian Silvermana29ebf92014-04-23 13:08:49 -0500446 exit(1)
Brian Silvermanb3d50542014-04-23 14:28:55 -0500447 if processor.is_crio():
448 subprocess.check_call(
449 ('sed', '-i',
450 's/nm -gD/nm/g', platform.build_ninja()),
Brian Silvermanbe6cfe22014-04-27 08:06:27 -0500451 stdin=open(os.devnull, 'r'))
Brian Silvermana9b1e5c2014-04-30 18:08:04 -0700452 user_output('Done running gyp.')
Brian Silvermana29ebf92014-04-23 13:08:49 -0500453 else:
Brian Silvermana9b1e5c2014-04-30 18:08:04 -0700454 user_output("Not running gyp.")
Brian Silvermana29ebf92014-04-23 13:08:49 -0500455
456 try:
Brian Silvermanb3d50542014-04-23 14:28:55 -0500457 build_env = dict(processor.build_env())
458 build_env['TERM'] = os.environ['TERM']
459 build_env['PATH'] = os.environ['PATH']
Brian Silvermana29ebf92014-04-23 13:08:49 -0500460 subprocess.check_call(
461 (tools_config['NINJA'],
462 '-C', platform.outdir()) + tuple(targets),
Brian Silvermanbe6cfe22014-04-27 08:06:27 -0500463 stdin=open(os.devnull, 'r'),
Brian Silvermanb3d50542014-04-23 14:28:55 -0500464 env=build_env)
Brian Silvermana29ebf92014-04-23 13:08:49 -0500465 except subprocess.CalledProcessError as e:
466 if unknown_platform_error is not None:
Brian Silvermana9b1e5c2014-04-30 18:08:04 -0700467 user_output(unknown_platform_error)
Brian Silvermana29ebf92014-04-23 13:08:49 -0500468 raise e
469
Brian Silvermanbe6cfe22014-04-27 08:06:27 -0500470 if args.action_name == 'deploy':
471 platform.deploy(args.dry_run)
Brian Silvermane48c09a2014-04-30 18:04:58 -0700472 elif args.action_name == 'tests':
473 dirname = os.path.join(platform.outdir(), 'tests')
474 for f in targets or os.listdir(dirname):
Brian Silvermana9b1e5c2014-04-30 18:08:04 -0700475 user_output('Running test %s...' % f)
Brian Silvermane48c09a2014-04-30 18:04:58 -0700476 subprocess.check_call(os.path.join(dirname, f))
Brian Silvermana9b1e5c2014-04-30 18:08:04 -0700477 user_output('Test %s succeeded' % f)
Brian Silvermanbe6cfe22014-04-27 08:06:27 -0500478
Brian Silvermanc2d8e5a2014-05-01 18:33:12 -0700479 user_output('Done building %s (%d/%d)' % (platform, num, len(platforms)))
480 num += 1
Brian Silvermana29ebf92014-04-23 13:08:49 -0500481
482if __name__ == '__main__':
483 main()