app.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  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. from distutils.dir_util import copy_tree
  10. from flask import Flask, render_template, request, url_for, send_from_directory, render_template_string
  11. from threading import Thread, Lock
  12. from filelock import FileLock
  13. # run at lower priority
  14. os.nice(20)
  15. #BOARDS = [ 'BeastF7', 'BeastH7' ]
  16. appdir = os.path.dirname(__file__)
  17. def get_boards():
  18. '''return a list of boards to build'''
  19. import importlib.util
  20. spec = importlib.util.spec_from_file_location("build_binaries.py",
  21. os.path.join(sourcedir,
  22. 'Tools', 'scripts',
  23. 'board_list.py'))
  24. mod = importlib.util.module_from_spec(spec)
  25. spec.loader.exec_module(mod)
  26. return mod.AUTOBUILD_BOARDS
  27. #return BOARDS
  28. # list of build options to offer
  29. BUILD_OPTIONS = [
  30. ('EKF2', 'HAL_NAVEKF2_AVAILABLE', 'Enable EKF2'),
  31. ('EKF3', 'HAL_NAVEKF3_AVAILABLE', 'Enable EKF3'),
  32. ('DSP', 'HAL_WITH_DSP', 'Enable DSP'),
  33. ('SPRAYER', 'HAL_SPRAYER_ENABLED', 'Enable Sprayer'),
  34. ('PARACHUTE', 'HAL_PARACHUTE_ENABLED', 'Enable Parachute'),
  35. ('MOUNT', 'HAL_MOUNT_ENABLED', 'Enable Mount'),
  36. ('HOTT_TELEM', 'HAL_HOTT_TELEM_ENABLED', 'Enable HoTT Telemetry'),
  37. ('BATTMON_FUEL', 'HAL_BATTMON_FUEL_ENABLE', 'Enable Fuel BatteryMonitor')
  38. ]
  39. VEHICLES = [ 'Copter', 'Plane', 'Rover', 'Sub' ]
  40. queue_lock = Lock()
  41. from logging.config import dictConfig
  42. dictConfig({
  43. 'version': 1,
  44. 'formatters': {'default': {
  45. 'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
  46. }},
  47. 'handlers': {'wsgi': {
  48. 'class': 'logging.StreamHandler',
  49. 'stream': 'ext://flask.logging.wsgi_errors_stream',
  50. 'formatter': 'default'
  51. }},
  52. 'root': {
  53. 'level': 'INFO',
  54. 'handlers': ['wsgi']
  55. }
  56. })
  57. #def get_template(filename):
  58. # return render_template(filename)
  59. def remove_directory_recursive(dirname):
  60. '''remove a directory recursively'''
  61. app.logger.info('Removing directory ' + dirname)
  62. if not os.path.exists(dirname):
  63. return
  64. f = pathlib.Path(dirname)
  65. if f.is_file():
  66. f.unlink()
  67. else:
  68. shutil.rmtree(f, True)
  69. def create_directory(dir_path):
  70. '''create a directory, don't fail if it exists'''
  71. app.logger.info('Creating ' + dir_path)
  72. pathlib.Path(dir_path).mkdir(parents=True, exist_ok=True)
  73. def run_build(task, tmpdir, outdir, logpath):
  74. '''run a build with parameters from task'''
  75. remove_directory_recursive(tmpdir_parent)
  76. create_directory(tmpdir)
  77. if not os.path.isfile(os.path.join(outdir, 'extra_hwdef.dat')):
  78. app.logger.error('Build aborted, missing extra_hwdef.dat')
  79. app.logger.info('Appending to build.log')
  80. with open(logpath, 'a') as log:
  81. # setup PATH to point at our compiler
  82. env = os.environ.copy()
  83. bindir = os.path.abspath(os.path.join(appdir, "..", "gcc", "bin"))
  84. cachedir = os.path.abspath(os.path.join(appdir, "..", "cache"))
  85. env["PATH"] = bindir + ":" + env["PATH"]
  86. env["CC"] = "ccache arm-none-eabi-gcc"
  87. env["CXX"] = "ccache arm-none-eabi-g++"
  88. env['CCACHE_DIR'] = cachedir
  89. app.logger.info('Running waf configure')
  90. subprocess.run(['python3', './waf', 'configure',
  91. '--board', task['board'],
  92. '--out', tmpdir,
  93. '--extra-hwdef', task['extra_hwdef']],
  94. cwd = task['sourcedir'],
  95. env=env,
  96. stdout=log, stderr=log)
  97. app.logger.info('Running clean')
  98. subprocess.run(['python3', './waf', 'clean'],
  99. cwd = task['sourcedir'],
  100. env=env,
  101. stdout=log, stderr=log)
  102. app.logger.info('Running build')
  103. subprocess.run(['python3', './waf', task['vehicle']],
  104. cwd = task['sourcedir'],
  105. env=env,
  106. stdout=log, stderr=log)
  107. def sort_json_files(reverse=False):
  108. json_files = list(filter(os.path.isfile,
  109. glob.glob(os.path.join(outdir_parent,
  110. '*', 'q.json'))))
  111. json_files.sort(key=lambda x: os.path.getmtime(x), reverse=reverse)
  112. return json_files
  113. def check_queue():
  114. '''thread to continuously run queued builds'''
  115. queue_lock.acquire()
  116. json_files = sort_json_files()
  117. queue_lock.release()
  118. if len(json_files) == 0:
  119. return
  120. # remove multiple build requests from same ip address (keep newest)
  121. queue_lock.acquire()
  122. ip_list = []
  123. for f in json_files:
  124. file = json.loads(open(f).read())
  125. ip_list.append(file['ip'])
  126. seen = set()
  127. ip_list.reverse()
  128. for index, value in enumerate(ip_list):
  129. if value in seen:
  130. file = json.loads(open(json_files[-index-1]).read())
  131. outdir_to_delete = os.path.join(outdir_parent, file['token'])
  132. remove_directory_recursive(outdir_to_delete)
  133. else:
  134. seen.add(value)
  135. queue_lock.release()
  136. if len(json_files) == 0:
  137. return
  138. # open oldest q.json file
  139. json_files = sort_json_files()
  140. taskfile = json_files[0]
  141. app.logger.info('Opening ' + taskfile)
  142. task = json.loads(open(taskfile).read())
  143. app.logger.info('Removing ' + taskfile)
  144. os.remove(taskfile)
  145. outdir = os.path.join(outdir_parent, task['token'])
  146. tmpdir = os.path.join(tmpdir_parent, task['token'])
  147. logpath = os.path.abspath(os.path.join(outdir, 'build.log'))
  148. app.logger.info("LOGPATH: %s" % logpath)
  149. try:
  150. # run build and rename build directory
  151. run_build(task, tmpdir, outdir, logpath)
  152. app.logger.info('Copying build files from %s to %s',
  153. os.path.join(tmpdir, task['board']),
  154. outdir)
  155. copy_tree(os.path.join(tmpdir, task['board'], 'bin'), outdir)
  156. app.logger.info('Build successful!')
  157. remove_directory_recursive(tmpdir)
  158. # remove extra_hwdef.dat and q.json
  159. app.logger.info('Removing ' +
  160. os.path.join(outdir, 'extra_hwdef.dat'))
  161. os.remove(os.path.join(outdir, 'extra_hwdef.dat'))
  162. except Exception as ex:
  163. app.logger.info('Build failed')
  164. app.logger.error(ex)
  165. pass
  166. open(logpath,'a').write("\nBUILD_FINISHED\n")
  167. def file_age(fname):
  168. '''return file age in seconds'''
  169. return time.time() - os.stat(fname).st_mtime
  170. def remove_old_builds():
  171. '''as a cleanup, remove any builds older than 24H'''
  172. for f in os.listdir(outdir_parent):
  173. bdir = os.path.join(outdir_parent, f)
  174. if os.path.isdir(bdir) and file_age(bdir) > 24 * 60 * 60:
  175. remove_directory_recursive(bdir)
  176. time.sleep(5)
  177. def queue_thread():
  178. while True:
  179. try:
  180. check_queue()
  181. remove_old_builds()
  182. except Exception as ex:
  183. app.logger.error(ex)('Failed queue: ', ex)
  184. pass
  185. def get_build_status():
  186. '''return build status tuple list
  187. returns tuples of form (status,age,board,vehicle,genlink)
  188. '''
  189. ret = []
  190. # get list of directories
  191. blist = []
  192. for b in os.listdir(outdir_parent):
  193. if os.path.isdir(os.path.join(outdir_parent,b)):
  194. blist.append(b)
  195. for b in blist:
  196. a = b.split(':')
  197. if len(a) < 2:
  198. continue
  199. vehicle = a[0]
  200. board = a[1]
  201. link = "/view?token=%s" % b
  202. age_min = int(file_age(os.path.join(outdir_parent,b))/60.0)
  203. age_str = "%u:%02u" % ((age_min // 60), age_min % 60)
  204. if os.path.exists(os.path.join(outdir_parent,b,'q.json')):
  205. status = "Pending"
  206. elif not os.path.exists(os.path.join(outdir_parent,b,'build.log')):
  207. status = "Error"
  208. else:
  209. build = open(os.path.join(outdir_parent,b,'build.log')).read()
  210. if build.find("'%s' finished successfully" % vehicle.lower()) != -1:
  211. status = "Finished"
  212. elif build.find('BUILD_FINISHED') == -1:
  213. status = "Running"
  214. else:
  215. status = "Failed"
  216. ret.append((status,age_str,board,vehicle,link))
  217. return ret
  218. def create_status():
  219. '''create status.html'''
  220. build_status = get_build_status()
  221. tmpfile = os.path.join(outdir_parent, "status.tmp")
  222. statusfile = os.path.join(outdir_parent, "status.html")
  223. f = open(tmpfile, "w")
  224. app2 = Flask("status")
  225. with app2.app_context():
  226. f.write(render_template_string(open(os.path.join(appdir, 'templates', 'status.html')).read(),
  227. build_status=build_status))
  228. f.close()
  229. os.replace(tmpfile, statusfile)
  230. def status_thread():
  231. while True:
  232. try:
  233. create_status()
  234. except Exception as ex:
  235. app.logger.info(ex)
  236. pass
  237. time.sleep(3)
  238. def update_source():
  239. '''update submodules and ardupilot git tree'''
  240. app.logger.info('Fetching ardupilot origin')
  241. subprocess.run(['git', 'fetch', 'upstream'],
  242. cwd=sourcedir)
  243. app.logger.info('Updating ardupilot git tree')
  244. subprocess.run(['git', 'reset', '--hard',
  245. 'upstream/master'],
  246. cwd=sourcedir)
  247. app.logger.info('Updating submodules')
  248. subprocess.run(['git', 'submodule',
  249. 'update', '--recursive',
  250. '--force', '--init'],
  251. cwd=sourcedir)
  252. import optparse
  253. parser = optparse.OptionParser("app.py")
  254. parser.add_option("", "--basedir", type="string",
  255. default=os.path.abspath(os.path.join(os.path.dirname(__file__),"..","base")),
  256. help="base directory")
  257. cmd_opts, cmd_args = parser.parse_args()
  258. # define directories
  259. basedir = os.path.abspath(cmd_opts.basedir)
  260. sourcedir = os.path.abspath(os.path.join(basedir, 'ardupilot'))
  261. outdir_parent = os.path.join(basedir, 'builds')
  262. tmpdir_parent = os.path.join(basedir, 'tmp')
  263. app = Flask(__name__, template_folder='templates')
  264. if not os.path.isdir(outdir_parent):
  265. create_directory(outdir_parent)
  266. if FileLock(os.path.join(basedir, "queue.lck")):
  267. # we only want one set of threads
  268. thread = Thread(target=queue_thread, args=())
  269. thread.daemon = True
  270. thread.start()
  271. status_thread = Thread(target=status_thread, args=())
  272. status_thread.daemon = True
  273. status_thread.start()
  274. @app.route('/generate', methods=['GET', 'POST'])
  275. def generate():
  276. try:
  277. update_source()
  278. # fetch features from user input
  279. extra_hwdef = []
  280. feature_list = []
  281. app.logger.info('Fetching features from user input')
  282. for (label, define, text) in BUILD_OPTIONS:
  283. if label not in request.form:
  284. continue
  285. extra_hwdef.append(request.form[label])
  286. if request.form[label][-1] == '1':
  287. feature_list.append(text)
  288. undefine = 'undef ' + define
  289. extra_hwdef.insert(0, undefine)
  290. extra_hwdef = '\n'.join(extra_hwdef)
  291. spaces = '\n' + ' '*len('Selected Features: ')
  292. feature_list = spaces.join(feature_list)
  293. queue_lock.acquire()
  294. # create extra_hwdef.dat file and obtain md5sum
  295. app.logger.info('Creating ' +
  296. os.path.join(outdir_parent, 'extra_hwdef.dat'))
  297. file = open(os.path.join(outdir_parent, 'extra_hwdef.dat'), 'w')
  298. app.logger.info('Writing\n' + extra_hwdef)
  299. file.write(extra_hwdef)
  300. file.close()
  301. app.logger.info('Getting md5sum')
  302. md5sum = subprocess.check_output(['md5sum',
  303. os.path.join(outdir_parent,
  304. 'extra_hwdef.dat')],
  305. encoding = 'utf-8')
  306. md5sum = md5sum[:len(md5sum)
  307. -(3+len(os.path.join(outdir_parent,
  308. 'extra_hwdef.dat')))]
  309. app.logger.info('md5sum = ' + md5sum)
  310. app.logger.info('Removing ' +
  311. os.path.join(outdir_parent, 'extra_hwdef.dat'))
  312. os.remove(os.path.join(outdir_parent, 'extra_hwdef.dat'))
  313. # obtain git-hash of source
  314. app.logger.info('Getting git hash')
  315. git_hash = subprocess.check_output(['git', 'rev-parse', 'HEAD'],
  316. cwd = sourcedir,
  317. encoding = 'utf-8')
  318. git_hash = git_hash[:len(git_hash)-1]
  319. app.logger.info('Git hash = ' + git_hash)
  320. # create directories using concatenated token
  321. # of vehicle, board, git-hash of source, and md5sum of hwdef
  322. vehicle = request.form['vehicle']
  323. if not vehicle in VEHICLES:
  324. raise Exception("bad vehicle")
  325. vehicle = vehicle.lower()
  326. board = request.form['board']
  327. if board not in get_boards():
  328. raise Exception("bad board")
  329. token = vehicle + ':' + board + ':' + git_hash + ':' + md5sum
  330. app.logger.info('token = ' + token)
  331. global outdir
  332. outdir = os.path.join(outdir_parent, token)
  333. if os.path.isdir(outdir):
  334. app.logger.info('Build already exists')
  335. else:
  336. create_directory(outdir)
  337. # create build.log
  338. build_log_info = ('Vehicle: ' + vehicle +
  339. '\nBoard: ' + board +
  340. '\nSelected Features: ' + feature_list +
  341. '\n\nWaiting for build to start...\n\n')
  342. app.logger.info('Creating build.log')
  343. build_log = open(os.path.join(outdir, 'build.log'), 'w')
  344. build_log.write(build_log_info)
  345. build_log.close()
  346. # create hwdef.dat
  347. app.logger.info('Opening ' +
  348. os.path.join(outdir, 'extra_hwdef.dat'))
  349. file = open(os.path.join(outdir, 'extra_hwdef.dat'),'w')
  350. app.logger.info('Writing\n' + extra_hwdef)
  351. file.write(extra_hwdef)
  352. file.close()
  353. # fill dictionary of variables and create json file
  354. task = {}
  355. task['token'] = token
  356. task['sourcedir'] = sourcedir
  357. task['extra_hwdef'] = os.path.join(outdir, 'extra_hwdef.dat')
  358. task['vehicle'] = vehicle
  359. task['board'] = board
  360. task['ip'] = request.remote_addr
  361. app.logger.info('Opening ' + os.path.join(outdir, 'q.json'))
  362. jfile = open(os.path.join(outdir, 'q.json'), 'w')
  363. app.logger.info('Writing task file to ' +
  364. os.path.join(outdir, 'q.json'))
  365. jfile.write(json.dumps(task, separators=(',\n', ': ')))
  366. jfile.close()
  367. queue_lock.release()
  368. base_url = request.url_root
  369. app.logger.info(base_url)
  370. app.logger.info('Rendering generate.html')
  371. return render_template('generate.html', token=token)
  372. except Exception as ex:
  373. app.logger.error(ex)
  374. return render_template('generate.html', error='Error occured')
  375. @app.route('/view', methods=['GET'])
  376. def view():
  377. '''view a build from status'''
  378. token=request.args['token']
  379. app.logger.info("viewing %s" % token)
  380. return render_template('generate.html', token=token)
  381. def get_build_options():
  382. return BUILD_OPTIONS
  383. def get_vehicles():
  384. return VEHICLES
  385. @app.route('/')
  386. def home():
  387. app.logger.info('Rendering index.html')
  388. return render_template('index.html',
  389. get_boards=get_boards,
  390. get_vehicles=get_vehicles,
  391. get_build_options=get_build_options)
  392. @app.route("/builds/<path:name>")
  393. def download_file(name):
  394. app.logger.info('Downloading %s' % name)
  395. return send_from_directory(os.path.join(basedir,'builds'), name, as_attachment=False)
  396. if __name__ == '__main__':
  397. app.run()