app.py 27 KB

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