app.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854
  1. #!/usr/bin/env python3
  2. import os
  3. import subprocess
  4. import json
  5. import pathlib
  6. import shutil
  7. import glob
  8. import time
  9. import fcntl
  10. import hashlib
  11. import fnmatch
  12. from distutils.dir_util import copy_tree
  13. from flask import Flask, render_template, request, send_from_directory, render_template_string, jsonify, redirect
  14. from threading import Thread, Lock
  15. import sys
  16. import re
  17. import requests
  18. # run at lower priority
  19. os.nice(20)
  20. #BOARDS = [ 'BeastF7', 'BeastH7' ]
  21. appdir = os.path.dirname(__file__)
  22. builds_dict = {}
  23. class Vehicle:
  24. def __init__(self, name, dir):
  25. self.name = name
  26. self.dir = dir
  27. # create vehicle objects
  28. copter = Vehicle('Copter', 'ArduCopter')
  29. plane = Vehicle('Plane', 'ArduPlane')
  30. rover = Vehicle('Rover', 'Rover')
  31. sub = Vehicle('Sub', 'ArduSub')
  32. tracker = Vehicle('AntennaTracker', 'AntennaTracker')
  33. blimp = Vehicle('Blimp', 'Blimp')
  34. heli = Vehicle('Heli', 'ArduCopter')
  35. VEHICLES = [copter, plane, rover, sub, tracker, blimp, heli]
  36. default_vehicle = copter
  37. # Note: Current implementation of BRANCHES means we can't have multiple branches with the same name even if they're in different remote repos.
  38. # Branch names (the git branch name not the Label) also cannot contain anything not valid in folder names.
  39. # the first branch in this list is always the default branch
  40. BRANCHES = [
  41. {
  42. 'full_name' : 'upstream/master',
  43. 'label' : 'Latest',
  44. 'allowed_vehicles' : [copter, plane, rover, sub, tracker, blimp, heli],
  45. 'artifacts_dir' : '/latest',
  46. },
  47. {
  48. 'full_name' : 'upstream/Copter-4.5',
  49. 'label' : 'Copter 4.5 stable',
  50. 'allowed_vehicles' : [copter, heli],
  51. 'artifacts_dir' : '/stable-4.5.0',
  52. },
  53. {
  54. 'full_name' : 'upstream/Plane-4.5',
  55. 'label' : 'Plane 4.5 stable',
  56. 'allowed_vehicles' : [plane],
  57. 'artifacts_dir' : '/stable-4.5.0',
  58. },
  59. {
  60. 'full_name' : 'upstream/Rover-4.5',
  61. 'label' : 'Rover 4.5 stable',
  62. 'allowed_vehicles' : [rover],
  63. 'artifacts_dir' : '/stable-4.5.0',
  64. },
  65. {
  66. 'full_name' : 'upstream/Tracker-4.5',
  67. 'label' : 'Tracker 4.5 stable',
  68. 'allowed_vehicles' : [tracker],
  69. 'artifacts_dir' : '/stable-4.5.0',
  70. },
  71. {
  72. 'full_name' : 'upstream/Sub-4.5',
  73. 'label' : 'Sub 4.5 beta',
  74. 'allowed_vehicles' : [sub],
  75. 'artifacts_dir' : '/beta',
  76. },
  77. {
  78. 'full_name' : 'upstream/Plane-4.4',
  79. 'label' : 'Plane 4.4 stable',
  80. 'allowed_vehicles' : [plane],
  81. 'artifacts_dir' : '/stable-4.4.4',
  82. },
  83. {
  84. 'full_name' : 'upstream/Copter-4.4',
  85. 'label' : 'Copter 4.4 stable',
  86. 'allowed_vehicles' : [copter, heli],
  87. 'artifacts_dir' : '/stable-4.4.4',
  88. },
  89. {
  90. 'full_name' : 'upstream/Rover-4.4',
  91. 'label' : 'Rover 4.4 stable',
  92. 'allowed_vehicles' : [rover],
  93. 'artifacts_dir' : '/stable-4.4.4',
  94. },
  95. {
  96. 'full_name' : 'upstream/Plane-4.3',
  97. 'label' : 'Plane 4.3 stable',
  98. 'allowed_vehicles' : [plane],
  99. 'artifacts_dir' : '/stable-4.3.8',
  100. },
  101. {
  102. 'full_name' : 'upstream/Copter-4.3',
  103. 'label' : 'Copter 4.3 stable',
  104. 'allowed_vehicles' : [copter, heli],
  105. 'artifacts_dir' : '/stable-4.3.8',
  106. },
  107. {
  108. 'full_name' : 'upstream/Rover-4.3',
  109. 'label' : 'Rover 4.3 stable',
  110. 'allowed_vehicles' : [rover],
  111. 'artifacts_dir' : '/stable-4.3.8',
  112. },
  113. ]
  114. default_branch = BRANCHES[0]
  115. def get_vehicle_names():
  116. return sorted([vehicle.name for vehicle in VEHICLES])
  117. def get_default_vehicle_name():
  118. return default_vehicle.name
  119. def get_branch_names():
  120. return sorted([branch['full_name'] for branch in BRANCHES])
  121. def get_branches():
  122. return sorted(BRANCHES, key=lambda x: x['full_name'])
  123. def get_default_branch_name():
  124. return default_branch['full_name']
  125. # LOCKS
  126. queue_lock = Lock()
  127. head_lock = Lock() # lock git HEAD, i.e., no branch change until this lock is released
  128. def is_valid_vehicle(vehicle_name):
  129. return vehicle_name in get_vehicle_names()
  130. def is_valid_branch(branch_name):
  131. return branch_name in get_branch_names()
  132. def run_git(cmd, cwd):
  133. app.logger.info("Running git: %s" % ' '.join(cmd))
  134. return subprocess.run(cmd, cwd=cwd, shell=False)
  135. def get_git_hash(branch):
  136. app.logger.info("Running git rev-parse %s in %s" % (branch, sourcedir))
  137. return subprocess.check_output(['git', 'rev-parse', branch], cwd=sourcedir, encoding='utf-8', shell=False).rstrip()
  138. def on_branch(branch):
  139. git_hash_target = get_git_hash(branch)
  140. app.logger.info("Expected branch git-hash '%s'" % git_hash_target)
  141. git_hash_current = get_git_hash('HEAD')
  142. app.logger.info("Current branch git-hash '%s'" % git_hash_current)
  143. return git_hash_target == git_hash_current
  144. def delete_branch(branch_name, s_dir):
  145. run_git(['git', 'checkout', get_default_branch_name()], cwd=s_dir) # to make sure we are not already on branch to be deleted
  146. run_git(['git', 'branch', '-D', branch_name], cwd=s_dir) # delete branch
  147. def checkout_branch(targetBranch, s_dir, fetch_and_reset=False, temp_branch_name=None):
  148. '''checkout to given branch and return the git hash'''
  149. # Note: remember to acquire head_lock before calling this method
  150. if not is_valid_branch(targetBranch):
  151. app.logger.error("Checkout requested for an invalid branch")
  152. return None
  153. remote = targetBranch.split('/', 1)[0]
  154. if not on_branch(targetBranch):
  155. app.logger.info("Checking out to %s branch" % targetBranch)
  156. run_git(['git', 'checkout', targetBranch], cwd=s_dir)
  157. if fetch_and_reset:
  158. run_git(['git', 'fetch', remote], cwd=s_dir)
  159. run_git(['git', 'reset', '--hard', targetBranch], cwd=s_dir)
  160. if temp_branch_name is not None:
  161. delete_branch(temp_branch_name, s_dir=s_dir) # delete temp branch if it already exists
  162. run_git(['git', 'checkout', '-b', temp_branch_name, targetBranch], cwd=s_dir) # creates new temp branch
  163. git_hash = get_git_hash('HEAD')
  164. return git_hash
  165. def clone_branch(targetBranch, sourcedir, out_dir, temp_branch_name):
  166. # check if target branch is a valid branch
  167. if not is_valid_branch(targetBranch):
  168. return False
  169. remove_directory_recursive(out_dir)
  170. head_lock.acquire()
  171. checkout_branch(targetBranch, s_dir=sourcedir, fetch_and_reset=True, temp_branch_name=temp_branch_name)
  172. output = run_git(['git', 'clone', '--single-branch', '--branch='+temp_branch_name, sourcedir, out_dir], cwd=sourcedir)
  173. delete_branch(temp_branch_name, sourcedir) # delete temp branch
  174. head_lock.release()
  175. return output.returncode == 0
  176. def get_boards_from_ardupilot_tree(s_dir):
  177. '''return a list of boards to build'''
  178. tstart = time.time()
  179. import importlib.util
  180. spec = importlib.util.spec_from_file_location("board_list.py",
  181. os.path.join(s_dir,
  182. 'Tools', 'scripts',
  183. 'board_list.py'))
  184. mod = importlib.util.module_from_spec(spec)
  185. spec.loader.exec_module(mod)
  186. all_boards = mod.AUTOBUILD_BOARDS
  187. exclude_patterns = [ 'fmuv*', 'SITL*' ]
  188. boards = []
  189. for b in all_boards:
  190. excluded = False
  191. for p in exclude_patterns:
  192. if fnmatch.fnmatch(b.lower(), p.lower()):
  193. excluded = True
  194. break
  195. if not excluded:
  196. boards.append(b)
  197. app.logger.info('Took %f seconds to get boards' % (time.time() - tstart))
  198. boards.sort()
  199. default_board = boards[0]
  200. return (boards, default_board)
  201. def get_build_options_from_ardupilot_tree(s_dir):
  202. '''return a list of build options'''
  203. tstart = time.time()
  204. import importlib.util
  205. spec = importlib.util.spec_from_file_location(
  206. "build_options.py",
  207. os.path.join(s_dir, 'Tools', 'scripts', 'build_options.py'))
  208. mod = importlib.util.module_from_spec(spec)
  209. spec.loader.exec_module(mod)
  210. app.logger.info('Took %f seconds to get build options' % (time.time() - tstart))
  211. return mod.BUILD_OPTIONS
  212. from logging.config import dictConfig
  213. dictConfig({
  214. 'version': 1,
  215. 'formatters': {'default': {
  216. 'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
  217. }},
  218. 'handlers': {'wsgi': {
  219. 'class': 'logging.StreamHandler',
  220. 'stream': 'ext://flask.logging.wsgi_errors_stream',
  221. 'formatter': 'default'
  222. }},
  223. 'root': {
  224. 'level': 'INFO',
  225. 'handlers': ['wsgi']
  226. }
  227. })
  228. def remove_directory_recursive(dirname):
  229. '''remove a directory recursively'''
  230. app.logger.info('Removing directory ' + dirname)
  231. if not os.path.exists(dirname):
  232. return
  233. f = pathlib.Path(dirname)
  234. if f.is_file():
  235. f.unlink()
  236. else:
  237. shutil.rmtree(f, True)
  238. def create_directory(dir_path):
  239. '''create a directory, don't fail if it exists'''
  240. app.logger.info('Creating ' + dir_path)
  241. pathlib.Path(dir_path).mkdir(parents=True, exist_ok=True)
  242. def run_build(task, tmpdir, outdir, logpath):
  243. '''run a build with parameters from task'''
  244. remove_directory_recursive(tmpdir_parent)
  245. create_directory(tmpdir)
  246. # clone target branch in temporary source directory
  247. tmp_src_dir = os.path.join(tmpdir, 'build_src')
  248. clone_branch(task['branch'], sourcedir, tmp_src_dir, task['branch']+'_clone')
  249. # update submodules in temporary source directory
  250. update_submodules(tmp_src_dir)
  251. if not os.path.isfile(os.path.join(outdir, 'extra_hwdef.dat')):
  252. app.logger.error('Build aborted, missing extra_hwdef.dat')
  253. app.logger.info('Appending to build.log')
  254. with open(logpath, 'a') as log:
  255. log.write('Setting vehicle to: ' + task['vehicle'].capitalize() + '\n')
  256. log.flush()
  257. # setup PATH to point at our compiler
  258. env = os.environ.copy()
  259. bindir1 = os.path.abspath(os.path.join(appdir, "..", "bin"))
  260. bindir2 = os.path.abspath(os.path.join(appdir, "..", "gcc", "bin"))
  261. cachedir = os.path.abspath(os.path.join(appdir, "..", "cache"))
  262. env["PATH"] = bindir1 + ":" + bindir2 + ":" + env["PATH"]
  263. env['CCACHE_DIR'] = cachedir
  264. app.logger.info('Running waf configure')
  265. log.write('Running waf configure\n')
  266. log.flush()
  267. subprocess.run(['python3', './waf', 'configure',
  268. '--board', task['board'],
  269. '--out', tmpdir,
  270. '--extra-hwdef', task['extra_hwdef']],
  271. cwd = tmp_src_dir,
  272. env=env,
  273. stdout=log, stderr=log, shell=False)
  274. app.logger.info('Running clean')
  275. log.write('Running clean\n')
  276. log.flush()
  277. subprocess.run(['python3', './waf', 'clean'],
  278. cwd = tmp_src_dir,
  279. env=env,
  280. stdout=log, stderr=log, shell=False)
  281. app.logger.info('Running build')
  282. log.write('Running build\n')
  283. log.flush()
  284. subprocess.run(['python3', './waf', task['vehicle']],
  285. cwd = tmp_src_dir,
  286. env=env,
  287. stdout=log, stderr=log, shell=False)
  288. log.write('done build\n')
  289. log.flush()
  290. def sort_json_files(reverse=False):
  291. json_files = list(filter(os.path.isfile,
  292. glob.glob(os.path.join(outdir_parent,
  293. '*', 'q.json'))))
  294. json_files.sort(key=lambda x: os.path.getmtime(x), reverse=reverse)
  295. return json_files
  296. def check_queue():
  297. '''thread to continuously run queued builds'''
  298. queue_lock.acquire()
  299. json_files = sort_json_files()
  300. queue_lock.release()
  301. if len(json_files) == 0:
  302. return
  303. # remove multiple build requests from same ip address (keep newest)
  304. queue_lock.acquire()
  305. ip_list = []
  306. for f in json_files:
  307. file = json.loads(open(f).read())
  308. ip_list.append(file['ip'])
  309. seen = set()
  310. ip_list.reverse()
  311. for index, value in enumerate(ip_list):
  312. if value in seen:
  313. file = json.loads(open(json_files[-index-1]).read())
  314. outdir_to_delete = os.path.join(outdir_parent, file['token'])
  315. remove_directory_recursive(outdir_to_delete)
  316. else:
  317. seen.add(value)
  318. queue_lock.release()
  319. if len(json_files) == 0:
  320. return
  321. # open oldest q.json file
  322. json_files = sort_json_files()
  323. taskfile = json_files[0]
  324. app.logger.info('Opening ' + taskfile)
  325. task = json.loads(open(taskfile).read())
  326. app.logger.info('Removing ' + taskfile)
  327. os.remove(taskfile)
  328. outdir = os.path.join(outdir_parent, task['token'])
  329. tmpdir = os.path.join(tmpdir_parent, task['token'])
  330. logpath = os.path.abspath(os.path.join(outdir, 'build.log'))
  331. app.logger.info("LOGPATH: %s" % logpath)
  332. try:
  333. # run build and rename build directory
  334. app.logger.info('MIR: Running build ' + str(task))
  335. run_build(task, tmpdir, outdir, logpath)
  336. app.logger.info('Copying build files from %s to %s',
  337. os.path.join(tmpdir, task['board']),
  338. outdir)
  339. copy_tree(os.path.join(tmpdir, task['board'], 'bin'), outdir)
  340. app.logger.info('Build successful!')
  341. remove_directory_recursive(tmpdir)
  342. except Exception as ex:
  343. app.logger.info('Build failed: ', ex)
  344. pass
  345. open(logpath,'a').write("\nBUILD_FINISHED\n")
  346. def file_age(fname):
  347. '''return file age in seconds'''
  348. return time.time() - os.stat(fname).st_mtime
  349. def remove_old_builds():
  350. '''as a cleanup, remove any builds older than 24H'''
  351. for f in os.listdir(outdir_parent):
  352. bdir = os.path.join(outdir_parent, f)
  353. if os.path.isdir(bdir) and file_age(bdir) > 24 * 60 * 60:
  354. remove_directory_recursive(bdir)
  355. time.sleep(5)
  356. def queue_thread():
  357. while True:
  358. try:
  359. check_queue()
  360. remove_old_builds()
  361. except Exception as ex:
  362. app.logger.error('Failed queue: ', ex)
  363. pass
  364. def get_build_progress(build_id, build_status):
  365. '''return build progress on scale of 0 to 100'''
  366. if build_status in ['Pending', 'Error']:
  367. return 0
  368. if build_status == 'Finished':
  369. return 100
  370. log_file_path = os.path.join(outdir_parent,build_id,'build.log')
  371. app.logger.info('Opening ' + log_file_path)
  372. build_log = open(log_file_path, encoding='utf-8').read()
  373. compiled_regex = re.compile(r'(\[\D*(\d+)\D*\/\D*(\d+)\D*\])')
  374. all_matches = compiled_regex.findall(build_log)
  375. if (len(all_matches) < 1):
  376. return 0
  377. completed_steps, total_steps = all_matches[-1][1:]
  378. if (int(total_steps) < 20):
  379. # these steps are just little compilation and linking that happen at initialisation
  380. # these do not contribute significant percentage to overall build progress
  381. return 1
  382. if (int(total_steps) < 200):
  383. # these steps are for building the OS
  384. # we give this phase 4% weight in the whole build progress
  385. return (int(completed_steps) * 4 // int(total_steps)) + 1
  386. # these steps are the major part of the build process
  387. # we give 95% of weight to these
  388. return (int(completed_steps) * 95 // int(total_steps)) + 5
  389. def get_build_status(build_id):
  390. build_id_split = build_id.split(':')
  391. if len(build_id_split) < 2:
  392. raise Exception('Invalid build id')
  393. if os.path.exists(os.path.join(outdir_parent,build_id,'q.json')):
  394. status = "Pending"
  395. elif not os.path.exists(os.path.join(outdir_parent,build_id,'build.log')):
  396. status = "Error"
  397. else:
  398. log_file_path = os.path.join(outdir_parent,build_id,'build.log')
  399. app.logger.info('Opening ' + log_file_path)
  400. build_log = open(log_file_path, encoding='utf-8').read()
  401. if build_log.find("'%s' finished successfully" % build_id_split[0].lower()) != -1:
  402. status = "Finished"
  403. elif build_log.find('The configuration failed') != -1 or build_log.find('Build failed') != -1 or build_log.find('compilation terminated') != -1:
  404. status = "Failed"
  405. elif build_log.find('BUILD_FINISHED') == -1:
  406. status = "Running"
  407. else:
  408. status = "Failed"
  409. return status
  410. def update_build_dict():
  411. '''update the build_dict dictionary which keeps track of status of all builds'''
  412. global builds_dict
  413. # get list of directories
  414. blist = []
  415. for b in os.listdir(outdir_parent):
  416. if os.path.isdir(os.path.join(outdir_parent,b)):
  417. blist.append(b)
  418. #remove deleted builds from build_dict
  419. for build in builds_dict:
  420. if build not in blist:
  421. builds_dict.pop(build, None)
  422. for b in blist:
  423. build_id_split = b.split(':')
  424. if len(build_id_split) < 2:
  425. continue
  426. build_info = builds_dict.get(b, None)
  427. # add an entry for the build in build_dict if not exists
  428. if (build_info is None):
  429. build_info = {}
  430. build_info['vehicle'] = build_id_split[0].capitalize()
  431. build_info['board'] = build_id_split[1]
  432. feature_file = os.path.join(outdir_parent, b, 'selected_features.json')
  433. app.logger.info('Opening ' + feature_file)
  434. selected_features_dict = json.loads(open(feature_file).read())
  435. selected_features = selected_features_dict['selected_features']
  436. build_info['git_hash_short'] = selected_features_dict['git_hash_short']
  437. features = ''
  438. for feature in selected_features:
  439. if features == '':
  440. features = features + feature
  441. else:
  442. features = features + ", " + feature
  443. build_info['features'] = features
  444. age_min = int(file_age(os.path.join(outdir_parent,b))/60.0)
  445. build_info['age'] = "%u:%02u" % ((age_min // 60), age_min % 60)
  446. # refresh build status only if it was pending, running or not initialised
  447. if (build_info.get('status', None) in ['Pending', 'Running', None]):
  448. build_info['status'] = get_build_status(b)
  449. build_info['progress'] = get_build_progress(b, build_info['status'])
  450. # update dictionary entry
  451. builds_dict[b] = build_info
  452. temp_list = sorted(list(builds_dict.items()), key=lambda x: os.path.getmtime(os.path.join(outdir_parent,x[0])), reverse=True)
  453. builds_dict = {ele[0] : ele[1] for ele in temp_list}
  454. def create_status():
  455. '''create status.json'''
  456. global builds_dict
  457. update_build_dict()
  458. tmpfile = os.path.join(outdir_parent, "status.tmp")
  459. statusfile = os.path.join(outdir_parent, "status.json")
  460. json_object = json.dumps(builds_dict)
  461. with open(tmpfile, "w") as outfile:
  462. outfile.write(json_object)
  463. os.replace(tmpfile, statusfile)
  464. def status_thread():
  465. while True:
  466. try:
  467. create_status()
  468. except Exception as ex:
  469. app.logger.info(ex)
  470. pass
  471. time.sleep(3)
  472. def update_submodules(s_dir):
  473. if not os.path.exists(s_dir):
  474. return
  475. app.logger.info('Updating submodules')
  476. run_git(['git', 'submodule', 'update', '--recursive', '--force', '--init'], cwd=s_dir)
  477. import optparse
  478. parser = optparse.OptionParser("app.py")
  479. parser.add_option("", "--basedir", type="string",
  480. default=os.path.abspath(os.path.join(os.path.dirname(__file__),"..","base")),
  481. help="base directory")
  482. cmd_opts, cmd_args = parser.parse_args()
  483. # define directories
  484. basedir = os.path.abspath(cmd_opts.basedir)
  485. sourcedir = os.path.join(basedir, 'ardupilot')
  486. outdir_parent = os.path.join(basedir, 'builds')
  487. tmpdir_parent = os.path.join(basedir, 'tmp')
  488. app = Flask(__name__, template_folder='templates')
  489. if not os.path.isdir(outdir_parent):
  490. create_directory(outdir_parent)
  491. try:
  492. lock_file = open(os.path.join(basedir, "queue.lck"), "w")
  493. fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
  494. app.logger.info("Got queue lock")
  495. # we only want one set of threads
  496. thread = Thread(target=queue_thread, args=())
  497. thread.daemon = True
  498. thread.start()
  499. status_thread = Thread(target=status_thread, args=())
  500. status_thread.daemon = True
  501. status_thread.start()
  502. except IOError:
  503. app.logger.info("No queue lock")
  504. app.logger.info('Initial fetch')
  505. # checkout to default branch, fetch remote, update submodules
  506. checkout_branch(get_default_branch_name(), s_dir=sourcedir, fetch_and_reset=True)
  507. update_submodules(s_dir=sourcedir)
  508. app.logger.info('Python version is: %s' % sys.version)
  509. @app.route('/generate', methods=['GET', 'POST'])
  510. def generate():
  511. try:
  512. chosen_branch = request.form['branch']
  513. if not is_valid_branch(chosen_branch):
  514. raise Exception("bad branch")
  515. chosen_vehicle = request.form['vehicle']
  516. if not is_valid_vehicle(chosen_vehicle):
  517. raise Exception("bad vehicle")
  518. chosen_board = request.form['board']
  519. head_lock.acquire()
  520. checkout_branch(targetBranch=chosen_branch, s_dir=sourcedir)
  521. if chosen_board not in get_boards_from_ardupilot_tree(s_dir=sourcedir)[0]:
  522. raise Exception("bad board")
  523. #ToDo - maybe have the if-statement to check if it's changed.
  524. build_options = get_build_options_from_ardupilot_tree(s_dir=sourcedir)
  525. head_lock.release()
  526. # fetch features from user input
  527. extra_hwdef = []
  528. feature_list = []
  529. selected_features = []
  530. app.logger.info('Fetching features from user input')
  531. # add all undefs at the start
  532. for f in build_options:
  533. extra_hwdef.append('undef %s' % f.define)
  534. for f in build_options:
  535. if f.label not in request.form or request.form[f.label] != '1':
  536. extra_hwdef.append('define %s 0' % f.define)
  537. else:
  538. extra_hwdef.append('define %s 1' % f.define)
  539. feature_list.append(f.description)
  540. selected_features.append(f.label)
  541. extra_hwdef = '\n'.join(extra_hwdef)
  542. spaces = '\n'
  543. feature_list = spaces.join(feature_list)
  544. selected_features_dict = {}
  545. selected_features_dict['selected_features'] = selected_features
  546. queue_lock.acquire()
  547. # create extra_hwdef.dat file and obtain md5sum
  548. app.logger.info('Creating ' +
  549. os.path.join(outdir_parent, 'extra_hwdef.dat'))
  550. file = open(os.path.join(outdir_parent, 'extra_hwdef.dat'), 'w')
  551. app.logger.info('Writing\n' + extra_hwdef)
  552. file.write(extra_hwdef)
  553. file.close()
  554. extra_hwdef_md5sum = hashlib.md5(extra_hwdef.encode('utf-8')).hexdigest()
  555. app.logger.info('Removing ' +
  556. os.path.join(outdir_parent, 'extra_hwdef.dat'))
  557. os.remove(os.path.join(outdir_parent, 'extra_hwdef.dat'))
  558. new_git_hash = get_git_hash(chosen_branch)
  559. git_hash_short = new_git_hash[:10]
  560. app.logger.info('Git hash = ' + new_git_hash)
  561. selected_features_dict['git_hash_short'] = git_hash_short
  562. # create directories using concatenated token
  563. # of vehicle, board, git-hash of source, and md5sum of hwdef
  564. token = chosen_vehicle.lower() + ':' + chosen_board + ':' + new_git_hash + ':' + extra_hwdef_md5sum
  565. app.logger.info('token = ' + token)
  566. outdir = os.path.join(outdir_parent, token)
  567. if os.path.isdir(outdir):
  568. app.logger.info('Build already exists')
  569. else:
  570. create_directory(outdir)
  571. # create build.log
  572. build_log_info = ('Vehicle: ' + chosen_vehicle +
  573. '\nBoard: ' + chosen_board +
  574. '\nBranch: ' + chosen_branch +
  575. '\nSelected Features:\n' + feature_list +
  576. '\n\nWaiting for build to start...\n\n')
  577. app.logger.info('Creating build.log')
  578. build_log = open(os.path.join(outdir, 'build.log'), 'w')
  579. build_log.write(build_log_info)
  580. build_log.close()
  581. # create hwdef.dat
  582. app.logger.info('Opening ' +
  583. os.path.join(outdir, 'extra_hwdef.dat'))
  584. file = open(os.path.join(outdir, 'extra_hwdef.dat'),'w')
  585. app.logger.info('Writing\n' + extra_hwdef)
  586. file.write(extra_hwdef)
  587. file.close()
  588. # fill dictionary of variables and create json file
  589. task = {}
  590. task['token'] = token
  591. task['branch'] = chosen_branch
  592. task['extra_hwdef'] = os.path.join(outdir, 'extra_hwdef.dat')
  593. task['vehicle'] = chosen_vehicle.lower()
  594. task['board'] = chosen_board
  595. task['ip'] = request.remote_addr
  596. app.logger.info('Opening ' + os.path.join(outdir, 'q.json'))
  597. jfile = open(os.path.join(outdir, 'q.json'), 'w')
  598. app.logger.info('Writing task file to ' +
  599. os.path.join(outdir, 'q.json'))
  600. jfile.write(json.dumps(task, separators=(',\n', ': ')))
  601. jfile.close()
  602. # create selected_features.dat for status table
  603. feature_file = open(os.path.join(outdir, 'selected_features.json'), 'w')
  604. app.logger.info('Writing\n' + os.path.join(outdir, 'selected_features.json'))
  605. feature_file.write(json.dumps(selected_features_dict))
  606. feature_file.close()
  607. queue_lock.release()
  608. base_url = request.url_root
  609. app.logger.info(base_url)
  610. app.logger.info('Redirecting to /viewlog')
  611. return redirect('/viewlog/'+token)
  612. except Exception as ex:
  613. app.logger.error(ex)
  614. return render_template('error.html', ex=ex)
  615. @app.route('/add_build')
  616. def add_build():
  617. app.logger.info('Rendering add_build.html')
  618. return render_template('add_build.html',
  619. get_vehicle_names=get_vehicle_names,
  620. get_default_vehicle_name=get_default_vehicle_name)
  621. def filter_build_options_by_category(build_options, category):
  622. return sorted([f for f in build_options if f.category == category], key=lambda x: x.description.lower())
  623. def parse_build_categories(build_options):
  624. return sorted(list(set([f.category for f in build_options])))
  625. @app.route('/', defaults={'token': None}, methods=['GET'])
  626. @app.route('/viewlog/<token>', methods=['GET'])
  627. def home(token):
  628. if token:
  629. app.logger.info("Showing log for build id " + token)
  630. app.logger.info('Rendering index.html')
  631. return render_template('index.html', token=token)
  632. @app.route("/builds/<path:name>")
  633. def download_file(name):
  634. app.logger.info('Downloading %s' % name)
  635. return send_from_directory(os.path.join(basedir,'builds'), name, as_attachment=False)
  636. @app.route("/boards_and_features/<string:remote>/<string:branch_name>", methods = ['GET'])
  637. def boards_and_features(remote, branch_name):
  638. branch = remote + '/' + branch_name
  639. if not is_valid_branch(branch):
  640. app.logger.error("Bad branch")
  641. return ("Bad branch", 400)
  642. app.logger.info('Board list and build options requested for %s' % branch)
  643. # getting board list for the branch
  644. head_lock.acquire()
  645. checkout_branch(targetBranch=branch, s_dir=sourcedir)
  646. (boards, default_board) = get_boards_from_ardupilot_tree(s_dir=sourcedir)
  647. options = get_build_options_from_ardupilot_tree(s_dir=sourcedir) # this is a list of Feature() objects defined in build_options.py
  648. head_lock.release()
  649. # parse the set of categories from these objects
  650. categories = parse_build_categories(options)
  651. features = []
  652. for category in categories:
  653. filtered_options = filter_build_options_by_category(options, category)
  654. category_options = [] # options belonging to a given category
  655. for option in filtered_options:
  656. category_options.append({
  657. 'label' : option.label,
  658. 'description' : option.description,
  659. 'default' : option.default,
  660. 'define' : option.define,
  661. 'dependency' : option.dependency,
  662. })
  663. features.append({
  664. 'name' : category,
  665. 'options' : category_options,
  666. })
  667. # creating result dictionary
  668. result = {
  669. 'boards' : boards,
  670. 'default_board' : default_board,
  671. 'features' : features,
  672. }
  673. # return jsonified result dict
  674. return jsonify(result)
  675. @app.route("/get_allowed_branches/<string:vehicle_name>", methods=['GET'])
  676. def get_allowed_branches(vehicle_name):
  677. if not is_valid_vehicle(vehicle_name):
  678. valid_vehicles = get_vehicle_names()
  679. app.logger.error("Bad vehicle")
  680. return (f"Bad Vehicle. Expected {valid_vehicles}", 400)
  681. app.logger.info("Supported branches requested for %s" % vehicle_name)
  682. branches = []
  683. for branch in get_branches():
  684. if vehicle_name in [vehicle.name for vehicle in branch['allowed_vehicles']]:
  685. branches.append({
  686. 'full_name': branch['full_name'],
  687. 'label' : branch['label']
  688. })
  689. result = {
  690. 'branches' : branches,
  691. 'default_branch' : get_default_branch_name()
  692. }
  693. # return jsonified result dictionary
  694. return jsonify(result)
  695. def get_firmware_version(vehicle_name, branch):
  696. app.logger.info("Retrieving firmware version information for %s on branch: %s" % (vehicle_name, branch))
  697. dir = ""
  698. for vehicle in VEHICLES:
  699. if vehicle.name == vehicle_name:
  700. dir = vehicle.dir
  701. break
  702. if dir == "":
  703. raise Exception("Could not determine vehicle directory")
  704. head_lock.acquire()
  705. output = subprocess.check_output(['git', 'show', branch+':'+dir+'/version.h'], cwd=sourcedir, encoding='utf-8', shell=False).rstrip()
  706. head_lock.release()
  707. match = re.search('define.THISFIRMWARE[\s\S]+V([0-9]+.[0-9]+.[0-9]+)', output)
  708. if match is None:
  709. raise Exception("Failed to retrieve firmware version from version.h")
  710. firmware_version = match.group(1)
  711. return firmware_version
  712. def get_artifacts_dir(branch_full_name):
  713. for branch in BRANCHES:
  714. if branch_full_name == branch['full_name']:
  715. return branch['artifacts_dir']
  716. return ""
  717. @app.route("/get_defaults/<string:vehicle_name>/<string:remote>/<string:branch_name>/<string:board>", methods = ['GET'])
  718. def get_deafults(vehicle_name, remote, branch_name, board):
  719. if not remote == "upstream":
  720. app.logger.error("Defaults requested for remote '%s' which is not supported" % remote)
  721. return ("Bad remote. Only upstream is supported.", 400)
  722. branch = remote + '/' + branch_name
  723. if not is_valid_branch(branch):
  724. app.logger.error("Bad branch")
  725. return ("Bad branch", 400)
  726. if not is_valid_vehicle(vehicle_name):
  727. app.logger.error("Bad vehicle")
  728. return ("Bad Vehicle", 400)
  729. # Heli is built on copter
  730. if vehicle_name == "Heli":
  731. vehicle_name = "Copter"
  732. artifacts_dir = get_artifacts_dir(branch)
  733. if artifacts_dir == "":
  734. return ("Could not determine artifacts directory for given combination", 400)
  735. artifacts_dir = vehicle_name + artifacts_dir + "/" + board
  736. path = "https://firmware.ardupilot.org/"+artifacts_dir+"/features.txt"
  737. response = requests.get(path, timeout=30)
  738. if not response.status_code == 200:
  739. return ("Could not retrieve features.txt for given vehicle, branch and board combination (Status Code: %d, path: %s)" % (response.status_code, path), response.status_code)
  740. # split response by new line character to get a list of defines
  741. result = response.text.split('\n')
  742. # omit the last string as its always blank
  743. return jsonify(result[:-1])
  744. if __name__ == '__main__':
  745. app.run()