Path: blob/master/Tools/scripts/generate_manifest.py
9575 views
#!/usr/bin/env python312'''3AP_FLAKE8_CLEAN4'''56import fnmatch7import gen_stable8import gzip9import json10import os11import re12import subprocess13import shutil14import sys1516FIRMWARE_TYPES = ["AntennaTracker", "Copter", "Plane", "Rover", "Sub", "AP_Periph", "Blimp"]17RELEASE_TYPES = ["beta", "latest", "stable", "stable-*", "dirty"]1819# mapping for board names to brand name and manufacturer20brand_map = {21'Pixhawk4' : ('Pixhawk 4', 'Holybro'),22'Pixhawk4-bdshot' : ('Pixhawk 4', 'Holybro'),23'Pix32v5' : ('Pix32 v5', 'Holybro'),24'Durandal' : ('Durandal', 'Holybro'),25'Durandal-bdshot' : ('Durandal', 'Holybro'),26'PH4-mini' : ('Pixhawk 4 Mini', 'Holybro'),27'KakuteF4' : ('KakuteF4', 'Holybro'),28'KakuteF7' : ('KakuteF7', 'Holybro'),29'KakuteF7Mini' : ('KakuteF7Mini', 'Holybro'),30'KakuteF4Mini' : ('KakuteF4Mini', 'Holybro'),31'KakuteH7Mini' : ('KakuteH7Mini', 'Holybro'),32'KakuteH7Mini-Nand' : ('KakuteH7Mini-Nand', 'Holybro'),33'KakuteH7' : ('KakuteH7', 'Holybro'),34'KakuteH7-bdshot' : ('KakuteH7', 'Holybro'),35'KakuteH7v2' : ('KakuteH7v2', 'Holybro'),36'CubeBlack' : ('CubeBlack', 'Hex/ProfiCNC'),37'CubeYellow' : ('CubeYellow', 'Hex/ProfiCNC'),38'CubeOrange' : ('CubeOrange', 'Hex/ProfiCNC'),39'CubeOrange-bdshot' : ('CubeOrange', 'Hex/ProfiCNC'),40'CubePurple' : ('CubePurple', 'Hex/ProfiCNC'),41'CubeSolo' : ('CubeSolo', '3DR'),42'CubeGreen-solo' : ('CubeGreen Solo', 'Hex/ProfiCNC'),43'CUAVv5' : ('CUAVv5', 'CUAV'),44'CUAVv5-bdshot' : ('CUAVv5', 'CUAV'),45'CUAVv5Nano' : ('CUAVv5 Nano', 'CUAV'),46'CUAVv5Nano-bdshot' : ('CUAVv5 Nano', 'CUAV'),47'CUAV-Nora' : ('CUAV Nora', 'CUAV'),48'CUAV-Nora-bdshot' : ('CUAV Nora', 'CUAV'),49'CUAV-X7' : ('CUAV X7', 'CUAV'),50'CUAV-X7-bdshot' : ('CUAV X7', 'CUAV'),51'DrotekP3Pro' : ('Pixhawk 3 Pro', 'Drotek'),52'MambaF405v2' : ('Diatone Mamba F405 MK2', 'Diatone'),53'MatekF405' : ('Matek F405', 'Matek'),54'MatekF405-bdshot' : ('Matek F405', 'Matek'),55'MatekF405-STD' : ('Matek F405 STD', 'Matek'),56'MatekF405-Wing' : ('Matek F405 Wing', 'Matek'),57'MatekF765-SE' : ('Matek F765 SE', 'Matek'),58'MatekH743' : ('Matek H743', 'Matek'),59'MatekH743-bdshot' : ('Matek H743', 'Matek'),60'mini-pix' : ('MiniPix', 'Radiolink'),61'Pixhawk1' : ('Pixhawk1', 'mRobotics'),62'Pixracer' : ('PixRacer', 'mRobotics'),63'Pixracer-bdshot' : ('PixRacer', 'mRobotics'),64'mRoX21' : ('mRo X2.1', 'mRobotics'),65'mRoX21-777' : ('mRo X2.1-777', 'mRobotics'),66'mRoPixracerPro' : ('mRo PixracerPro', 'mRobotics'),67'mRoPixracerPro-bdshot' : ('mRo PixracerPro', 'mRobotics'),68'mRoControlZeroOEMH7' : ('mRo ControlZero OEM H7', 'mRobotics'),69'mRoNexus' : ('mRo Nexus', 'mRobotics'),70'TBS-Colibri-F7' : ('Colibri F7', 'TBS'),71'sparky2' : ('Sparky2', 'TauLabs'),72'mindpx-v2' : ('MindPX V2', 'AirMind'),73'OMNIBUSF7V2' : ('Omnibus F7 V2', 'Airbot'),74'omnibusf4pro' : ('Omnibus F4 Pro', 'Airbot'),75'omnibusf4pro-bdshot' : ('Omnibus F4 Pro', 'Airbot'),76'omnibusf4v6' : ('Omnibus F4 V6', 'Airbot'),77'OmnibusNanoV6' : ('Omnibus Nano V6', 'Airbot'),78'OmnibusNanoV6-bdshot' : ('Omnibus Nano V6', 'Airbot'),79'speedybeef4' : ('SpeedyBee F4', 'SpeedyBee'),80'speedybeef4v3' : ('SpeedyBee F4 v3', 'SpeedyBee'),81'QioTekZealotF427' : ('ZealotF427', 'QioTek'),82'BeastH7' : ('Beast H7 55A AIO', 'iFlight'),83'BeastH7v2' : ('Beast H7 v2 55A AIO', 'iFlight'),84'BeastF7' : ('Beast F7 45A AIO', 'iFlight'),85'BeastF7v2' : ('Beast F7 v2 55A AIO', 'iFlight'),86'MambaF405US-I2C' : ('Diatone Mamba Basic F405 MK3/MK3.5', 'Diatone'),87'MambaF405-2022' : ('Diatone Mamba Basic F405 MK4', 'Diatone'),88'MambaH743v4' : ('Diatone Mamba H743 MK4', 'Diatone'),89"FlywooF745" : ('Flywoo Goku GN 745 AIO', 'Flywoo'),90"FlywooF745Nano" : ('Flywoo Goku Hex F745', 'Flywoo'),91"modalai_fc-v1" : ('ModalAI FlightCore v1', 'ModalAI'),92'Pixhawk5X' : ('Pixhawk 5X', 'Holybro'),93"AIRLink" : ("Sky-Drones Technologies", "AIRLink"),94"SPRacingH7" : ("Seriously Pro Racing", "H7 Extreme"),95"SkystarsH7HD" : ("Skystars", "H743 HD"),96"SkystarsH7HD-bdshot" : ("Skystars", "H743 HD"),97"MicoAir405v2" : ("MicoAir F405 v2.1", "MicoAir"),98"MicoAir405Mini" : ("MicoAir F405 Mini", "MicoAir"),99"MicoAir743" : ("MicoAir H743 v1.3", "MicoAir"),100"MicoAir743-AIO" : ("MicoAir H743 AIO", "MicoAir"),101"MicoAir743v2" : ("MicoAir H743 v2.0", "MicoAir"),102"MicoAir743-Lite" : ("MicoAir H743 Lite v1.1", "MicoAir"),103"GEPRCF745BTHD": ("TAKER F745 BT", "GEPRC"),104"GEPRC_TAKER_H743": ("TAKER H743 BT", "GEPRC"),105}106107108class Firmware():109def __init__(self,110date=None,111platform=None,112vehicletype=None,113filepath=None,114git_sha=None,115frame=None):116self.atts = dict()117self.atts["date"] = date118self.atts["platform"] = platform119self.atts["vehicletype"] = vehicletype120self.atts["filepath"] = filepath121self.atts["git_sha"] = git_sha122self.atts["firmware-version-str"] = ""123self.atts["frame"] = frame124self.atts["release-type"] = None125self.atts["firmware-version"] = None126127def __getitem__(self, what):128return self.atts[what]129130def __setitem__(self, name, value):131self.atts[name] = value132133134class ManifestGenerator():135'''Return a JSON string describing "binary" directory contents under136basedir'''137138def __init__(self, basedir, baseurl):139self.basedir = basedir140self.baseurl = baseurl141142def frame_map(self, frame):143'''translate from ArduPilot frame type terminology into mavlink144terminology'''145frame_to_mavlink_dict = {146"quad": "QUADROTOR",147"hexa": "HEXAROTOR",148"y6": "ARDUPILOT_Y6",149"tri": "TRICOPTER",150"octa": "OCTOROTOR",151"octa-quad": "ARDUPILOT_OCTAQUAD",152"deca": "DECAROTOR",153"heli": "HELICOPTER",154"Plane": "FIXED_WING",155"AntennaTracker": "ANTENNA_TRACKER",156"Rover": "GROUND_ROVER",157"Sub": "SUBMARINE",158"AP_Periph": "CAN_PERIPHERAL",159}160if frame in frame_to_mavlink_dict:161return frame_to_mavlink_dict[frame]162163return frame164165def releasetype_map(self, releasetype):166'''translate from ArduPilot release type terminology into mavlink167terminology'''168if releasetype == 'stable':169return 'OFFICIAL'170return releasetype.upper()171172def looks_like_binaries_directory(self, dir):173'''returns True if dir looks like it is a build_binaries.py output174directory'''175for entry in os.listdir(dir):176if entry in FIRMWARE_TYPES:177return True178return False179180def git_sha_from_git_version(self, filepath):181'''parses get-version.txt (as emitted by build_binaries.py, returns182git sha from it'''183content = open(filepath).read()184sha_regex = re.compile("commit (?P<sha>[0-9a-f]+)")185m = sha_regex.search(content)186if m is None:187raise Exception(188"filepath (%s) does not contain a git sha" % (filepath,))189return m.group("sha")190191def fwversion_from_git_version(self, filepath):192'''parses get-version.txt (as emitted by build_binaries.py, returns193git sha from it'''194content = open(filepath).read()195sha_regex = re.compile(r"APMVERSION: \S+\s+(\S+)")196m = sha_regex.search(content)197if m is None:198raise Exception(199"filepath (%s) does not contain an APMVERSION" % (filepath,))200return m.group(1)201202def add_USB_IDs_PX4(self, firmware):203'''add USB IDs to a .px4 firmware'''204url = firmware['url']205suffix = url.split('-')[-1]206if suffix == "v1.px4":207firmware['USBID'] = ['0x26AC/0x0010']208firmware['board_id'] = 5209firmware['bootloader_str'] = ['PX4 BL FMU v1.x']210elif suffix in ["v2.px4", "v3.px4"]:211firmware['USBID'] = ['0x26AC/0x0011']212firmware['board_id'] = 9213firmware['bootloader_str'] = ['PX4 BL FMU v2.x']214elif suffix == "v4.px4":215firmware['USBID'] = ['0x26AC/0x0012']216firmware['board_id'] = 11217firmware['bootloader_str'] = ['PX4 BL FMU v4.x']218elif suffix == "v4pro.px4":219firmware['USBID'] = ['0x26AC/0x0013']220firmware['board_id'] = 13221firmware['bootloader_str'] = ['PX4 BL FMU v4.x PRO']222223def add_USB_IDs_ChibiOS(self, firmware):224'''add USB IDs to a ChbiOS apj firmware'''225url = firmware['url'][len(self.baseurl)+1:]226apj_path = os.path.join(self.basedir, url)227if not os.path.exists(apj_path):228print("bad apj path %s" % apj_path, file=sys.stderr)229return230apj_json = json.load(open(apj_path, 'r'))231if 'board_id' not in apj_json:232print("no board_id in %s" % apj_path, file=sys.stderr)233return234if 'platform' not in firmware:235print("no platform for %s" % apj_path, file=sys.stderr)236return237board_id = apj_json['board_id']238platform = firmware['platform']239240# all ChibiOS builds can have platform as bootloader_str and board_id from241# hwdef.dat242firmware['board_id'] = board_id243firmware['bootloader_str'] = [platform+"-BL"]244245# map of vendor specific USB IDs246USBID_MAP = {247'CubeBlack': ['0x2DAE/0x1011'],248'CubeOrange': ['0x2DAE/0x1016', '0x2DAE/0x1017'],249'CubePurple': ['0x2DAE/0x1005'],250'CubeYellow': ['0x2DAE/0x1002'],251'Pixhawk4': ['0x3162/0x0047'],252'PH4-mini': ['0x3162/0x0049'],253'Durandal': ['0x3162/0x004B'],254'VRBrain-v51': ['0x27AC/0x1151'],255'VRBrain-v52': ['0x27AC/0x1152'],256'VRBrain-v54': ['0x27AC/0x1154'],257'VRCore-v10': ['0x27AC/0x1910'],258'VRUBrain-v51': ['0x27AC/0x1351']259}260if 'USBID' in apj_json:261# newer APJ files have USBID in the json data262firmware['USBID'] = [apj_json['USBID']]263elif platform in USBID_MAP:264firmware['USBID'] = USBID_MAP[platform]265else:266# all others use a single USB VID/PID267firmware['USBID'] = ['0x0483/0x5740']268269if board_id == 50:270# special case for FMUv5, they always get the px4 bootloader IDs as an option271firmware['bootloader_str'].append('PX4 BL FMU v5.x')272firmware['USBID'].append('0x26AC/0x0032')273274if board_id == 9:275# special case for FMUv3, they always get the px4 bootloader IDs as an option276firmware['bootloader_str'].append('PX4 BL FMU v2.x')277firmware['USBID'].append('0x26AC/0x0011')278279if board_id == 11:280# special case for FMUv4, they always get the px4 bootloader IDs as an option281firmware['bootloader_str'].append('PX4 BL FMU v4.x')282firmware['USBID'].append('0x26AC/0x0012')283284if board_id == 13:285# special case for FMUv4pro, they always get the px4 bootloader IDs as an option286firmware['bootloader_str'].append('PX4 BL FMU v4.x PRO')287firmware['USBID'].append('0x26AC/0x0013')288289if board_id == 88:290# special case for MindPX-v2 boards291firmware['bootloader_str'].append('MindPX BL FMU v2.x')292firmware['USBID'].append('0x26AC/0x0030')293294if board_id == 53:295# special case for 6X, they always get the px4 bootloader IDs as an option296firmware['bootloader_str'].append('PX4 BL FMU v6X.x')297firmware['USBID'].append('0x3185/0x0035')298299if board_id == 56:300# special case for 6C, they always get the px4 bootloader IDs as an option301firmware['bootloader_str'].append('PX4 BL FMU v6C.x')302firmware['USBID'].append('0x3185/0x0038')303304if platform in brand_map:305(brand_name, manufacturer) = brand_map[platform]306firmware['brand_name'] = brand_name307firmware['manufacturer'] = manufacturer308309# copy over some extra information if available310extra_tags = ['image_size', 'brand_name', 'manufacturer']311for tag in extra_tags:312if tag in apj_json:313firmware[tag] = apj_json[tag]314315def add_USB_IDs(self, firmware):316'''add USB IDs to a firmware'''317fmt = firmware['format']318if fmt == "px4":319self.add_USB_IDs_PX4(firmware)320return321if fmt == "apj":322self.add_USB_IDs_ChibiOS(firmware)323return324325def firmware_format_for_filepath(self, filepath):326filename = os.path.basename(filepath)327if "." in filename:328return "".join(filename.split(".")[-1:])329# no extension; ensure this is an elf:330text = subprocess.check_output(["file", "-b", filepath])331text = text.decode('ascii')332333if re.match("^ELF", text):334return "ELF"335print("Unknown file type (%s)" % filepath)336print("Got: %s" % text)337return "Unknown" # should raise an error somehow338339def add_firmware_data_from_dir(self,340dir,341firmware_data,342vehicletype,343releasetype="dev"):344'''accumulate additional information about firmwares from directory'''345variant_firmware_regex = re.compile(r"[^-]+-(?P<variant>v\d+)[.px4]")346if not os.path.isdir(dir):347return348try:349dlist = os.listdir(dir)350except Exception:351print("Error listing '%s'" % dir)352return353for platformdir in dlist:354if platformdir.startswith("."):355continue356some_dir = os.path.join(dir, platformdir)357if not os.path.isdir(some_dir):358continue359git_version_txt = os.path.join(some_dir, "git-version.txt")360if not os.path.exists(git_version_txt):361print("No file %s" % git_version_txt, file=sys.stderr)362continue363try:364git_sha = self.git_sha_from_git_version(git_version_txt)365except Exception as ex:366print("Failed to parse %s" % git_version_txt, ex, file=sys.stderr)367continue368try:369fwversion_str = self.fwversion_from_git_version(git_version_txt)370except Exception as ex:371print("Failed to parse APMVERSION %s" % git_version_txt, ex, file=sys.stderr)372continue373374# we require a firmware-version.txt. These files have been added to375# old builds that didn't have them376firmware_version_file = os.path.join(some_dir,377"firmware-version.txt")378if not os.path.exists(firmware_version_file):379print("Missing %s" % firmware_version_file, file=sys.stderr)380continue381382try:383firmware_version = open(firmware_version_file).read()384firmware_version = firmware_version.strip()385(_, _) = firmware_version.split("-")386except ValueError:387print("malformed firmware-version.txt at (%s)" % (firmware_version_file,), file=sys.stderr)388continue389except Exception:390print("bad file %s" % firmware_version_file, file=sys.stderr)391# this exception is swallowed.... the current archive392# is incomplete.393continue394395# Directory names for heli builds end in -heli396platform_frame_regex = re.compile("(?P<board>.+)(-(?P<frame>heli)$)")397m = platform_frame_regex.match(platformdir)398if m is not None:399# This is a heli build400platform = m.group("board") # e.g. navio401frame = "heli"402else:403# Non-heli build404frame = vehicletype # e.g. Plane405platform = platformdir # e.g. apm2406407# also gather information from any features.txt files present:408features_text = None409features_filepath = os.path.join(some_dir, "features.txt")410if os.path.exists(features_filepath):411features_text = sorted(open(features_filepath).read().rstrip().split("\n"))412413for filename in os.listdir(some_dir):414if filename in ["git-version.txt", "firmware-version.txt", "files.html", "features.txt"]:415continue416if filename.startswith("."):417continue418419m = variant_firmware_regex.match(filename)420if m:421# the platform variant is422# encoded in the firmware filename423# (e.g. the "v1" in424# ArduCopter-v1.px4)425variant = m.group("variant")426file_platform = "-".join([platform, variant])427else:428file_platform = platform429430filepath = os.path.join(some_dir, filename)431firmware_format = self.firmware_format_for_filepath(filepath)432if firmware_format not in ["elf", "ELF", "abin", "apj", "hex", "px4", "bin"]:433print("Unknown firmware format (%s)" % firmware_format)434435firmware = Firmware()436437# translate from supplied "release type" into both a438# "latest" flag and an actual release type. Also sort439# out which filepath we should use:440firmware["latest"] = 0441if releasetype == "dev":442if firmware["filepath"] is None:443firmware["filepath"] = filepath444if firmware["release-type"] is None:445firmware["release-type"] = "dev"446elif releasetype == "latest":447firmware["latest"] = 1448firmware["filepath"] = filepath449if firmware["release-type"] is None:450firmware["release-type"] = "dev"451else:452if (not firmware["latest"]):453firmware["filepath"] = filepath454firmware["release-type"] = releasetype455456firmware["platform"] = file_platform457firmware["vehicletype"] = vehicletype458firmware["git_sha"] = git_sha459firmware["firmware-version-str"] = fwversion_str460firmware["frame"] = frame461firmware["timestamp"] = os.path.getctime(firmware["filepath"])462firmware["format"] = firmware_format463firmware["firmware-version"] = firmware_version464465firmware["features"] = features_text466467firmware_data.append(firmware)468469def valid_release_type(self, tag):470'''check for valid release type'''471for r in RELEASE_TYPES:472if fnmatch.fnmatch(tag, r):473return True474return False475476def parse_fw_version(self, version):477(version_numbers, release_type) = version.split("-")478(major, minor, patch) = version_numbers.split(".")479return (major, minor, patch, version)480481def walk_directory(self, basedir):482'''walks directory structure created by build_binaries, returns Python483structure representing releases in that structure'''484year_month_regex = re.compile(r"(?P<year>\d{4})-(?P<month>\d{2})")485486firmwares = []487488# used to listdir basedir here, but since this is also a web489# document root, there's a lot of other stuff accumulated...490vehicletypes = FIRMWARE_TYPES491for vehicletype in vehicletypes:492try:493# the sort means we prefer 'stable' to 'stable-x.y.z' when they494# both contain the same contents495vdir = sorted(os.listdir(os.path.join(basedir, vehicletype)), reverse=True)496except OSError as e:497if e.errno == 2:498continue499for firstlevel in vdir:500if firstlevel == "files.html" or firstlevel.startswith("."):501# generated file which should be ignored502continue503# skip any non-directories (e.g. "files.html"):504if year_month_regex.match(firstlevel):505# this is a dated directory e.g. binaries/Copter/2016-02506# we do not include dated directories in the manifest ATM:507continue508509# assume this is a release directory such as510# "beta", or the "latest" directory (treated as a511# release and handled specially later)512tag = firstlevel513if not self.valid_release_type(tag):514print("Unknown tag (%s) in directory (%s)" %515(tag, os.path.join(*vdir)), file=sys.stderr)516continue517tag_path = os.path.join(basedir, vehicletype, tag)518if not os.path.isdir(tag_path):519continue520self.add_firmware_data_from_dir(tag_path,521firmwares,522vehicletype,523releasetype=tag)524525# convert from ardupilot-naming conventions to common JSON format:526firmware_json = []527features_json = [] # a structure containing summarised features per firmware528529for firmware in firmwares:530filepath = firmware["filepath"]531# replace the base directory with the base URL532urlifier = re.compile("^" + re.escape(basedir))533url = re.sub(urlifier, self.baseurl, filepath)534version_type = self.releasetype_map(firmware["release-type"])535some_json = dict({536"mav-autopilot": "ARDUPILOTMEGA",537"vehicletype": firmware["vehicletype"],538"platform": firmware["platform"],539"git-sha": firmware["git_sha"],540"url": url,541"mav-type": self.frame_map(firmware["frame"]),542"mav-firmware-version-type": version_type,543"mav-firmware-version-str": firmware["firmware-version-str"],544"latest": firmware["latest"],545"format": firmware["format"],546})547548if firmware["firmware-version"]:549try:550(major, minor, patch, release_type) = self.parse_fw_version(551firmware["firmware-version"])552except Exception:553print("Badly formed firmware-version.txt %s" % firmware["firmware-version"], file=sys.stderr)554continue555some_json["mav-firmware-version"] = ".".join([major,556minor,557patch])558some_json["mav-firmware-version-major"] = major559some_json["mav-firmware-version-minor"] = minor560some_json["mav-firmware-version-patch"] = patch561562self.add_USB_IDs(some_json)563564firmware_json.append(some_json)565566# now the features the firmware supports...567try:568features = firmware["features"]569# check apj here in case we're creating bin and apj etc:570if (firmware["format"] == "apj" and571features is not None and572bool(firmware["latest"])):573x = dict({574"vehicletype": firmware["vehicletype"],575"platform": firmware["platform"],576"git-sha": firmware["git_sha"],577"latest": firmware["latest"],578})579x["features"] = features580features_json.append(x)581582except KeyError:583pass584585ret = {586"format-version": "1.0.0", # semantic versioning587"firmware": firmware_json588}589590features_ret = {591"format-version": "1.0.0", # semantic versioning592"features": features_json593}594595return ret, features_ret596597def run(self):598'''walk directory supplied in constructor, record results in self'''599if not self.looks_like_binaries_directory(self.basedir):600print("Warning: this does not look like a binaries directory",601file=sys.stderr)602603self.structure, self.features_structure = self.walk_directory(self.basedir)604605def json(self):606'''returns JSON string for version information for all firmwares'''607if getattr(self, 'structure', None) is None:608self.run()609return json.dumps(self.structure, indent=4, separators=(',', ': '))610611def json_features(self):612'''returns JSON string for supported features for all firmwares.613run() method must have been called already'''614return json.dumps(self.features_structure, indent=4, separators=(',', ': '))615616def write_string_to_filepath(self, string, filepath):617'''writes the entirety of string to filepath'''618with open(filepath, "w") as x:619x.write(string)620621def write_json(self, content, path):622'''write content to path, also creating a compress .gz version'''623new_json_filepath = path + ".new"624self.write_string_to_filepath(content, new_json_filepath)625# provide a pre-compressed version. For reference, a 7M manifest626# "gzip -9"s to 300k in 1 second, "xz -e"s to 80k in 26 seconds627new_json_filepath_gz = path + ".gz.new"628with gzip.open(new_json_filepath_gz, 'wb') as gf:629content = bytes(content, 'ascii')630gf.write(content)631gf.close()632shutil.move(new_json_filepath, path)633shutil.move(new_json_filepath_gz, path + ".gz")634635def write_manifest_json(self, path):636'''write generated JSON content to path'''637self.write_json(self.json(), path)638639def write_features_json(self, path):640'''write generated features JSON content to path'''641self.write_json(self.json_features(), path)642643644def usage():645return '''Usage:646generate-manifest.py basedir'''647648649if __name__ == "__main__":650import argparse651parser = argparse.ArgumentParser(description='generate manifest.json')652653parser.add_argument('--outfile', type=str, default=None, help='output file, default stdout')654parser.add_argument('--outfile-features-json', type=str, default=None, help='output file for features json file')655parser.add_argument('--baseurl', type=str, default="https://firmware.ardupilot.org", help='base binaries directory')656parser.add_argument('basedir', type=str, default="-", help='base binaries directory')657658args = parser.parse_args()659660# ensure all stable directories are created661gen_stable.make_all_stable(args.basedir)662663generator = ManifestGenerator(args.basedir, args.baseurl)664generator.run()665666content = generator.json()667if args.outfile is None:668print(content)669else:670generator.write_manifest_json(args.outfile)671672if args.outfile_features_json is not None:673generator.write_features_json(args.outfile_features_json)674675676