# Released under the MIT License. See LICENSE for details.
#
"""Utils for wrangling running the app as part of a build.
Manages constructing or downloading it as well as running it.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import platform
import subprocess
import os
from efro.terminal import Clr
if TYPE_CHECKING:
from typing import Mapping
[docs]
def test_runs_disabled() -> bool:
"""Are test runs disabled on the current platform?"""
# Currently skipping this on Windows, as we aren't able to assemble
# complete build there without WSL.
if platform.system() == 'Windows':
return True
return False
[docs]
def test_runs_disabled_reason() -> str:
"""Why are test runs disabled here?"""
# Can get more specific later.
return 'App test runs disabled here.'
[docs]
def acquire_binary_for_python_command(purpose: str) -> str:
"""Run acquire_binary as used for python_command call."""
return acquire_binary(assets=True, purpose=purpose)
[docs]
def python_command(
cmd: str,
purpose: str,
include_project_tools: bool = False,
env: Mapping[str, str] | None = None,
) -> None:
"""Run a cmd with a built bin and PYTHONPATH set to its scripts."""
binpath = acquire_binary_for_python_command(purpose=purpose)
bindir = os.path.dirname(binpath)
# We'll set both the app python dir and its site-python-dir. This
# should let us get at most engine stuff. We could also just use
# baenv to set up app paths, but that might be overkill and could
# unintentionally bring in stuff like local mods.
pydir = f'{bindir}/ba_data/python'
assert os.path.isdir(pydir)
pysitedir = f'{bindir}/ba_data/python-site-packages'
assert os.path.isdir(pysitedir)
# Make our tools dir available if asked.
tools_path_extra = ':tools' if include_project_tools else ''
env_final = {} if env is None else dict(env)
env_final['PYTHONPATH'] = f'{pydir}:{pysitedir}{tools_path_extra}'
cmdargs = [binpath, '--command', cmd]
print(f"apprun: Running with Python command: '{cmdargs}'...", flush=True)
subprocess.run(cmdargs, env=env_final, check=True)
[docs]
def acquire_binary(assets: bool, purpose: str) -> str:
"""Return path to a runnable binary, building/downloading as needed.
If 'assets' is False, only the binary itself will be fetched or
assembled; no scripts or assets. This generally saves some time, but
must only be used for very simple '-c' command cases where no assets
will be needed.
Be aware that it is up to the particular environment whether a gui
or headless binary will be provided. Commands should be designed to
work with either.
By default, downloaded prefab builds will be used here. This allows
people without full compiler setups to still perform app runs for
things like dummy-module generation. However, someone who *is* able
to compile their own binaries might prefer to use their own binaries
here so that changes to their local repo are properly reflected in
app runs and whatnot. Set environment variable
BA_APP_RUN_ENABLE_BUILDS=1 to enable that.
When local builds are enabled, we use the same gui build targets as
the 'make cmake-build' command. This works well if you are iterating
using that build target anyway, minimizing redundant rebuilds. You
may, however, prefer to instead assemble headless builds for various
reasons including faster build times and fewer dependencies
(equivalent to 'make cmake-server-build'). To do so, set environment
variable BA_APP_RUN_BUILD_HEADLESS=1.
"""
import textwrap
binary_build_command: list[str]
if os.environ.get('BA_APP_RUN_ENABLE_BUILDS') == '1':
# Going the build-it-ourselves route.
# Don't need any env mods for this path.
env = None
if os.environ.get('BA_APP_RUN_BUILD_HEADLESS') == '1':
# User has opted for headless builds.
if assets:
print(
f'{Clr.SMAG}Building headless binary & assets for'
f' {purpose}...{Clr.RST}',
flush=True,
)
binary_build_command = ['make', 'cmake-server-build']
else:
print(
f'{Clr.SMAG}Building headless binary for'
f' {purpose}...{Clr.RST}',
flush=True,
)
binary_build_command = ['make', 'cmake-server-binary']
binary_path = (
'build/cmake/server-debug/staged/dist/ballisticakit_headless'
)
else:
# Using default gui builds.
if assets:
print(
f'{Clr.SMAG}Building gui binary & assets for'
f' {purpose}...{Clr.RST}',
flush=True,
)
binary_build_command = ['make', 'cmake-build']
else:
print(
f'{Clr.SMAG}Building gui binary for {purpose}...{Clr.RST}',
flush=True,
)
binary_build_command = ['make', 'cmake-binary']
binary_path = 'build/cmake/debug/staged/ballisticakit'
else:
# Ok; going with a downloaded prefab headless build.
# By default, prefab build targets on WSL (Linux running on
# Windows) will give us Windows builds which won't work right
# here. Ask it for Linux builds instead.
env = dict(os.environ, BA_WSL_TARGETS_WINDOWS='0')
# Let the user know how to use their own built binaries instead
# if they prefer.
note = '\n' + textwrap.fill(
f'NOTE: You can set env-var BA_APP_RUN_ENABLE_BUILDS=1'
f' to use locally-built binaries for {purpose} instead'
f' of prefab ones. This will properly reflect any changes'
f' you\'ve made to the C/C++ layer.',
80,
)
if assets:
print(
f'{Clr.SMAG}Fetching prefab binary & assets for'
f' {purpose}...{note}{Clr.RST}',
flush=True,
)
binary_path = (
subprocess.run(
['tools/pcommand', 'prefab_binary_path', 'server-release'],
env=env,
check=True,
capture_output=True,
)
.stdout.decode()
.strip()
)
binary_build_command = ['make', 'prefab-server-release-build']
else:
print(
f'{Clr.SMAG}Fetching prefab binary for {purpose}...'
f'{note}{Clr.RST}',
flush=True,
)
binary_path = (
subprocess.run(
['tools/pcommand', 'prefab_binary_path', 'server-release'],
env=env,
check=True,
capture_output=True,
)
.stdout.decode()
.strip()
)
binary_build_command = ['make', binary_path]
subprocess.run(binary_build_command, env=env, check=True)
if not os.path.exists(binary_path):
raise RuntimeError(
f"Binary not found at expected path '{binary_path}'."
)
return binary_path