def print_version_info():
    # The bootstrapper will print its own version, as well as that of
    # the west repository itself, then exit. So if this file is being
    # asked to print the version, it's because it's being run
    # directly, and not via the bootstrapper.
    #
    # Rather than play tricks like invoking "pip show west" (which
    # assumes the bootstrapper was installed via pip, the common but
    # not universal case), refuse the temptation to make guesses and
    # print an honest answer.
    log.inf('West bootstrapper version: N/A, not run via bootstrapper')

    # The running west installation.
    if IN_MULTIREPO_INSTALL:
        try:
            desc = check_output(['git', 'describe', '--tags'],
                                stderr=DEVNULL,
                                cwd=os.path.dirname(__file__))
            west_version = desc.decode(sys.getdefaultencoding()).strip()
        except CalledProcessError as e:
            west_version = 'unknown'
    else:
        west_version = 'N/A, monorepo installation'
    west_src_west = os.path.dirname(__file__)
    print('West repository version: {} ({})'.format(
        west_version, os.path.dirname(os.path.dirname(west_src_west))))
Exemplo n.º 2
0
 def run(self):
     while True:
         time.sleep(1)
         if self.timeout:
             self.timeout -= 1
             if not self.timeout:
                 inf('idle')
Exemplo n.º 3
0
def delete_wan_file(filepath):
    """
    Delete file on WAN if WAN is configured. This will be triggered when a file is
    deleted from LAN filestorage.
    """
    if not FILUXE_WAN:
        deb(f'no wan configured, not deleting {filepath} on wan')
        return

    filestorage_path = os.path.relpath(filepath, FILUXE_LAN.root())
    path = os.path.dirname(filestorage_path)

    try:
        if not ACTIVE_RULES['dirs'][path]['delete']:
            deb(f'not deleting on wan since delete=false for {filepath}')
            return
    except:
        pass

    try:
        file = os.path.basename(filepath)
        rule_path = os.path.normpath(path)
        dir_rules = ACTIVE_RULES['dirs'][rule_path]
        if not fwd_util.filename_is_included(file, dir_rules):
            inf(f'filename {file} is not in scope and will not be exported')
            return
    except:
        deb(f'from "{FILUXE_WAN.log_path(filepath)}" deleting file {file} (no rules)'
            )

    fwd_util.delete_http_file(FILUXE_WAN, filestorage_path)
Exemplo n.º 4
0
def export_file(filepath):
    """ Use filuxe to upload the file if it first matches the include regex and
        second doesn't match the exclude regex.
        If the include regex and the exclude regex are both empty strings then
        the file is exported.
    """
    if not FILUXE_WAN:
        return

    path = os.path.dirname(filepath)
    relpath = os.path.relpath(path, FILE_ROOT)

    file = os.path.basename(filepath)

    try:
        dir_rules = ACTIVE_RULES['dirs'][relpath]
        if not fwd_util.filename_is_included(file, dir_rules):
            inf(f'filename {file} is not in scope and will not be exported')
            return
        deb(f'forwarding {file}')
    except:
        inf(f'from {relpath} uploading file {file} (no rules)')

    try:
        deb(f'uploading {FILUXE_LAN.log_path(filepath)}')
        FILUXE_WAN.upload(filepath, os.path.join(relpath, file))
    except requests.ConnectionError:
        war('upload failed, WAN server is not reachable.')
    except FileNotFoundError:
        war(f'exception file not found, {os.path.join(relpath, file)} (internal race)'
            )
    def flash(self, **kwargs):
        if self.bin_name is None:
            raise ValueError('Cannot flash; bin_name is missing')

        lines = ['r'] # Reset and halt the target

        if self.erase:
            lines.append('erase') # Erase all flash sectors

        lines.append('loadfile {} 0x{:x}'.format(self.bin_name,
                                                 self.flash_addr))
        lines.append('g') # Start the CPU
        lines.append('q') # Close the connection and quit

        # Don't use NamedTemporaryFile: the resulting file can't be
        # opened again on Windows.
        with tempfile.TemporaryDirectory(suffix='jlink') as d:
            fname = os.path.join(d, 'runner.jlink')
            with open(fname, 'wb') as f:
                f.writelines(bytes(line + '\n', 'utf-8') for line in lines)

            cmd = ([self.commander] +
                   ['-if', self.iface,
                    '-speed', self.speed,
                    '-device', self.device,
                    '-CommanderScript', fname])

            log.inf('Flashing Target Device')
            self.check_call(cmd)
def main(argv=None):
    # Makes ANSI color escapes work on Windows, and strips them when
    # stdout/stderr isn't a terminal
    colorama.init()

    if argv is None:
        argv = sys.argv[1:]
    args, unknown = parse_args(argv)

    for_stack_trace = 'run as "west -v ... {} ..." for a stack trace'.format(
        args.command)
    try:
        args.handler(args, unknown)
    except WestUpdated:
        # West has been automatically updated. Restart ourselves to run the
        # latest version, with the same arguments that we were given.
        os.execv(sys.executable, [sys.executable] + sys.argv)
    except KeyboardInterrupt:
        sys.exit(0)
    except CalledProcessError as cpe:
        log.err('command exited with status {}: {}'.format(
            cpe.args[0], quote_sh_list(cpe.args[1])))
        if args.verbose:
            raise
        else:
            log.inf(for_stack_trace)
    except CommandContextError as cce:
        log.die('command', args.command, 'cannot be run in this context:',
                *cce.args)
    except Exception as exc:
        log.err(*exc.args, fatal=True)
        if args.verbose:
            raise
        else:
            log.inf(for_stack_trace)
Exemplo n.º 7
0
def _inf(project, msg):
    # Print '=== msg' (to clearly separate it from Git output). Supports the
    # same (foo) shorthands as the git commands.
    #
    # Prints the message in green if stdout is a terminal, to clearly separate
    # it from command (usually Git) output.

    log.inf('=== ' + _expand_shorthands(project, msg), colorize=True)
Exemplo n.º 8
0
 def process_IN_CREATE(self, event):
     if event.dir:
         inf(f'new directory "{FILUXE_LAN.log_path(event.pathname)}"')
         path = os.path.relpath(event.pathname, FILUXE_LAN.root())
         calculate_rules(path)
     else:
         inf(f'new file "{FILUXE_LAN.log_path(event.pathname)}"')
         new_file(event.pathname)
Exemplo n.º 9
0
    def do_run(self, args, user_args):
        log.inf("Manifest path: {}\n".format(_manifest_path(args)))

        for project in _all_projects(args):
            log.inf('{:15} {:30} {:15} {}'.format(
                project.name,
                os.path.join(project.path, ''),  # Add final '/' if missing
                project.revision,
                "(cloned)" if _cloned(project) else "(not cloned)"))
Exemplo n.º 10
0
def route_delete(path):
    path = safe_join(os.path.join(app.config['fileroot'], path))
    try:
        os.remove(path)
        inf(f'deleting file "{path}"')
    except OSError:
        inf(f'file not found deleting "{path}"')
        return '', 404
    return '', 200
def _dump_runner_cached_opts(cache, runner, initial_indent, subsequent_indent):
    runner_args = _get_runner_args(cache, runner)
    if not runner_args:
        return

    log.inf('{}Cached runner-specific options:'.format(initial_indent),
            colorize=True)
    for arg in runner_args:
        log.inf('{}{}'.format(subsequent_indent, arg))
Exemplo n.º 12
0
    def flash(self, **kwargs):
        if self.bin_name is None:
            raise ValueError('Cannot flash; bin_name is missing')

        cmd = ([self.flashtool] + self.flash_addr_args + self.daparg_args +
               self.target_args + self.board_args + self.flashtool_extra +
               [self.bin_name])

        log.inf('Flashing Target Device')
        self.check_call(cmd)
Exemplo n.º 13
0
def run_filesystem_observer(root):
    global LOOP, WATCH_MANAGER
    inf(f'starting file observer in {root}')

    LOOP = asyncio.get_event_loop()
    WATCH_MANAGER = pyinotify.WatchManager()
    pyinotify.AsyncioNotifier(WATCH_MANAGER,
                              LOOP,
                              default_proc_fun=EventHandler())
    mask = pyinotify.IN_CLOSE_WRITE | pyinotify.IN_DELETE | pyinotify.IN_MOVED_FROM | pyinotify.IN_MOVED_TO
    WATCH_MANAGER.add_watch(root, mask, rec=True, auto_add=True)

    signal.signal(signal.SIGINT, terminate)
    signal.signal(signal.SIGTERM, terminate)
Exemplo n.º 14
0
def route_upload(path):
    global chunked_file_handle
    time = request.args.get('time', type=float, default=0.0)
    force = request.args.get('force', type=inputs.boolean, default=False)
    path = safe_join(os.path.join(app.config['fileroot'], path))
    if path is None:
        abort(404)

    try:
        content_range = request.environ['HTTP_CONTENT_RANGE']
        parsed_ranges = re.search(r'bytes (\d*)-(\d*)\/(\d*)', content_range)
        _from, _to, _size = [int(x) for x in parsed_ranges.groups()]
        deb(f'chunked upload, {_from} to {_to} ({_size}), {_to - _from + 1} bytes')
    except:
        content_range = None

    if not content_range or _from == 0:
        if os.path.exists(path):
            if not force:
                # if force was not given then the default is that the server refuses to rewrite an existing file
                err(f'file {path} already exist, returning 403 (see --force)')
                return '', 403
        else:
            directory = os.path.dirname(path)
            if not os.path.exists(directory):
                inf(f'constructing new path {directory}')
                Path(directory).mkdir(parents=True, exist_ok=True)

    if content_range:
        if _from == 0:
            try:
                if chunked_file_handle.get(path):
                    err('internal error in upload, non closed filehandle')
                    chunked_file_handle[path].close()
                open(path, 'w').close()
                chunked_file_handle[path] = open(path, "ab")
            except:
                pass

            inf(f'writing file "{path}" ({human_file_size(_size)})')

        chunked_file_handle[path].write(request.data)

        if _to == _size - 1:
            inf(f'{path} transfer complete')
            chunked_file_handle[path].close()
            del chunked_file_handle[path]

    else:
        # ordinary non-chunked upload, single write
        inf(f'writing file "{path}"')
        with open(path, "wb") as fp:
            fp.write(request.data)

    if time > 0.0:
        deb(f'setting {path} time to {time}')
        os.utime(path, (time, time))

    # 201: Created
    return '', 201
    def call(self, cmd):
        '''Subclass subprocess.call() wrapper.

        Subclasses should use this method to run command in a
        subprocess and get its return code, rather than
        using subprocess directly, to keep accurate debug logs.
        '''
        quoted = quote_sh_list(cmd)

        if JUST_PRINT:
            log.inf(quoted)
            return 0

        log.dbg(quoted)
        return subprocess.call(cmd)
Exemplo n.º 16
0
    def do_run(self, args, user_args):
        if args.branch:
            # Create a branch in the specified projects
            for project in _cloned_projects(args):
                _create_branch(project, args.branch)
        else:
            # No arguments. List local branches from all cloned projects along
            # with the projects they appear in.

            branch2projs = collections.defaultdict(list)
            for project in _cloned_projects(args):
                for branch in _branches(project):
                    branch2projs[branch].append(project.name)

            for branch, projs in sorted(branch2projs.items()):
                log.inf('{:18} {}'.format(branch, ", ".join(projs)))
Exemplo n.º 17
0
    def download(self, filename, path, force=False):
        url = f'{self.server}/download/{path}'
        response = requests.get(url, verify=self.certificate)
        if response.status_code != 200:
            err(f'server returned error {response.status_code} for downloading "{path}"'
                )
            return ErrorCode.FILE_NOT_FOUND

        if force or not os.path.exists(filename):
            with open(filename, 'wb') as f:
                f.write(response.content)
            inf(f'downloaded {url} ({human_file_size(os.path.getsize(filename))}) as "{filename}"'
                )
        else:
            die(f'local file "{filename}" already exists, bailing out (see --force)',
                error_code=ErrorCode.FILE_ALREADY_EXIST)
        return ErrorCode.OK
Exemplo n.º 18
0
def synchonize(lan_files):
    inf('synchonizing with WAN server, please wait')

    with Indent() as _:
        # If the WAN server is missing then the forwarder will not be able to do its job before
        # the WAN server can be reached.
        wan_files = fwd_util.get_http_filelist(FILUXE_WAN, rules=ACTIVE_RULES)

        if lan_files is None or wan_files is None:
            war('retrieving filelists failed, synchonization aborted')
            return

        inf(f'found {lan_files["info"]["files"]} files on LAN server and {wan_files["info"]["files"]} on WAN server'
            )

        new_files = []
        modified_files = []
        copy_bytes = 0
        for directory, filelist in lan_files['filelist'].items():
            if directory not in wan_files['filelist']:
                for filename, metrics in lan_files['filelist'][
                        directory].items():
                    pathname = os.path.join(directory, filename)
                    new_files.append(pathname)
                    copy_bytes += metrics['size']
                continue

            for filename, metrics in filelist.items():
                pathname = os.path.join(directory, filename)
                if filename not in wan_files['filelist'][directory]:
                    new_files.append(pathname)
                    copy_bytes += metrics['size']
                elif metrics['time'] != wan_files['filelist'][directory][
                        filename]['time']:
                    modified_files.append(pathname)
                    copy_bytes += metrics['size']

        if not len(new_files) + len(modified_files):
            inf('WAN server is up-to-date')
        else:
            inf(f'synchonizing: uploading {human_file_size(copy_bytes)} in {len(new_files)} new files '
                f'and {len(modified_files)} modified files')
            for file in new_files + modified_files:
                export_file(os.path.join(FILE_ROOT, file))
            inf('synchonizing: complete')
    def check_output(self, cmd):
        '''Subclass subprocess.check_output() wrapper.

        Subclasses should use this method to run command in a
        subprocess and check that it executed correctly, rather than
        using subprocess directly, to keep accurate debug logs.
        '''
        quoted = quote_sh_list(cmd)

        if JUST_PRINT:
            log.inf(quoted)
            return b''

        log.dbg(quoted)
        try:
            return subprocess.check_output(cmd)
        except subprocess.CalledProcessError:
            raise
Exemplo n.º 20
0
    def parse_into_file_groups(self, directory, filelist, directory_settings):
        file_groups = {}
        max_files, delete_by, file_group_rules = directory_settings
        deb(f'find groups in {self.domain.domain} "{directory}" ({len(filelist["filelist"][directory])} files)')

        for filename, fileinfo in filelist['filelist'][directory].items():
            group_key = 'ungrouped'

            for group in file_group_rules:
                try:
                    match = re.match(fr'{group}', filename)
                except Exception as e:
                    err(f'regex gave exception {e.__repr__()} with regex "{group}"')
                    exit(1)

                if match:
                    nof_groups = len(match.groups())
                    nof_group_regex_groups = re.compile(group).groups

                    if nof_groups != nof_group_regex_groups:
                        deb(f'parsing {filename} failed, found {nof_groups} groups, not {nof_group_regex_groups}')
                    else:
                        group_key = ':'.join(match.groups())
                        break

            if file_group_rules and group_key == 'ungrouped':
                inf(f'no group match for {filename}, adding to "ungrouped"')

            group_key = os.path.join(directory, group_key)
            try:
                file_groups[group_key]
            except:
                file_groups[group_key] = {}
                file_groups[group_key]['files'] = {}

            file_groups[group_key]['maxfiles'] = max_files
            file_groups[group_key]['deleteby'] = delete_by
            file_groups[group_key]['directory'] = directory
            file_groups[group_key]['files'][filename] = fileinfo

        for group in file_groups.keys():
            deb(f'group {group} with {len(file_groups[group]["files"])} files')

        return file_groups
Exemplo n.º 21
0
    def do_run(self, command, **kwargs):
        commands = []
        if (self.snr is None):
            board_snr = self.get_board_snr_from_user()
        else:
            board_snr = self.snr.lstrip("0")
        program_cmd = [
            'nrfjprog', '--program', self.hex_, '-f', self.family, '--snr',
            board_snr
        ]

        print('Flashing file: {}'.format(self.hex_))
        if self.erase:
            commands.extend([[
                'nrfjprog', '--eraseall', '-f', self.family, '--snr', board_snr
            ], program_cmd])
        else:
            if self.family == 'NRF51':
                commands.append(program_cmd + ['--sectorerase'])
            else:
                commands.append(program_cmd + ['--sectoranduicrerase'])

        if self.family == 'NRF52' and self.softreset == False:
            commands.extend([
                # Enable pin reset
                [
                    'nrfjprog', '--pinresetenable', '-f', self.family, '--snr',
                    board_snr
                ],
            ])

        if self.softreset:
            commands.append(
                ['nrfjprog', '--reset', '-f', self.family, '--snr', board_snr])
        else:
            commands.append([
                'nrfjprog', '--pinreset', '-f', self.family, '--snr', board_snr
            ])

        for cmd in commands:
            self.check_call(cmd)

        log.inf('Board with serial number {} flashed successfully.'.format(
            board_snr))
Exemplo n.º 22
0
def list_files(path):
    recursive = request.args.get('recursive', type=inputs.boolean, default=False)
    path = safe_join(os.path.join(app.config['fileroot'], path))
    if not os.path.exists(path):
        err(f'filelist failed, path not found "{path}"')
        return 'path not found', 404
    fileroot = os.path.join(app.config['fileroot'], '')
    file_result = {}
    dir_result = []
    nof_files = 0

    for _root, dirs, _files in os.walk(path):
        for file in _files:
            p = os.path.join(_root, file)
            if not file_is_closed(p):
                war(f'skipping {p} since it is busy')
                continue
            relative = os.path.relpath(_root, fileroot)
            if not file_result.get(relative):
                file_result[relative] = {}
            file_result[relative][file] = {'size': os.path.getsize(p), 'time': get_file_time(p)}

        for directory in dirs:
            rel_path = os.path.join(os.path.relpath(_root, fileroot), directory)
            dir_result.append(os.path.normpath(rel_path))

        nof_files += len(_files)

        if not recursive:
            break

    extra = "(recursive)" if recursive else ""

    inf(f'returning filelist at "{path}", {nof_files} files and {len(dir_result)} directories. {extra}')

    ret = {'info':
               {
                   'fileroot': fileroot, 'files': nof_files, 'dirs': len(dir_result)
               },
           'filelist': file_result,
           'dirlist': dir_result}
    return jsonify(ret)
Exemplo n.º 23
0
def filter_filelist(filelist, rules):
    """
    Return a copy of the filelist where not-included and excluded files are removed according to the rule set.
    """
    try:
        filtered_filelist = copy.deepcopy(filelist)

        for path, files in filelist['filelist'].items():
            if not rules['dirs'].get(path):
                # Probably got a directory from a wan filelist that does not exist on lan.
                inf(f'filter filelist: ignoring directory "{path}" which is not found in rules'
                    )
            else:
                for filename in files:
                    if not filename_is_included(filename, rules['dirs'][path]):
                        del filtered_filelist['filelist'][path][filename]
    except:
        deb('http filelist returned unfiltered (bad rules?)')

    return filtered_filelist
    def popen_ignore_int(self, cmd):
        '''Spawn a child command, ensuring it ignores SIGINT.

        The returned subprocess.Popen object must be manually terminated.'''
        cflags = 0
        preexec = None
        system = platform.system()
        quoted = quote_sh_list(cmd)

        if system == 'Windows':
            cflags |= subprocess.CREATE_NEW_PROCESS_GROUP
        elif system in {'Linux', 'Darwin'}:
            preexec = os.setsid

        if JUST_PRINT:
            log.inf(quoted)
            return _DebugDummyPopen()

        log.dbg(quoted)
        return subprocess.Popen(cmd, creationflags=cflags, preexec_fn=preexec)
Exemplo n.º 25
0
def new_file(filename):
    if not os.path.exists(filename):
        deb(f'listener: changed file "{filename}" does not exist anymore?')
        return

    inf(f'listener: new/changed file "{FILUXE_LAN.log_path(filename)}"')

    with Indent() as _:
        if LAN_FILE_DELETER:
            path = os.path.dirname(filename)
            filestorage_path = os.path.relpath(path, FILUXE_LAN.root())
            LAN_FILE_DELETER.enforce_max_files(filestorage_path,
                                               rules=ACTIVE_RULES,
                                               recursive=False)

        if not os.path.exists(filename):
            war(f'listener: new file "{FILUXE_LAN.log_path(filename)}" already deleted and will not be forwarded'
                )
            return

        export_file(filename)
    def do_run(self, args, ignored):
        self.args = args  # Avoid having to pass them around
        log.dbg('args:', args, level=log.VERBOSE_EXTREME)
        self._sanity_precheck()
        self._setup_build_dir()
        if is_zephyr_build(self.build_dir):
            self._update_cache()
            if self.args.cmake or self.args.cmake_opts:
                self.run_cmake = True
        else:
            self.run_cmake = True
        self._setup_source_dir()
        self._sanity_check()

        log.inf('source directory: {}'.format(self.source_dir), colorize=True)
        log.inf('build directory: {}{}'.format(
            self.build_dir, (' (created)' if self.created_build_dir else '')),
                colorize=True)
        if self.cmake_cache:
            board = self.cmake_cache.get('CACHED_BOARD')
        elif self.args.board:
            board = self.args.board
        else:
            board = 'UNKNOWN'  # shouldn't happen
        log.inf('BOARD:', board, colorize=True)

        self._run_cmake(self.args.cmake_opts)
        self._sanity_check()
        self._update_cache()

        extra_args = ['--target', args.target] if args.target else []
        cmake.run_build(self.build_dir, extra_args=extra_args)
Exemplo n.º 27
0
def start(_, cfg):
    global users
    try:
        file_root = cfg['wan_filestorage']
        host = cfg['wan_host']
        port = cfg['wan_port']
        realm = 'WAN'
    except:
        file_root = cfg['lan_filestorage']
        host = cfg['lan_host']
        port = cfg['lan_port']
        realm = 'LAN'

    try:
        for i in (0, 1):
            if not os.path.exists(cfg['certificates'][i]):
                die(f'specified certificate not found, "{cfg["certificates"][i]}"')
        ssl_context = (cfg['certificates'][0], cfg['certificates'][1])
    except:
        ssl_context = False

    if not os.path.exists(file_root):
        try:
            os.makedirs(file_root)
        except:
            die(f'unable to create file root {file_root}, please make it yourself and fix permissions',
                ErrorCode.SERVER_ERROR)

    app.config['fileroot'] = file_root
    app.secret_key = os.urandom(50)
    app.name = f'filuxe_server_{realm}'
    try:
        app.config['writekey'] = cfg['write_key']
    except:
        app.config['writekey'] = ''

    try:
        users = {cfg['username']: generate_password_hash(cfg['password'])}
        inf('HTTP-AUTH enabled')
    except:
        inf('HTTP-AUTH disabled')

    inf(f'filuxe {realm} server {filuxe_server_version} running at http{"s" if ssl_context else ""}://{host}:{port}')
    inf(f'filestorage root "{file_root}"')

    if ssl_context:
        app.run(host=host, port=port, ssl_context=ssl_context)
    else:
        app.run(host=host, port=port)
    return ErrorCode.OK
Exemplo n.º 28
0
    def __init__(self, cfg, lan=True, force=False):
        self.certificate = False
        protocol = 'http://'
        self.force = force
        try:
            if lan:
                self.domain = 'LAN'
                try:
                    self.certificate = cfg['lan_certificate']
                    protocol = 'https://'
                except:
                    pass

                self.server = f'{protocol}{cfg["lan_host"]}:{cfg["lan_port"]}'
                inf(f'filuxe LAN server is {self.server}')
                self.file_root = cfg.get('lan_filestorage')

            else:
                self.domain = 'WAN'
                try:
                    self.certificate = cfg['wan_certificate']
                    protocol = 'https://'
                except:
                    pass

                self.server = f'{protocol}{cfg["wan_host"]}:{cfg["wan_port"]}'
                inf(f'filuxe WAN server is {self.server}')
                try:
                    self.file_root = cfg['wan_filestorage']
                except:
                    self.file_root = ''

        except KeyError as e:
            err(f'expected a {e} entry in the configuration file',
                ErrorCode.MISSING_KEY)

        try:
            self.write_key = cfg['write_key']
        except:
            self.write_key = ''
Exemplo n.º 29
0
    def do_run(self, command, **kwargs):
        bin_name = path.splitext(self.elf)[0] + path.extsep + 'bin'
        cmd_convert = [self.espidf, '--chip', 'esp32', 'elf2image', self.elf]
        cmd_flash = [
            self.espidf, '--chip', 'esp32', '--port', self.device, '--baud',
            self.baud, '--before', 'default_reset', '--after', 'hard_reset',
            'write_flash', '-u', '--flash_mode', self.flash_mode,
            '--flash_freq', self.flash_freq, '--flash_size', self.flash_size
        ]

        if self.bootloader_bin:
            cmd_flash.extend(['0x1000', self.bootloader_bin])
            cmd_flash.extend(['0x8000', self.partition_table_bin])
            cmd_flash.extend(['0x10000', bin_name])
        else:
            cmd_flash.extend(['0x1000', bin_name])

        log.inf("Converting ELF to BIN")
        self.check_call(cmd_convert)

        log.inf("Flashing ESP32 on {} ({}bps)".format(self.device, self.baud))
        self.check_call(cmd_flash)
def _dump_runner_opt_help(runner, cls):
    # Construct and print the usage text
    dummy_parser = argparse.ArgumentParser(prog='', add_help=False)
    cls.add_parser(dummy_parser)
    formatter = dummy_parser._get_formatter()
    for group in dummy_parser._action_groups:
        # Break the abstraction to filter out the 'flash', 'debug', etc.
        # TODO: come up with something cleaner (may require changes
        # in the runner core).
        actions = group._group_actions
        if len(actions) == 1 and actions[0].dest == 'command':
            # This is the lone positional argument. Skip it.
            continue
        formatter.start_section('REMOVE ME')
        formatter.add_text(group.description)
        formatter.add_arguments(actions)
        formatter.end_section()
    # Get the runner help, with the "REMOVE ME" string gone
    runner_help = '\n'.join(formatter.format_help().splitlines()[1:])

    log.inf('{} options:'.format(runner), colorize=True)
    log.inf(runner_help)