def _check_python_file(self, fname: str) -> None: from efrotools import get_public_license, PYVER with open(fname, encoding='utf-8') as infile: contents = infile.read() lines = contents.splitlines() # Make sure all standalone scripts are pointing to the right # version of python (with a few exceptions where it needs to # differ) if contents.startswith('#!/'): copyrightline = 1 if fname not in ['tools/vmshell']: if not contents.startswith(f'#!/usr/bin/env python{PYVER}'): raise CleanError(f'Incorrect shebang (first line) for ' f'{fname}.') else: copyrightline = 0 # Special case: it there's spinoff autogenerate notice there, # look below it. if (lines[copyrightline] == '' and 'THIS FILE IS AUTOGENERATED' in lines[copyrightline + 1]): copyrightline += 2 # In all cases, look for our one-line legal notice. # In the public case, look for the rest of our public license too. if self._license_line_checks: public_license = get_public_license('python') private_license = '# ' + get_legal_notice_private() lnum = copyrightline if len(lines) < lnum + 1: raise RuntimeError('Not enough lines in file:', fname) disable_note = ('NOTE: You can disable license line' ' checks by adding "license_line_checks": false\n' 'to the root dict in config/localconfig.json.\n' 'see https://ballistica.net/wiki' '/Knowledge-Nuggets#' 'hello-world-creating-a-new-game-type') if self._public: # Check for public license only. if lines[lnum] != public_license: raise CleanError(f'License text not found' f" at '{fname}' line {lnum+1};" f' please correct.\n' f'Expected text is: {public_license}\n' f'{disable_note}') else: # Check for public or private license. if (lines[lnum] != public_license and lines[lnum] != private_license): raise CleanError(f'License text not found' f" at '{fname}' line {lnum+1};" f' please correct.\n' f'Expected text (for public files):' f' {public_license}\n' f'Expected text (for private files):' f' {private_license}\n' f'{disable_note}')
def _run_pylint(projroot: Path, pylintrc: Union[Path, str], cache: Optional[FileCache], dirtyfiles: List[str], allfiles: Optional[List[str]]) -> Dict[str, Any]: import time from pylint import lint from efro.error import CleanError from efro.terminal import Clr start_time = time.time() args = ['--rcfile', str(pylintrc), '--output-format=colorized'] args += dirtyfiles name = f'{len(dirtyfiles)} file(s)' run = lint.Run(args, do_exit=False) if cache is not None: assert allfiles is not None result = _apply_pylint_run_to_cache(projroot, run, dirtyfiles, allfiles, cache) if result != 0: raise CleanError(f'Pylint failed for {result} file(s).') # Sanity check: when the linter fails we should always be failing too. # If not, it means we're probably missing something and incorrectly # marking a failed file as clean. if run.linter.msg_status != 0 and result == 0: raise RuntimeError('Pylint linter returned non-zero result' ' but we did not; this is probably a bug.') else: if run.linter.msg_status != 0: raise CleanError('Pylint failed.') duration = time.time() - start_time print(f'{Clr.GRN}Pylint passed for {name}' f' in {duration:.1f} seconds.{Clr.RST}') sys.stdout.flush() return {'f': dirtyfiles, 't': duration}
def android_archive_unstripped_libs() -> None: """Copy libs to a build archive.""" import subprocess from pathlib import Path from efro.error import CleanError from efro.terminal import Clr if len(sys.argv) != 4: raise CleanError('Expected 2 args; src-dir and dst-dir') src = Path(sys.argv[2]) dst = Path(sys.argv[3]) if dst.exists(): subprocess.run(['rm', '-rf', dst], check=True) dst.mkdir(parents=True, exist_ok=True) if not src.is_dir(): raise CleanError(f"Source dir not found: '{src}'") libname = 'libmain' libext = '.so' for abi, abishort in [ ('armeabi-v7a', 'arm'), ('arm64-v8a', 'arm64'), ('x86', 'x86'), ('x86_64', 'x86-64'), ]: srcpath = Path(src, abi, libname + libext) dstname = f'{libname}_{abishort}{libext}' dstpath = Path(dst, dstname) if srcpath.exists(): print(f'Archiving unstripped library: {Clr.BLD}{dstname}{Clr.RST}') subprocess.run(['cp', srcpath, dstpath], check=True) subprocess.run(['tar', '-zcf', dstname + '.tgz', dstname], cwd=dst, check=True) subprocess.run(['rm', dstpath], check=True)
def try_repeat() -> None: """Run a command with repeat attempts on failure. First arg is the number of retries; remaining args are the command. """ import subprocess from efro.error import CleanError # We require one number arg and at least one command arg. if len(sys.argv) < 4: raise CleanError( 'Expected a retry-count arg and at least one command arg') try: repeats = int(sys.argv[2]) except Exception: raise CleanError('Expected int as first arg') from None if repeats < 0: raise CleanError('Retries must be >= 0') cmd = sys.argv[3:] for i in range(repeats + 1): result = subprocess.run(cmd, check=False) if result.returncode == 0: return print(f'try_repeat attempt {i + 1} of {repeats + 1} failed for {cmd}.', file=sys.stderr, flush=True) raise CleanError(f'Command failed {repeats + 1} time(s): {cmd}')
def ensure_prefab_platform() -> None: """Ensure we are running on a particular prefab platform.""" import batools.build from efro.error import CleanError if len(sys.argv) != 3: raise CleanError('Expected 1 platform name arg.') needed = sys.argv[2] current = batools.build.get_current_prefab_platform() if current != needed: raise CleanError( f'Incorrect platform: we are {current}, this requires {needed}.')
def runmypy() -> None: """Run mypy checks on provided filenames.""" from efro.terminal import Clr from efro.error import CleanError import efrotools.code if len(sys.argv) < 3: raise CleanError('Expected at least 1 filename arg.') filenames = sys.argv[2:] try: efrotools.code.runmypy(PROJROOT, filenames) print(f'{Clr.GRN}Mypy Passed.{Clr.RST}') except Exception as exc: raise CleanError('Mypy Failed.') from exc
def stage_server_file() -> None: """Stage files for the server environment with some filtering.""" import os import subprocess import batools.build from efro.error import CleanError from efrotools import replace_one from efrotools import PYVER if len(sys.argv) != 5: raise CleanError('Expected 3 args (mode, infile, outfile).') mode, infilename, outfilename = sys.argv[2], sys.argv[3], sys.argv[4] if mode not in ('debug', 'release'): raise CleanError(f"Invalid mode '{mode}'; expected debug or release.") print(f'Building server file: {os.path.basename(outfilename)}') basename = os.path.basename(infilename) if basename == 'config_template.yaml': # Inject all available config values into the config file. batools.build.filter_server_config(str(PROJROOT), infilename, outfilename) elif basename == 'ballisticacore_server.py': # Run Python in opt mode for release builds. with open(infilename) as infile: lines = infile.read().splitlines() if mode == 'release': lines[0] = replace_one(lines[0], f'#!/usr/bin/env python{PYVER}', f'#!/usr/bin/env -S python{PYVER} -O') with open(outfilename, 'w') as outfile: outfile.write('\n'.join(lines) + '\n') subprocess.run(['chmod', '+x', outfilename], check=True) elif basename == 'launch_ballisticacore_server.bat': # Run Python in opt mode for release builds. with open(infilename) as infile: lines = infile.read().splitlines() if mode == 'release': lines[1] = replace_one( lines[1], ':: Python interpreter.', ':: Python interpreter.' ' (in opt mode so we use bundled .opt-1.pyc files)') lines[2] = replace_one( lines[2], 'dist\\\\python.exe ballisticacore_server.py', 'dist\\\\python.exe -O ballisticacore_server.py') with open(outfilename, 'w') as outfile: outfile.write('\n'.join(lines) + '\n') else: raise CleanError(f"Unknown server file for staging: '{basename}'.")
def wsl_path_to_win() -> None: """Forward escape slashes in a provided win path arg.""" import subprocess import logging import os from efro.error import CleanError try: create = False escape = False if len(sys.argv) < 3: raise CleanError('Expected at least 1 path arg.') wsl_path: Optional[str] = None for arg in sys.argv[2:]: if arg == '--create': create = True elif arg == '--escape': escape = True else: if wsl_path is not None: raise CleanError('More than one path provided.') wsl_path = arg if wsl_path is None: raise CleanError('No path provided.') # wslpath fails on nonexistent paths; make it clear when that happens. if create: os.makedirs(wsl_path, exist_ok=True) if not os.path.exists(wsl_path): raise CleanError(f'Path \'{wsl_path}\' does not exist.') results = subprocess.run(['wslpath', '-w', '-a', wsl_path], capture_output=True, check=True) except Exception: # This gets used in a makefile so our returncode is ignored; # let's try to make our failure known in other ways. logging.exception('wsl_to_escaped_win_path failed.') print('wsl_to_escaped_win_path_error_occurred', end='') return out = results.stdout.decode().strip() # If our input ended with a slash, match in the output. if wsl_path.endswith('/') and not out.endswith('\\'): out += '\\' if escape: out = out.replace('\\', '\\\\') print(out, end='')
def urandom_pretty() -> None: """Spits out urandom bytes formatted for source files.""" # Note; this is not especially efficient. It should probably be rewritten # if ever needed in a performance-sensitive context. import os from efro.error import CleanError if len(sys.argv) not in (3, 4): raise CleanError( 'Expected one arg (count) and possibly two (line len).') size = int(sys.argv[2]) linemax = 72 if len(sys.argv) < 4 else int(sys.argv[3]) val = os.urandom(size) lines: list[str] = [] line = b'' for i in range(len(val)): char = val[i:i + 1] thislinelen = len(repr(line + char)) if thislinelen > linemax: lines.append(repr(line)) line = b'' line += char if line: lines.append(repr(line)) bstr = '\n'.join(str(l) for l in lines) print(f'({bstr})')
def run(self) -> None: """Do the thing.""" self._run_cmd(self._build_cmd_args()) assert self._returncode is not None # In some failure cases we may want to run a clean and try again. if self._returncode != 0: # Getting this error sometimes after xcode updates. if 'error: PCH file built from a different branch' in '\n'.join( self._output): print(f'{Clr.MAG}WILL CLEAN AND' f' RE-ATTEMPT XCODE BUILD{Clr.RST}') self._run_cmd([ 'xcodebuild', '-project', self._project, '-scheme', self._scheme, '-configuration', self._configuration, 'clean' ]) # Now re-run the original build. print(f'{Clr.MAG}RE-ATTEMPTING XCODE BUILD' f' AFTER CLEAN{Clr.RST}') self._run_cmd(self._build_cmd_args()) if self._returncode != 0: raise CleanError(f'Command failed with code {self._returncode}.')
def dmypy(projroot: Path) -> None: """Type check all of our scripts using mypy in daemon mode.""" import time from efro.terminal import Clr from efro.error import CleanError filenames = get_script_filenames(projroot) # Special case; explicitly kill the daemon. if '-stop' in sys.argv: subprocess.run(['dmypy', 'stop'], check=False) return print('Running Mypy (daemon)...', flush=True) starttime = time.time() try: args = [ 'dmypy', 'run', '--timeout', '3600', '--', '--config-file', '.mypy.ini', '--follow-imports=error', '--pretty' ] + filenames subprocess.run(args, check=True) except Exception: raise CleanError('Mypy daemon: fail.') duration = time.time() - starttime print(f'{Clr.GRN}Mypy daemon passed in {duration:.1f} seconds.{Clr.RST}', flush=True)
def handle_test_message_1(self, msg: _TMessage1) -> _TResponse1: """Test.""" if msg.ival == 1: raise CleanError('Testing Clean Error') if msg.ival == 2: raise RuntimeError('Testing Runtime Error') return _TResponse1(bval=True)
def lint_file(filename: str) -> None: result = subprocess.call( [f'python{PYVER}', '-m', 'cpplint', '--root=src', filename], env=env) if result != 0: raise CleanError( f'{Clr.RED}Cpplint failed for {filename}.{Clr.RST}')
def __init__(self) -> None: try: self._config = self._load_config() except Exception as exc: raise CleanError(f'Error loading config: {exc}') from exc self._wrapper_shutdown_desired = False self._done = False self._subprocess_commands: List[Union[str, ServerCommand]] = [] self._subprocess_commands_lock = Lock() self._subprocess_force_kill_time: Optional[float] = None self._restart_minutes: Optional[float] = None self._running_interactive = False self._subprocess: Optional[subprocess.Popen[bytes]] = None self._launch_time = time.time() self._subprocess_launch_time: Optional[float] = None self._subprocess_sent_auto_restart = False self._subprocess_sent_clean_exit = False self._subprocess_sent_unclean_exit = False self._subprocess_thread: Optional[Thread] = None # If we don't have any explicit exit conditions set, # we run indefinitely (though we restart our subprocess # periodically to clear out leaks/cruft) if (self._config.clean_exit_minutes is None and self._config.unclean_exit_minutes is None and self._config.idle_exit_minutes is None): self._restart_minutes = 360.0
def tool_config_install() -> None: """Install a tool config file (with some filtering).""" from efro.terminal import Clr from efro.error import CleanError if len(sys.argv) != 4: raise CleanError('expected 2 args') src = Path(sys.argv[2]) dst = Path(sys.argv[3]) print(f'Creating tool config: {Clr.BLD}{dst}{Clr.RST}') with src.open(encoding='utf-8') as infile: cfg = infile.read() # Rome substitutions, etc. cfg = _filter_tool_config(cfg) # Add an auto-generated notice. comment = None if dst.name in ['.dir-locals.el']: comment = ';;' elif dst.name in [ '.mypy.ini', '.pycheckers', '.pylintrc', '.style.yapf', '.clang-format', '.editorconfig' ]: comment = '#' if comment is not None: cfg = (f'{comment} THIS FILE WAS AUTOGENERATED; DO NOT EDIT.\n' f'{comment} Source: {src}.\n\n' + cfg) with dst.open('w', encoding='utf-8') as outfile: outfile.write(cfg)
def pytest() -> None: """Run pytest with project environment set up properly.""" import os import platform import subprocess from efrotools import getconfig, PYTHON_BIN from efro.error import CleanError # Grab our python paths for the project and stuff them in PYTHONPATH. pypaths = getconfig(PROJROOT).get('python_paths') if pypaths is None: raise CleanError('python_paths not found in project config.') separator = ';' if platform.system() == 'Windows' else ':' os.environ['PYTHONPATH'] = separator.join(pypaths) # Also tell Python interpreters not to write __pycache__ dirs everywhere # which can screw up our builds. os.environ['PYTHONDONTWRITEBYTECODE'] = '1' # Do the thing. results = subprocess.run([PYTHON_BIN, '-m', 'pytest'] + sys.argv[2:], check=False) if results.returncode != 0: sys.exit(results.returncode)
def _update_meta_makefile(self) -> None: try: subprocess.run(['tools/pcommand', 'update_meta_makefile'] + self._checkarglist, check=True) except Exception as exc: raise CleanError('Error checking/updating meta Makefile.') from exc
def makefile_target_list() -> None: """Prints targets in a makefile. Takes a single argument: a path to a Makefile. """ from dataclasses import dataclass from efro.error import CleanError from efro.terminal import Clr @dataclass class _Entry: kind: str line: int title: str if len(sys.argv) != 3: raise CleanError('Expected exactly one filename arg.') with open(sys.argv[2], encoding='utf-8') as infile: lines = infile.readlines() def _docstr(lines2: list[str], linenum: int) -> str: doc = '' j = linenum - 1 while j >= 0 and lines2[j].startswith('#'): doc = lines2[j][1:].strip() j -= 1 if doc != '': return ' - ' + doc return doc print('----------------------\n' 'Available Make Targets\n' '----------------------') entries: list[_Entry] = [] for i, line in enumerate(lines): # Targets. if ':' in line and line.split(':')[0].replace('-', '').replace( '_', '').isalnum() and not line.startswith('_'): entries.append( _Entry(kind='target', line=i, title=line.split(':')[0])) # Section titles. if (line.startswith('# ') and line.endswith(' #\n') and len(line.split()) > 2): entries.append( _Entry(kind='section', line=i, title=line[1:-2].strip())) for i, entry in enumerate(entries): if entry.kind == 'section': # Don't print headers for empty sections. if i + 1 >= len(entries) or entries[i + 1].kind == 'section': continue print('\n' + entry.title + '\n' + '-' * len(entry.title)) elif entry.kind == 'target': print(Clr.MAG + entry.title + Clr.BLU + _docstr(lines, entry.line) + Clr.RST)
def standard_message_receiver_gen_pcommand( projroot: Path, basename: str, source_module: str, is_async: bool, get_protocol_call: str = 'get_protocol', embedded: bool = False, ) -> None: """Used by pcommands generating efro.message receiver modules.""" # pylint: disable=too-many-locals import efro.message from efro.terminal import Clr from efro.error import CleanError if len(sys.argv) != 3: raise CleanError('Expected 1 arg: out-path.') dst = sys.argv[2] # Use wrapping-friendly form for long call names. get_protocol_import = (f'({get_protocol_call})' if len(get_protocol_call) >= 14 else get_protocol_call) # In embedded situations we have to pass different code to import # the protocol at build time than we do in our runtime code (where # there is only a dummy import for type-checking purposes) protocol_module_level_import_code: Optional[str] build_time_protocol_create_code: Optional[str] if embedded: protocol_module_level_import_code = ( f'\n# Dummy import for type-checking purposes.\n' f'if bool(False):\n' f' from {source_module} import {get_protocol_import}') protocol_create_code = f'protocol = {get_protocol_call}()' build_time_protocol_create_code = ( f'from {source_module} import {get_protocol_import}\n' f'protocol = {get_protocol_call}()') else: protocol_module_level_import_code = None protocol_create_code = ( f'from {source_module} import {get_protocol_import}\n' f'protocol = {get_protocol_call}()') build_time_protocol_create_code = None module_code = efro.message.create_receiver_module( basename, protocol_create_code=protocol_create_code, protocol_module_level_import_code=protocol_module_level_import_code, build_time_protocol_create_code=build_time_protocol_create_code, is_async=is_async, ) out = format_yapf_str(projroot, module_code) print(f'Meta-building {Clr.BLD}{dst}{Clr.RST}') Path(dst).parent.mkdir(parents=True, exist_ok=True) with open(dst, 'w', encoding='utf-8') as outfile: outfile.write(out)
def lazybuild() -> None: """Run a build command only if an input has changed.""" import subprocess import batools.build from efro.error import CleanError if len(sys.argv) < 5: raise CleanError('Expected at least 3 args') try: category = batools.build.LazyBuildCategory(sys.argv[2]) except ValueError as exc: raise CleanError(exc) from exc target = sys.argv[3] command = ' '.join(sys.argv[4:]) try: batools.build.lazybuild(target, category, command) except subprocess.CalledProcessError as exc: raise CleanError(exc) from exc
def checkenv() -> None: """Check for tools necessary to build and run the app.""" import batools.build from efro.error import CleanError try: batools.build.checkenv() except RuntimeError as exc: raise CleanError(exc)
def stage_server_file() -> None: """Stage files for the server environment with some filtering.""" from efro.error import CleanError import batools.assetstaging if len(sys.argv) != 5: raise CleanError('Expected 3 args (mode, infile, outfile).') mode, infilename, outfilename = sys.argv[2], sys.argv[3], sys.argv[4] batools.assetstaging.stage_server_file(str(PROJROOT), mode, infilename, outfilename)
def gen_binding_code() -> None: """Generate binding.inc file.""" from efro.error import CleanError import batools.meta if len(sys.argv) != 4: raise CleanError('Expected 2 args (srcfile, dstfile)') inpath = sys.argv[2] outpath = sys.argv[3] batools.meta.gen_binding_code(str(PROJROOT), inpath, outpath)
def ensure_prefab_platform() -> None: """Ensure we are running on a particular prefab platform. Note that prefab platform may not exactly match hardware/os. For example, when running in Linux under a WSL environment, the prefab platform may be Windows; not Linux. Also, a 64-bit os may be targeting a 32-bit platform. """ import batools.build from efro.error import CleanError if len(sys.argv) != 3: raise CleanError('Expected 1 platform name arg.') needed = sys.argv[2] current = batools.build.get_current_prefab_platform() if current != needed: raise CleanError( f'Incorrect platform: we are {current}, this requires {needed}.')
def gen_flat_data_code() -> None: """Generate a C++ include file from a Python file.""" from efro.error import CleanError import batools.meta if len(sys.argv) != 5: raise CleanError('Expected 3 args (srcfile, dstfile, varname)') inpath = sys.argv[2] outpath = sys.argv[3] varname = sys.argv[4] batools.meta.gen_flat_data_code(str(PROJROOT), inpath, outpath, varname)
def handle_test_message_1(self, msg: _TMsg1) -> _TResp1: """Test.""" if msg.ival == 1: raise CleanError('Testing Clean Error') if msg.ival == 2: raise RuntimeError('Testing Runtime Error') out = _TResp1(bval=True) if self.test_sidecar: setattr(out, '_sidecar_data', getattr(msg, '_sidecar_data')) return out
def runpylint() -> None: """Run pylint checks on provided filenames.""" from efro.terminal import Clr from efro.error import CleanError import efrotools.code if len(sys.argv) < 3: raise CleanError('Expected at least 1 filename arg.') filenames = sys.argv[2:] efrotools.code.runpylint(PROJROOT, filenames) print(f'{Clr.GRN}Pylint Passed.{Clr.RST}')
def wsl_build_check_win_drive() -> None: """Make sure we're building on a windows drive.""" import os import subprocess import textwrap from efro.error import CleanError if subprocess.run(['which', 'wslpath'], check=False, capture_output=True).returncode != 0: raise CleanError('wslpath not found; you must run' ' this from a WSL environment') if os.environ.get('WSL_BUILD_CHECK_WIN_DRIVE_IGNORE') == '1': return # Get a windows path to the current dir. path = subprocess.run( ['wslpath', '-w', '-a', os.getcwd()], capture_output=True, check=True).stdout.decode().strip() # If we're sitting under the linux filesystem, our path # will start with \\wsl$; fail in that case and explain why. if not path.startswith('\\\\wsl$'): return def _wrap(txt: str) -> str: return textwrap.fill(txt, 76) raise CleanError('\n\n'.join([ _wrap('ERROR: This project appears to live on the Linux filesystem.'), _wrap('Visual Studio compiles will error here for reasons related' ' to Linux filesystem case-sensitivity, and thus are' ' disallowed.' ' Clone the repo to a location that maps to a native' ' Windows drive such as \'/mnt/c/ballistica\' and try again.'), _wrap('Note that WSL2 filesystem performance is poor when accessing' ' native Windows drives, so if Visual Studio builds are not' ' needed it may be best to keep things on the Linux filesystem.' ' This behavior may differ under WSL1 (untested).'), _wrap('Set env-var WSL_BUILD_CHECK_WIN_DRIVE_IGNORE=1 to skip' ' this check.') ]))
def sync_all() -> None: """Runs full syncs between all efrotools projects. This list is defined in the EFROTOOLS_SYNC_PROJECTS env var. This assumes that there is a 'sync-full' and 'sync-list' Makefile target under each project. """ import os import subprocess import concurrent.futures from efro.error import CleanError from efro.terminal import Clr print(f'{Clr.BLD}Updating formatting for all projects...{Clr.RST}') projects_str = os.environ.get('EFROTOOLS_SYNC_PROJECTS') if projects_str is None: raise CleanError('EFROTOOL_SYNC_PROJECTS is not defined.') projects = projects_str.split(':') def _format_project(fproject: str) -> None: fcmd = f'cd "{fproject}" && make format' print(fcmd) subprocess.run(fcmd, shell=True, check=True) # No matter what we're doing (even if just listing), run formatting # in all projects before beginning. Otherwise if we do a sync and then # a preflight we'll often wind up getting out-of-sync errors due to # formatting changing after the sync. with concurrent.futures.ThreadPoolExecutor( max_workers=len(projects)) as executor: # Converting this to a list will propagate any errors. list(executor.map(_format_project, projects)) if len(sys.argv) > 2 and sys.argv[2] == 'list': # List mode for project in projects_str.split(':'): cmd = f'cd "{project}" && make sync-list' print(cmd) subprocess.run(cmd, shell=True, check=True) else: # Real mode for i in range(2): if i == 0: print(Clr.BLD + 'Running sync pass 1:' ' (ensures all changes at dsts are pushed to src)' + Clr.RST) else: print(Clr.BLD + 'Running sync pass 2:' ' (ensures latest src is pulled to all dsts)' + Clr.RST) for project in projects_str.split(':'): cmd = f'cd "{project}" && make sync-full' print(cmd) subprocess.run(cmd, shell=True, check=True) print(Clr.BLD + 'Sync-all successful!' + Clr.RST)
def _update_dummy_module(self) -> None: # Update our dummy _ba module. # Note: This should happen near the end because it may run the cmake # build so its success may depend on the cmake build files having # already been updated. try: subprocess.run(['tools/pcommand', 'update_dummy_module'] + self._checkarglist, check=True) except Exception as exc: raise CleanError('Error checking/updating dummy module.') from exc