app.py 23 KB

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