mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-19 21:37:57 +08:00
946 lines
27 KiB
Python
946 lines
27 KiB
Python
# Released under the MIT License. See LICENSE for details.
|
|
#
|
|
"""A set of lovely pcommands ready for use."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from typing import TYPE_CHECKING
|
|
|
|
from efrotools import pcommand
|
|
|
|
if TYPE_CHECKING:
|
|
pass
|
|
|
|
|
|
def _spelling(words: list[str]) -> None:
|
|
from efrotools.code import sort_jetbrains_dict
|
|
import os
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
num_modded_dictionaries = 0
|
|
for fname in [
|
|
'.idea/dictionaries/ericf.xml',
|
|
'ballisticakit-cmake/.idea/dictionaries/ericf.xml',
|
|
]:
|
|
if not os.path.exists(fname):
|
|
continue
|
|
with open(fname, encoding='utf-8') as infile:
|
|
lines = infile.read().splitlines()
|
|
if lines[2] != ' <words>':
|
|
raise RuntimeError('Unexpected dictionary format.')
|
|
added_count = 0
|
|
for word in words:
|
|
line = f' <w>{word.lower()}</w>'
|
|
if line not in lines:
|
|
lines.insert(3, line)
|
|
added_count += 1
|
|
|
|
with open(fname, 'w', encoding='utf-8') as outfile:
|
|
outfile.write(sort_jetbrains_dict('\n'.join(lines)))
|
|
|
|
print(f'Added {added_count} words to {fname}.')
|
|
num_modded_dictionaries += 1
|
|
print(f'Modified {num_modded_dictionaries} dictionaries.')
|
|
|
|
|
|
def requirements_upgrade() -> None:
|
|
"""Upgrade project requirements."""
|
|
import os
|
|
import tempfile
|
|
import subprocess
|
|
|
|
from efro.error import CleanError
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
args = pcommand.get_args()
|
|
|
|
if len(args) != 1:
|
|
raise CleanError('Expected a single arg.')
|
|
reqpath = args[0]
|
|
|
|
with open(reqpath, encoding='utf-8') as infile:
|
|
reqs = infile.read()
|
|
|
|
# Operate on a temp file and compare against our existing so we don't
|
|
# write unless it has changed.
|
|
with tempfile.TemporaryDirectory() as tempdir:
|
|
fname = os.path.join(tempdir, 'reqs')
|
|
with open(fname, 'w', encoding='utf-8') as outfile:
|
|
outfile.write(reqs)
|
|
|
|
subprocess.run([sys.executable, '-m', 'pur', '-r', fname], check=True)
|
|
|
|
# Sort lines.
|
|
with open(fname, encoding='utf-8') as infile:
|
|
reqs2 = infile.read().strip()
|
|
reqs_new = (
|
|
'\n'.join(sorted(reqs2.splitlines(), key=lambda l: l.lower()))
|
|
+ '\n'
|
|
)
|
|
|
|
if reqs_new != reqs:
|
|
with open(reqpath, 'w', encoding='utf-8') as outfile:
|
|
outfile.write(reqs_new)
|
|
|
|
|
|
def spelling_all() -> None:
|
|
"""Add all misspellings from a pycharm run."""
|
|
import subprocess
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
print('Running "make pycharm-full"...')
|
|
lines = [
|
|
line
|
|
for line in subprocess.run(
|
|
['make', 'pycharm-full'], check=False, capture_output=True
|
|
)
|
|
.stdout.decode()
|
|
.splitlines()
|
|
if 'Typo: In word' in line
|
|
]
|
|
words = [line.split('Typo: In word')[1].strip() for line in lines]
|
|
|
|
# Strip enclosing quotes but not internal ones.
|
|
for i, word in enumerate(words):
|
|
assert word[0] == "'"
|
|
assert word[-1] == "'"
|
|
words[i] = word[1:-1]
|
|
|
|
_spelling(words)
|
|
|
|
|
|
def spelling() -> None:
|
|
"""Add words to the PyCharm dictionary."""
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
_spelling(sys.argv[2:])
|
|
|
|
|
|
def xcodebuild() -> None:
|
|
"""Run xcodebuild with added smarts."""
|
|
from efrotools.xcodebuild import XCodeBuild
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
XCodeBuild(projroot=str(pcommand.PROJROOT), args=sys.argv[2:]).run()
|
|
|
|
|
|
def _xcodepath(executable: bool) -> str:
|
|
import os
|
|
from efro.error import CleanError
|
|
from efrotools.xcodebuild import project_build_path
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
if len(sys.argv) != 5:
|
|
raise CleanError(
|
|
'Expected 3 args: <xcode project path> <configuration name>'
|
|
)
|
|
project_path = os.path.abspath(sys.argv[2])
|
|
scheme = sys.argv[3]
|
|
configuration = sys.argv[4]
|
|
return project_build_path(
|
|
projroot=str(pcommand.PROJROOT),
|
|
project_path=project_path,
|
|
scheme=scheme,
|
|
configuration=configuration,
|
|
executable=executable,
|
|
)
|
|
|
|
|
|
def xcodeshow() -> None:
|
|
"""Open folder containing xcode build in the finder."""
|
|
import subprocess
|
|
|
|
subprocess.run(['open', _xcodepath(executable=False)], check=True)
|
|
|
|
|
|
def xcoderun() -> None:
|
|
"""Run an xcode build in the terminal."""
|
|
import subprocess
|
|
|
|
path = _xcodepath(executable=True)
|
|
subprocess.run(path, check=True)
|
|
|
|
|
|
def pyver() -> None:
|
|
"""Prints the Python version used by this project."""
|
|
from efrotools.pyver import PYVER
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
print(PYVER, end='')
|
|
|
|
|
|
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
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
# 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 check_clean_safety() -> None:
|
|
"""Ensure all files are are added to git or in gitignore.
|
|
|
|
Use to avoid losing work if we accidentally do a clean without
|
|
adding something.
|
|
"""
|
|
import os
|
|
import subprocess
|
|
from efro.error import CleanError
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
if len(sys.argv) != 2:
|
|
raise CleanError('invalid arguments')
|
|
|
|
# Make sure we wouldn't be deleting anything not tracked by git
|
|
# or ignored.
|
|
output = subprocess.check_output(
|
|
['git', 'status', '--porcelain=v2']
|
|
).decode()
|
|
if any(line.startswith('?') for line in output.splitlines()):
|
|
raise CleanError(
|
|
'Untracked file(s) found; aborting.'
|
|
' (see "git status" from "'
|
|
+ os.getcwd()
|
|
+ '") Either \'git add\' them, add them to .gitignore,'
|
|
' or remove them and try again.'
|
|
)
|
|
|
|
|
|
def gen_empty_py_init() -> None:
|
|
"""Generate an empty __init__.py for a package dir.
|
|
|
|
Used as part of meta builds.
|
|
"""
|
|
from pathlib import Path
|
|
|
|
from efro.terminal import Clr
|
|
from efro.error import CleanError
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
if len(sys.argv) != 3:
|
|
raise CleanError('Expected a single path arg.')
|
|
|
|
outpath = Path(sys.argv[2])
|
|
outpath.parent.mkdir(parents=True, exist_ok=True)
|
|
print(f'Meta-building {Clr.BLD}{outpath}{Clr.RST}')
|
|
with open(outpath, 'w', encoding='utf-8') as outfile:
|
|
outfile.write('# This file is autogenerated; do not hand-edit.\n')
|
|
|
|
|
|
def formatcode() -> None:
|
|
"""Format all of our C/C++/etc. code."""
|
|
import efrotools.code
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
full = '-full' in sys.argv
|
|
efrotools.code.format_project_cpp_files(pcommand.PROJROOT, full)
|
|
|
|
|
|
def formatscripts() -> None:
|
|
"""Format all of our Python/etc. code."""
|
|
import efrotools.code
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
full = '-full' in sys.argv
|
|
efrotools.code.format_project_python_files(pcommand.PROJROOT, full)
|
|
|
|
|
|
def formatmakefile() -> None:
|
|
"""Format the main makefile."""
|
|
from efrotools.makefile import Makefile
|
|
|
|
with open('Makefile', encoding='utf-8') as infile:
|
|
original = infile.read()
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
formatted = Makefile(original).get_output()
|
|
|
|
# Only write if it changed.
|
|
if formatted != original:
|
|
with open('Makefile', 'w', encoding='utf-8') as outfile:
|
|
outfile.write(formatted)
|
|
|
|
|
|
def cpplint() -> None:
|
|
"""Run lint-checking on all code deemed lint-able."""
|
|
import efrotools.code
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
full = '-full' in sys.argv
|
|
efrotools.code.check_cpplint(pcommand.PROJROOT, full)
|
|
|
|
|
|
def scriptfiles() -> None:
|
|
"""List project script files.
|
|
|
|
Pass -lines to use newlines as separators. The default is spaces.
|
|
"""
|
|
import efrotools.code
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
paths = efrotools.code.get_script_filenames(projroot=pcommand.PROJROOT)
|
|
assert not any(' ' in path for path in paths)
|
|
if '-lines' in sys.argv:
|
|
print('\n'.join(paths))
|
|
else:
|
|
print(' '.join(paths))
|
|
|
|
|
|
def pylint() -> None:
|
|
"""Run pylint checks on our scripts."""
|
|
import efrotools.code
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
full = '-full' in sys.argv
|
|
fast = '-fast' in sys.argv
|
|
efrotools.code.pylint(pcommand.PROJROOT, full, fast)
|
|
|
|
|
|
def pylint_files() -> None:
|
|
"""Run pylint checks on provided filenames."""
|
|
from efro.terminal import Clr
|
|
from efro.error import CleanError
|
|
import efrotools.code
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
if len(sys.argv) < 3:
|
|
raise CleanError('Expected at least 1 filename arg.')
|
|
|
|
filenames = sys.argv[2:]
|
|
efrotools.code.runpylint(pcommand.PROJROOT, filenames)
|
|
print(f'{Clr.GRN}Pylint Passed.{Clr.RST}')
|
|
|
|
|
|
def mypy() -> None:
|
|
"""Run mypy checks on our scripts."""
|
|
import efrotools.code
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
full = '-full' in sys.argv
|
|
efrotools.code.mypy(pcommand.PROJROOT, full)
|
|
|
|
|
|
def mypy_files() -> None:
|
|
"""Run mypy checks on provided filenames."""
|
|
from efro.terminal import Clr
|
|
from efro.error import CleanError
|
|
import efrotools.code
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
if len(sys.argv) < 3:
|
|
raise CleanError('Expected at least 1 filename arg.')
|
|
|
|
filenames = sys.argv[2:]
|
|
try:
|
|
efrotools.code.mypy_files(pcommand.PROJROOT, filenames)
|
|
print(f'{Clr.GRN}Mypy Passed.{Clr.RST}')
|
|
except Exception as exc:
|
|
raise CleanError('Mypy Failed.') from exc
|
|
|
|
|
|
def dmypy() -> None:
|
|
"""Run mypy checks on our scripts using the mypy daemon."""
|
|
import efrotools.code
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
efrotools.code.dmypy(pcommand.PROJROOT)
|
|
|
|
|
|
def pycharm() -> None:
|
|
"""Run PyCharm checks on our scripts."""
|
|
import efrotools.code
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
full = '-full' in sys.argv
|
|
verbose = '-v' in sys.argv
|
|
efrotools.code.check_pycharm(pcommand.PROJROOT, full, verbose)
|
|
|
|
|
|
def clioncode() -> None:
|
|
"""Run CLion checks on our code."""
|
|
import efrotools.code
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
full = '-full' in sys.argv
|
|
verbose = '-v' in sys.argv
|
|
efrotools.code.check_clioncode(pcommand.PROJROOT, full, verbose)
|
|
|
|
|
|
def androidstudiocode() -> None:
|
|
"""Run Android Studio checks on our code."""
|
|
import efrotools.code
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
full = '-full' in sys.argv
|
|
verbose = '-v' in sys.argv
|
|
efrotools.code.check_android_studio(pcommand.PROJROOT, full, verbose)
|
|
|
|
|
|
def tool_config_install() -> None:
|
|
"""Install a tool config file (with some filtering)."""
|
|
from pathlib import Path
|
|
|
|
from efro.error import CleanError
|
|
|
|
import efrotools.toolconfig
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
if len(sys.argv) != 4:
|
|
raise CleanError('expected 2 args')
|
|
|
|
src = Path(sys.argv[2])
|
|
dst = Path(sys.argv[3])
|
|
|
|
efrotools.toolconfig.install_tool_config(pcommand.PROJROOT, src, dst)
|
|
|
|
|
|
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
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
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(
|
|
f'{Clr.BLD}Running sync pass 1'
|
|
f' (ensures all changes at dsts are pushed to src):'
|
|
f'{Clr.RST}'
|
|
)
|
|
else:
|
|
print(
|
|
f'{Clr.BLD}Running sync pass 2'
|
|
f' (ensures latest src is pulled to all dsts):{Clr.RST}'
|
|
)
|
|
for project in projects_str.split(':'):
|
|
cmd = f'cd "{project}" && make sync-full'
|
|
subprocess.run(cmd, shell=True, check=True)
|
|
print(Clr.BLD + 'Sync-all successful!' + Clr.RST)
|
|
|
|
|
|
def sync() -> None:
|
|
"""Runs standard syncs between this project and others."""
|
|
from efrotools.project import getprojectconfig
|
|
from efrotools.sync import Mode, SyncItem, run_standard_syncs
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
mode = Mode(sys.argv[2]) if len(sys.argv) > 2 else Mode.PULL
|
|
|
|
# Load sync-items from project config and run them
|
|
sync_items = [
|
|
SyncItem(**i)
|
|
for i in getprojectconfig(pcommand.PROJROOT).get('sync_items', [])
|
|
]
|
|
run_standard_syncs(pcommand.PROJROOT, mode, sync_items)
|
|
|
|
|
|
def copy_win_extra_file() -> None:
|
|
"""Copy a windows extra file."""
|
|
_simple_file_copy('Copying file')
|
|
|
|
|
|
def compile_language_file() -> None:
|
|
"""Compile a language file."""
|
|
_simple_file_copy('Compiling language json')
|
|
|
|
|
|
def compile_mesh_file() -> None:
|
|
"""Compile a mesh file."""
|
|
import os
|
|
import subprocess
|
|
from efro.error import CleanError
|
|
|
|
args = pcommand.get_args()
|
|
if len(args) != 3:
|
|
raise CleanError('Expected 3 args.')
|
|
|
|
src, dst, makebob = args
|
|
|
|
# Show project-relative paths when possible.
|
|
relpath = os.path.abspath(dst).removeprefix(f'{pcommand.PROJROOT}/')
|
|
pcommand.clientprint(f'Compiling mesh: {relpath}')
|
|
|
|
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
|
subprocess.run([makebob, src, dst], check=True)
|
|
|
|
assert os.path.exists(dst)
|
|
|
|
|
|
def compile_collision_mesh_file() -> None:
|
|
"""Compile a mesh file."""
|
|
import os
|
|
import subprocess
|
|
from efro.error import CleanError
|
|
|
|
args = pcommand.get_args()
|
|
if len(args) != 3:
|
|
raise CleanError('Expected 3 args.')
|
|
|
|
src, dst, makebob = args
|
|
|
|
# Show project-relative paths when possible.
|
|
relpath = os.path.abspath(dst).removeprefix(f'{pcommand.PROJROOT}/')
|
|
pcommand.clientprint(f'Compiling collision mesh: {relpath}')
|
|
|
|
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
|
subprocess.run([makebob, src, dst], check=True)
|
|
|
|
assert os.path.exists(dst)
|
|
|
|
|
|
def compile_python_file() -> None:
|
|
"""Compile pyc files for packaging.
|
|
|
|
This creates hash-based PYC files in opt level 1 with hash checks
|
|
defaulting to off, so we don't have to worry about timestamps or
|
|
loading speed hits due to hash checks. (see PEP 552).
|
|
We just need to tell modders that they'll need to clear these
|
|
cache files out or turn on debugging mode if they want to tweak
|
|
the built-in scripts directly (or go through the asset build system which
|
|
properly recreates the .pyc files).
|
|
"""
|
|
import os
|
|
import py_compile
|
|
|
|
from efro.error import CleanError
|
|
|
|
args = pcommand.get_args()
|
|
if len(args) != 1:
|
|
raise CleanError('Expected a single arg.')
|
|
fname = args[0]
|
|
|
|
# Show project-relative path when possible.
|
|
relpath = os.path.abspath(fname).removeprefix(f'{pcommand.PROJROOT}/')
|
|
pcommand.clientprint(f'Compiling script: {relpath}')
|
|
|
|
py_compile.compile(
|
|
fname,
|
|
doraise=True,
|
|
optimize=1,
|
|
invalidation_mode=py_compile.PycInvalidationMode.UNCHECKED_HASH,
|
|
)
|
|
|
|
|
|
def copy_python_file() -> None:
|
|
"""Copy Python files for packaging."""
|
|
_simple_file_copy('Copying script', make_unwritable=True)
|
|
|
|
|
|
def _simple_file_copy(msg: str, make_unwritable: bool = False) -> None:
|
|
import os
|
|
import shutil
|
|
from efro.error import CleanError
|
|
|
|
args = pcommand.get_args()
|
|
if len(args) != 2:
|
|
raise CleanError('Expected 2 args.')
|
|
|
|
src, dst = args
|
|
|
|
relpath = os.path.abspath(dst).removeprefix(f'{pcommand.PROJROOT}/')
|
|
pcommand.clientprint(f'{msg}: {relpath}')
|
|
|
|
# If we're making built files unwritable, we need to blow
|
|
# away exiting ones to allow this to succeed.
|
|
if make_unwritable:
|
|
if os.path.exists(dst):
|
|
os.unlink(dst)
|
|
|
|
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
|
shutil.copyfile(src, dst)
|
|
|
|
assert os.path.exists(dst)
|
|
|
|
# Make built files unwritable to save myself from accidentally
|
|
# doing editing there and then blowing away my work.
|
|
if make_unwritable:
|
|
os.chmod(dst, 0o444)
|
|
|
|
|
|
def compile_font_file() -> None:
|
|
"""Compile a font file."""
|
|
_simple_file_copy('Compiling font')
|
|
|
|
|
|
def pytest() -> None:
|
|
"""Run pytest with project environment set up properly."""
|
|
import os
|
|
import platform
|
|
import subprocess
|
|
from efrotools.project import getprojectconfig
|
|
from efro.error import CleanError
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
# Grab our python paths for the project and stuff them in PYTHONPATH.
|
|
pypaths = getprojectconfig(pcommand.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'
|
|
|
|
# Let's flip on dev mode to hopefully be informed on more bad stuff
|
|
# happening. https://docs.python.org/3/library/devmode.html
|
|
os.environ['PYTHONDEVMODE'] = '1'
|
|
|
|
# Do the thing.
|
|
results = subprocess.run(
|
|
[sys.executable, '-m', 'pytest'] + sys.argv[2:], check=False
|
|
)
|
|
if results.returncode != 0:
|
|
sys.exit(results.returncode)
|
|
|
|
|
|
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
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
@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 echo() -> None:
|
|
"""Echo with support for efro.terminal.Clr args (RED, GRN, BLU, etc).
|
|
|
|
Prints a Clr.RST at the end so that can be omitted.
|
|
"""
|
|
clr = pcommand.clr()
|
|
|
|
clrnames = {n for n in dir(clr) if n.isupper() and not n.startswith('_')}
|
|
first = True
|
|
out: list[str] = []
|
|
last_was_tag = False
|
|
for arg in pcommand.get_args():
|
|
if arg in clrnames:
|
|
out.append(getattr(clr, arg))
|
|
last_was_tag = True
|
|
else:
|
|
# Special case: punctuation by itself after a tag doesn't
|
|
# get a space before it.
|
|
if not first and not (last_was_tag and arg in ('.', '?', '!')):
|
|
out.append(' ')
|
|
first = False
|
|
last_was_tag = False
|
|
out.append(arg)
|
|
out.append(clr.RST)
|
|
pcommand.clientprint(''.join(out))
|
|
|
|
|
|
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
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
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 tweak_empty_py_files() -> None:
|
|
"""Find any zero-length Python files and make them length 1."""
|
|
from efro.error import CleanError
|
|
import efrotools.pybuild
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
if len(sys.argv) != 3:
|
|
raise CleanError('Expected exactly 1 path arg.')
|
|
efrotools.pybuild.tweak_empty_py_files(sys.argv[2])
|
|
|
|
|
|
def make_ensure() -> None:
|
|
"""Make sure a makefile target is up-to-date.
|
|
|
|
This can technically be done by simply `make --question`, but this
|
|
has some extra bells and whistles such as printing some of the commands
|
|
that would run.
|
|
Can be useful to run after cloud-builds to ensure the local results
|
|
consider themselves up-to-date.
|
|
"""
|
|
# pylint: disable=too-many-locals
|
|
from efro.error import CleanError
|
|
from efro.terminal import Clr
|
|
import subprocess
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
dirpath: str | None = None
|
|
args = sys.argv[2:]
|
|
if '--dir' in args:
|
|
argindex = args.index('--dir')
|
|
dirpath = args[argindex + 1]
|
|
del args[argindex : argindex + 2]
|
|
|
|
if len(args) not in (0, 1):
|
|
raise CleanError('Expected zero or one target args.')
|
|
target = args[0] if args else None
|
|
|
|
cmd = ['make', '--no-print-directory', '--dry-run']
|
|
if target is not None:
|
|
cmd.append(target)
|
|
results = subprocess.run(cmd, check=False, capture_output=True, cwd=dirpath)
|
|
out = results.stdout.decode()
|
|
err = results.stderr.decode()
|
|
if results.returncode != 0:
|
|
print(f'Failed command stdout:\n{out}\nFailed command stderr:\n{err}')
|
|
raise CleanError(f"Command failed during make_ensure: '{cmd}'.")
|
|
|
|
targetname: str = '<default>' if target is None else target
|
|
lines = out.splitlines()
|
|
in_str = '' if dirpath is None else f"in directory '{dirpath}' "
|
|
if len(lines) == 1 and 'Nothing to be done for ' in lines[0]:
|
|
print(f"make_ensure: '{targetname}' target {in_str}is up to date.")
|
|
else:
|
|
maxlines = 20
|
|
if len(lines) > maxlines:
|
|
outlines = '\n'.join(
|
|
lines[:maxlines] + [f'(plus {len(lines)-maxlines} more lines)']
|
|
)
|
|
else:
|
|
outlines = '\n'.join(lines)
|
|
|
|
print(
|
|
f"make_ensure: '{targetname}' target {in_str}"
|
|
f'is out of date; would run:\n\n'
|
|
'-------------------------- MAKE-ENSURE COMMANDS BEGIN '
|
|
f'--------------------------\n{Clr.YLW}'
|
|
f'{outlines}{Clr.RST}\n'
|
|
'--------------------------- MAKE-ENSURE COMMANDS END '
|
|
'---------------------------\n'
|
|
)
|
|
raise CleanError(
|
|
f"make_ensure: '{targetname}' target {in_str}is out of date."
|
|
)
|
|
|
|
|
|
def make_target_debug() -> None:
|
|
"""Debug makefile src/target mod times given src and dst path.
|
|
|
|
Built to debug stubborn Makefile targets that insist on being
|
|
rebuilt just after being built via a cloud target.
|
|
"""
|
|
import os
|
|
import datetime
|
|
|
|
from efro.error import CleanError
|
|
|
|
pcommand.disallow_in_batch()
|
|
|
|
# from efro.util import ago_str, utc_now
|
|
|
|
args = sys.argv[2:]
|
|
if len(args) != 2:
|
|
raise CleanError('Expected 2 args.')
|
|
|
|
def _utc_mod_time(path: str) -> datetime.datetime:
|
|
mtime = os.path.getmtime(path)
|
|
mdtime = datetime.datetime.fromtimestamp(mtime, datetime.timezone.utc)
|
|
# mdtime.replace(tzinfo=datetime.timezone.utc)
|
|
return mdtime
|
|
|
|
# srcname = os.path.basename(args[0])
|
|
# dstname = os.path.basename(args[1])
|
|
srctime = _utc_mod_time(args[0])
|
|
dsttime = _utc_mod_time(args[1])
|
|
# now = utc_now()
|
|
# src_ago = ago_str(srctime, maxparts=3, decimals=2, now=now)
|
|
# dst_ago = ago_str(dsttime, maxparts=3, decimals=2, now=now)
|
|
srctimestr = (
|
|
f'{srctime.hour}:{srctime.minute}:{srctime.second}:'
|
|
f'{srctime.microsecond}'
|
|
)
|
|
dsttimestr = (
|
|
f'{dsttime.hour}:{dsttime.minute}:{dsttime.second}:'
|
|
f'{dsttime.microsecond}'
|
|
)
|
|
print(f'SRC modified at {srctimestr}.')
|
|
print(f'DST modified at {dsttimestr}.')
|