app.py 30 KB

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