app.py 31 KB

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