diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml
index 034c70ec..e805fcfa 100644
--- a/.idea/dictionaries/ericf.xml
+++ b/.idea/dictionaries/ericf.xml
@@ -553,6 +553,7 @@
errored
erroring
errorsound
+ errstr
etcpack
etsel
etxt
@@ -867,6 +868,7 @@
inext
infileb
infilename
+ infilepath
infos
infotextcolor
inidividual
@@ -1259,6 +1261,7 @@
outdelay
outext
outfilename
+ outfilepath
outname
outpath
ouya
diff --git a/assets/.asset_manifest_public.json b/assets/.asset_manifest_public.json
index c5c5457c..6cc7b59f 100644
--- a/assets/.asset_manifest_public.json
+++ b/assets/.asset_manifest_public.json
@@ -454,6 +454,7 @@
"ba_data/python/efro/__init__.py",
"ba_data/python/efro/__pycache__/__init__.cpython-37.opt-1.pyc",
"ba_data/python/efro/__pycache__/dataclassutils.cpython-37.opt-1.pyc",
+ "ba_data/python/efro/__pycache__/error.cpython-37.opt-1.pyc",
"ba_data/python/efro/__pycache__/executils.cpython-37.opt-1.pyc",
"ba_data/python/efro/__pycache__/jsonutils.cpython-37.opt-1.pyc",
"ba_data/python/efro/__pycache__/terminal.cpython-37.opt-1.pyc",
@@ -473,6 +474,7 @@
"ba_data/python/efro/entity/_support.py",
"ba_data/python/efro/entity/_value.py",
"ba_data/python/efro/entity/util.py",
+ "ba_data/python/efro/error.py",
"ba_data/python/efro/executils.py",
"ba_data/python/efro/jsonutils.py",
"ba_data/python/efro/terminal.py",
diff --git a/assets/Makefile b/assets/Makefile
index 46b230e3..98acb2c1 100644
--- a/assets/Makefile
+++ b/assets/Makefile
@@ -1722,6 +1722,7 @@ build/ba_data/python/bastd/session/__pycache__/__init__.cpython-37.opt-1.pyc: \
SCRIPT_TARGETS_PY_PUBLIC_TOOLS = \
build/ba_data/python/efro/executils.py \
+ build/ba_data/python/efro/error.py \
build/ba_data/python/efro/dataclassutils.py \
build/ba_data/python/efro/terminal.py \
build/ba_data/python/efro/util.py \
@@ -1741,6 +1742,7 @@ SCRIPT_TARGETS_PY_PUBLIC_TOOLS = \
SCRIPT_TARGETS_PYC_PUBLIC_TOOLS = \
build/ba_data/python/efro/__pycache__/executils.cpython-37.opt-1.pyc \
+ build/ba_data/python/efro/__pycache__/error.cpython-37.opt-1.pyc \
build/ba_data/python/efro/__pycache__/dataclassutils.cpython-37.opt-1.pyc \
build/ba_data/python/efro/__pycache__/terminal.cpython-37.opt-1.pyc \
build/ba_data/python/efro/__pycache__/util.cpython-37.opt-1.pyc \
@@ -1776,6 +1778,11 @@ build/ba_data/python/efro/__pycache__/executils.cpython-37.opt-1.pyc: \
@echo Compiling script: $^
@rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@
+build/ba_data/python/efro/__pycache__/error.cpython-37.opt-1.pyc: \
+ build/ba_data/python/efro/error.py
+ @echo Compiling script: $^
+ @rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@
+
build/ba_data/python/efro/__pycache__/dataclassutils.cpython-37.opt-1.pyc: \
build/ba_data/python/efro/dataclassutils.py
@echo Compiling script: $^
diff --git a/docs/ba_module.md b/docs/ba_module.md
index 3bdd59a3..9aefc5d4 100644
--- a/docs/ba_module.md
+++ b/docs/ba_module.md
@@ -1,5 +1,5 @@
-
last updated on 2020-04-30 for Ballistica version 1.5.0 build 20001
+last updated on 2020-05-01 for Ballistica version 1.5.0 build 20001
This page documents the Python classes and functions in the 'ba' module,
which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please let me know. Happy modding!
diff --git a/tools/bacloud b/tools/bacloud
index b4683014..46d7924a 100755
--- a/tools/bacloud
+++ b/tools/bacloud
@@ -36,6 +36,7 @@ import tempfile
import requests
+from efro.error import CleanError
from efro.terminal import Clr
if TYPE_CHECKING:
@@ -115,10 +116,6 @@ class Response:
end_command: Optional[Tuple[str, Dict]] = None
-class CleanError(Exception):
- """Exception resulting in a clean error string print and exit."""
-
-
def get_tz_offset_seconds() -> float:
"""Return the offset between utc and local time in seconds."""
import time
@@ -443,6 +440,5 @@ if __name__ == '__main__':
# Can make this optional if a backtrace is ever useful..
sys.exit(-1)
except CleanError as exc:
- if str(exc):
- print(f'{Clr.SRED}{exc}{Clr.RST}')
+ exc.print()
sys.exit(-1)
diff --git a/tools/batools/android.py b/tools/batools/android.py
new file mode 100644
index 00000000..8fe178d9
--- /dev/null
+++ b/tools/batools/android.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python3.7
+# Copyright (c) 2011-2020 Eric Froemling
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+# -----------------------------------------------------------------------------
+"""Functionality related to android builds."""
+from __future__ import annotations
+
+import os
+import sys
+import subprocess
+from typing import TYPE_CHECKING
+
+import efrotools
+
+if TYPE_CHECKING:
+ pass
+
+
+def androidaddr(archive_dir: str, arch: str, addr: str) -> None:
+ """Print the source file location for an android program-counter.
+
+ command line args: archive_dir architecture addr
+ """
+ if not os.path.isdir(archive_dir):
+ print('ERROR: invalid archive dir: "' + archive_dir + '"')
+ sys.exit(255)
+ archs = {
+ 'x86': {
+ 'prefix': 'x86-',
+ 'libmain': 'libmain_x86.so'
+ },
+ 'arm': {
+ 'prefix': 'arm-',
+ 'libmain': 'libmain_arm.so'
+ },
+ 'arm64': {
+ 'prefix': 'aarch64-',
+ 'libmain': 'libmain_arm64.so'
+ },
+ 'x86-64': {
+ 'prefix': 'x86_64-',
+ 'libmain': 'libmain_x86-64.so'
+ }
+ }
+ if arch not in archs:
+ print('ERROR: invalid arch "' + arch + '"; (choices are ' +
+ ', '.join(archs.keys()) + ')')
+ sys.exit(255)
+ sdkutils = 'tools/android_sdk_utils'
+ rootdir = '.'
+ ndkpath = subprocess.check_output([sdkutils,
+ 'get-ndk-path']).decode().strip()
+ if not os.path.isdir(ndkpath):
+ print("ERROR: ndk-path '" + ndkpath + '" does not exist')
+ sys.exit(255)
+ lines = subprocess.check_output(
+ ['find',
+ os.path.join(ndkpath, 'toolchains'), '-name',
+ '*addr2line']).decode().strip().splitlines()
+ lines = [line for line in lines if archs[arch]['prefix'] in line]
+ if len(lines) != 1:
+ print("ERROR: couldn't find addr2line binary")
+ sys.exit(255)
+ addr2line = lines[0]
+ efrotools.run('mkdir -p "' + os.path.join(rootdir, 'android_addr_tmp') +
+ '"')
+ try:
+ efrotools.run('cd "' + os.path.join(rootdir, 'android_addr_tmp') +
+ '" && tar -xf "' +
+ os.path.join(archive_dir, 'unstripped_libs',
+ archs[arch]['libmain'] + '.tgz') + '"')
+ efrotools.run(
+ addr2line + ' -e "' +
+ os.path.join(rootdir, 'android_addr_tmp', archs[arch]['libmain']) +
+ '" ' + addr)
+ finally:
+ os.system('rm -rf "' + os.path.join(rootdir, 'android_addr_tmp') + '"')
diff --git a/tools/batools/build.py b/tools/batools/build.py
index 65af5c05..153cc147 100644
--- a/tools/batools/build.py
+++ b/tools/batools/build.py
@@ -577,9 +577,10 @@ def update_makebob() -> None:
print('All builds complete!', flush=True)
-def _get_server_config_raw_contents() -> str:
+def _get_server_config_raw_contents(projroot: str) -> str:
import textwrap
- with open('tools/bacommon/servermanager.py') as infile:
+ with open(os.path.join(projroot,
+ 'tools/bacommon/servermanager.py')) as infile:
lines = infile.read().splitlines()
firstline = lines.index('class ServerConfig:') + 1
lastline = firstline + 1
@@ -601,9 +602,9 @@ def _get_server_config_raw_contents() -> str:
return textwrap.dedent('\n'.join(lines[firstline:lastline + 1]))
-def _get_server_config_template_yaml() -> str:
+def _get_server_config_template_yaml(projroot: str) -> str:
import yaml
- lines_in = _get_server_config_raw_contents().splitlines()
+ lines_in = _get_server_config_raw_contents(projroot).splitlines()
lines_out: List[str] = []
for line in lines_in:
if line != '' and not line.startswith('#'):
@@ -628,11 +629,65 @@ def _get_server_config_template_yaml() -> str:
return '\n'.join(lines_out)
-def filter_server_config(infilename: str, outfilename: str) -> None:
+def filter_server_config(projroot: str, infilepath: str,
+ outfilepath: str) -> None:
"""Add commented-out config options to a server config."""
- with open(infilename) as infile:
+ with open(infilepath) as infile:
cfg = infile.read()
cfg = cfg.replace('#__CONFIG_TEMPLATE_VALUES__',
- _get_server_config_template_yaml())
- with open(outfilename, 'w') as outfile:
+ _get_server_config_template_yaml(projroot))
+ with open(outfilepath, 'w') as outfile:
outfile.write(cfg)
+
+
+def update_docs_md(check: bool) -> None:
+ """Updates docs markdown files if necessary."""
+ # pylint: disable=too-many-locals
+ from efrotools import get_files_hash, run
+
+ docs_path = 'docs/ba_module.md'
+
+ # We store the hash in a separate file that only exists on private
+ # so public isn't full of constant hash change commits.
+ # (don't care so much on private)
+ docs_hash_path = 'docs/ba_module_hash'
+
+ # Generate a hash from all c/c++ sources under the python subdir
+ # as well as all python scripts.
+ pysources = []
+ exts = ['.cc', '.c', '.h', '.py']
+ for basedir in [
+ 'src/ballistica/python',
+ 'tools/efro',
+ 'tools/bacommon',
+ 'assets/src/ba_data/python/ba',
+ ]:
+ assert os.path.isdir(basedir), f'{basedir} is not a dir.'
+ for root, _dirs, files in os.walk(basedir):
+ for fname in files:
+ if any(fname.endswith(ext) for ext in exts):
+ pysources.append(os.path.join(root, fname))
+ curhash = get_files_hash(pysources)
+
+ # Extract the current embedded hash.
+ with open(docs_hash_path) as infile:
+ storedhash = infile.read()
+
+ if curhash != storedhash or not os.path.exists(docs_path):
+ if check:
+ raise RuntimeError('Docs markdown is out of date.')
+
+ print(f'Updating {docs_path}...', flush=True)
+ run('make docs')
+
+ # Our docs markdown is just the docs html with a few added
+ # bits at the top.
+ with open('build/docs.html') as infile:
+ docs = infile.read()
+ docs = ('\n'
+ ) + docs
+ with open(docs_path, 'w') as outfile:
+ outfile.write(docs)
+ with open(docs_hash_path, 'w') as outfile:
+ outfile.write(curhash)
+ print(f'{docs_path} is up to date.')
diff --git a/tools/efro/error.py b/tools/efro/error.py
new file mode 100644
index 00000000..4777b42f
--- /dev/null
+++ b/tools/efro/error.py
@@ -0,0 +1,53 @@
+# Copyright (c) 2011-2020 Eric Froemling
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+# -----------------------------------------------------------------------------
+"""Functionality for dealing with errors."""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ pass
+
+
+class CleanError(Exception):
+ """An error that should be presented to the user as a simple message.
+
+ These errors should be completely self-explanatory, to the point where
+ a traceback or other context would not be useful.
+
+ A CleanError with no message can be used to inform a script to fail
+ without printing any message.
+
+ This should generally be limited to errors that will *always* be
+ presented to the user (such as those in high level tool code).
+ Exceptions that may be caught and handled by other code should use
+ more descriptive exception types.
+ """
+
+ def print(self, flush: bool = False) -> None:
+ """Print the error to stdout, using colored output if available.
+
+ If the error has no message, nothing will be printed.
+ """
+ from efro.terminal import Clr
+ errstr = str(self)
+ if errstr:
+ print(f'{Clr.SRED}{errstr}{Clr.RST}', flush=flush)
diff --git a/tools/efrotools/snippets.py b/tools/efrotools/snippets.py
index aee26073..6f9e8513 100644
--- a/tools/efrotools/snippets.py
+++ b/tools/efrotools/snippets.py
@@ -31,16 +31,12 @@ import sys
from pathlib import Path
from typing import TYPE_CHECKING
+from efro.error import CleanError
from efro.terminal import Clr
if TYPE_CHECKING:
from typing import Dict, Any, List
-
-class CleanError(Exception):
- """Exception resulting in a clean error string print and exit."""
-
-
# Absolute path of the project root.
PROJROOT = Path(__file__).resolve().parents[2]
@@ -76,7 +72,7 @@ def snippets_main(globs: Dict[str, Any]) -> None:
try:
funcs[sys.argv[1]]()
except CleanError as exc:
- print(Clr.SRED + str(exc) + Clr.RST)
+ exc.print()
sys.exit(-1)
else:
print('Unknown snippets command: "' + sys.argv[1] + '"',
diff --git a/tools/snippets b/tools/snippets
index 6d8c7c8a..e9fe7bdd 100755
--- a/tools/snippets
+++ b/tools/snippets
@@ -165,70 +165,15 @@ def androidaddr() -> None:
command line args: archive_dir architecture addr
"""
- import subprocess
+ import batools.android
if len(sys.argv) != 5:
- print('ERROR: expected 4 args; got ' + str(len(sys.argv) - 1) + '.\n' +
- 'Usage: "tools/snippets android_addr'
- ' "')
- sys.exit(255)
+ raise CleanError(f'ERROR: expected 3 args; got {len(sys.argv) - 2}\n'
+ f'Usage: "tools/snippets android_addr'
+ f' "')
archive_dir = sys.argv[2]
- if not os.path.isdir(archive_dir):
- print('ERROR: invalid archive dir: "' + archive_dir + '"')
- sys.exit(255)
arch = sys.argv[3]
- archs = {
- 'x86': {
- 'prefix': 'x86-',
- 'libmain': 'libmain_x86.so'
- },
- 'arm': {
- 'prefix': 'arm-',
- 'libmain': 'libmain_arm.so'
- },
- 'arm64': {
- 'prefix': 'aarch64-',
- 'libmain': 'libmain_arm64.so'
- },
- 'x86-64': {
- 'prefix': 'x86_64-',
- 'libmain': 'libmain_x86-64.so'
- }
- }
- if arch not in archs:
- print('ERROR: invalid arch "' + arch + '"; (choices are ' +
- ', '.join(archs.keys()) + ')')
- sys.exit(255)
addr = sys.argv[4]
- sdkutils = os.path.abspath(
- os.path.join(os.path.dirname(sys.argv[0]), 'android_sdk_utils'))
- rootdir = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), '..'))
- ndkpath = subprocess.check_output([sdkutils,
- 'get-ndk-path']).decode().strip()
- if not os.path.isdir(ndkpath):
- print("ERROR: ndk-path '" + ndkpath + '" does not exist')
- sys.exit(255)
- lines = subprocess.check_output(
- ['find',
- os.path.join(ndkpath, 'toolchains'), '-name',
- '*addr2line']).decode().strip().splitlines()
- lines = [line for line in lines if archs[arch]['prefix'] in line]
- if len(lines) != 1:
- print("ERROR: couldn't find addr2line binary")
- sys.exit(255)
- addr2line = lines[0]
- efrotools.run('mkdir -p "' + os.path.join(rootdir, 'android_addr_tmp') +
- '"')
- try:
- efrotools.run('cd "' + os.path.join(rootdir, 'android_addr_tmp') +
- '" && tar -xf "' +
- os.path.join(archive_dir, 'unstripped_libs',
- archs[arch]['libmain'] + '.tgz') + '"')
- efrotools.run(
- addr2line + ' -e "' +
- os.path.join(rootdir, 'android_addr_tmp', archs[arch]['libmain']) +
- '" ' + addr)
- finally:
- os.system('rm -rf "' + os.path.join(rootdir, 'android_addr_tmp') + '"')
+ batools.android.androidaddr(archive_dir=archive_dir, arch=arch, addr=addr)
def python_build_apple() -> None:
@@ -457,57 +402,8 @@ def warm_start_asset_build() -> None:
def update_docs_md() -> None:
"""Updates docs markdown files if necessary."""
- # pylint: disable=too-many-locals
- from efrotools import get_files_hash, run
-
- check = ('--check' in sys.argv)
-
- docs_path = 'docs/ba_module.md'
-
- # We store the hash in a separate file that only exists on private
- # so public isn't full of constant hash change commits.
- # (don't care so much on private)
- docs_hash_path = 'docs/ba_module_hash'
-
- # Generate a hash from all c/c++ sources under the python subdir
- # as well as all python scripts.
- pysources = []
- exts = ['.cc', '.c', '.h', '.py']
- for basedir in [
- 'src/ballistica/python',
- 'tools/efro',
- 'tools/bacommon',
- 'assets/src/ba_data/python/ba',
- ]:
- assert os.path.isdir(basedir), f'{basedir} is not a dir.'
- for root, _dirs, files in os.walk(basedir):
- for fname in files:
- if any(fname.endswith(ext) for ext in exts):
- pysources.append(os.path.join(root, fname))
- curhash = get_files_hash(pysources)
-
- # Extract the current embedded hash.
- with open(docs_hash_path) as infile:
- storedhash = infile.read()
-
- if curhash != storedhash or not os.path.exists(docs_path):
- if check:
- raise CleanError('Docs markdown is out of date.')
-
- print(f'Updating {docs_path}...', flush=True)
- run('make docs')
-
- # Our docs markdown is just the docs html with a few added
- # bits at the top.
- with open('build/docs.html') as infile:
- docs = infile.read()
- docs = ('\n'
- ) + docs
- with open(docs_path, 'w') as outfile:
- outfile.write(docs)
- with open(docs_hash_path, 'w') as outfile:
- outfile.write(curhash)
- print(f'{docs_path} is up to date.')
+ import batools.build
+ batools.build.update_docs_md(check='--check' in sys.argv)
def list_pip_reqs() -> None:
@@ -610,7 +506,7 @@ def filter_server_config() -> None:
import batools.build
if len(sys.argv) != 4:
raise CleanError('Expected 2 args (infile and outfile).')
- batools.build.filter_server_config(sys.argv[2], sys.argv[3])
+ batools.build.filter_server_config(str(PROJROOT), sys.argv[2], sys.argv[3])
def printcolors() -> None: