builder
FreeBodyEngine.build.builder
#
DATA_FILE_TYPES = ['txt', 'json', 'fbusl', 'fbvert', 'fbfrag', 'fbmat', 'fbspr', 'fbanim', 'fbsheet', 'mp3', 'wav', 'toml']
module-attribute
#
FONT_ATLAS_SIZE = 48
module-attribute
#
FONT_BUILD_DIR = '_ENGINE_fonts'
module-attribute
#
FONT_CACHE_NAME = '_ENGINE_font_cache.json'
module-attribute
#
FONT_FILE_TYPES = ['ttf', 'otf']
module-attribute
#
FONT_RANGE_PX = 4.0
module-attribute
#
IMAGE_FILE_TYPES = ['png', 'jpg', 'jpeg']
module-attribute
#
MESH_FILE_TYPES = ['fbx']
module-attribute
#
MODEL_BUILD_DIR = '_ENGINE_models'
module-attribute
#
MODEL_CACHE_NAME = '_ENGINE_model_cache.json'
module-attribute
#
MODEL_FILE_TYPES = ['glb', 'gltf']
module-attribute
#
PYODIDE_CDN_URL = 'https://cdn.jsdelivr.net/pyodide/v0.26.4/full/'
module-attribute
#
SUPPORTED_PLATFORMS = ['windows', 'darwin', 'linux']
module-attribute
#
Builder(path, dev)
#
Orchestrates a whole project build (dev or release) from fbproject.toml
at path: locates assets, pre-bakes fonts/models, generates the texture
atlas, bundles everything into .paks, and (for a release build) packages
the project's code with PyInstaller. Building is a side effect of
construction - __init__ runs the whole pipeline before returning.
Runs the full build pipeline for the project at path.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str
|
Project root directory containing |
required |
dev
|
bool
|
If True, builds loose assets for local development
( |
required |
android_output_path = os.path.abspath(f'{path}/dev/android/')
instance-attribute
#
asset_out_path = os.path.join(self.output_path, 'assets')
instance-attribute
#
asset_path = os.path.abspath(os.path.join(path, self.get_user_setting('assets')))
instance-attribute
#
build_cache = self.get_build_cache()
instance-attribute
#
build_path = os.path.abspath(f'{path}/build/')
instance-attribute
#
build_settings = load_toml(f'{path}/fbproject.toml')
instance-attribute
#
cache_path = os.path.join(self.build_path, 'cache.json')
instance-attribute
#
code_path = os.path.abspath(os.path.join(path, self.get_user_setting('code')))
instance-attribute
#
dependencies = user_dependencies + self.get_platform_dependencies(self.platform)
instance-attribute
#
fbusl_source_path = self._find_local_fbusl_source()
instance-attribute
#
font_cache_path = os.path.join(self.build_path, 'fonts')
instance-attribute
#
main_file = os.path.abspath(os.path.join(path, self.get_user_setting('main_file')))
instance-attribute
#
model_cache_path = os.path.join(self.build_path, 'models')
instance-attribute
#
output_path = os.path.abspath(f'{path}/dist/')
instance-attribute
#
platform = self.get_build_platform(args)
instance-attribute
#
progress = ProgressBar()
instance-attribute
#
project_path_root = os.path.abspath(path)
instance-attribute
#
temp_path = os.path.abspath(f'{path}/build/temp/')
instance-attribute
#
web_output_path = os.path.abspath(f'{path}/dev/web/')
instance-attribute
#
build_code()
#
Builds code into an execuatable usign pyinstaller.
build_fonts(fonts, output_dir)
#
Pre-builds every given font (.ttf/.otf) into an MSDF atlas +
.fbfont sidecar under output_dir/_ENGINE_fonts, and writes a
{original_relative_path: built_relative_path} manifest next to it -
core/files/loaders/font.py's resolve_font() reads this same
manifest at runtime so a font style's raw path (e.g.
"FreeMono.ttf") resolves to the pre-built asset instead of being
generated on the spot every run. output_dir is the real project
asset directory for a dev build (DevFileSystem reads these loose
files directly, nothing else to do) or a persistent per-project
build-cache directory for a release build (the caller still has to
fold the returned data/image file maps into the data.pak/
images.pak bundles - output_dir itself is never bundled, only
those explicit files are).
A _ENGINE_font_cache.json next to the manifest records each
source font's content hash - a font already built with the same
hash is left alone entirely (not re-read, not re-rasterized),
so re-running a build only pays for fonts that are new or actually
changed since last time.
Fonts under "engine://" (rel_path starting with "engine/") are skipped - they ship inside the installed FreeBodyEngine package, not a project's own build output, so resolve_font()'s on-the-fly generation fallback covers those instead.
Returns (manifest, data_files, image_files) - the latter two are {abs_path: out_relative_path} maps in the same shape bundle_assets() expects, empty for anything build_for_dev() doesn't need them for.
build_for_android()
#
Placeholder for a release Android build target (a signed, optimized APK/AAB) - not implemented yet. Dev-mode Android builds are a separate, already-implemented path - see build_for_dev_android().
build_for_dev()
#
Prepares a development build: pre-bakes project fonts straight
into the project's own asset directory (loose files, since
DevFileSystem reads them directly rather than from a .pak).
No atlas, model baking, or code packaging happens here - dev mode
reads everything else directly off disk.
build_for_dev_android()
#
Prepares a python-for-android/buildozer project directory at
dev/android/ for a debug build: real files on disk at a real
path (unlike build_for_dev_web()'s zip archives - buildozer shells
out to p4a, which reads source.dir straight off the filesystem,
not through this process), containing the project's
fbproject.toml, its asset/code directories copied verbatim, a
generated main.py shim (see _write_android_main_py() - p4a
always runs main.py at the source root, regardless of this
project's own main_file setting), and a generated
buildozer.spec (see _write_buildozer_spec()).
Rebuilt fresh on every fb build --android/fb run --android,
same as build_for_dev_web()'s dev/web/ - a first working loop, not
an incremental sync (buildozer's own build cache under
dev/android/.buildozer is what actually keeps repeat builds fast,
not anything on this side).
Bakes project fonts straight into the project's own asset directory first, same as build_for_dev()/build_for_dev_web() do, so the copy below picks up the baked files like any other asset.
build_for_dev_web()
#
Prepares a development build for the web platform: a small,
self-contained dev/web/ directory a caller (see dev/run.py's web
branch) serves over plain HTTP and points a browser at. Unlike
build_for_dev() (which reads everything straight off disk at
runtime, since it's still a normal local Python process), a
browser tab has no filesystem of its own at all - so this instead
packages two zip archives Pyodide unpacks into its own virtual
filesystem at page-load time (see _write_web_bootstrap_py()):
- vendor.zip: this engine's own Python source plus FBUSL's
(both pure Python - no native extension in either, see
FBUSL's own source tree - so a straight file copy is all
"installing" them into Pyodide's filesystem needs, unlike the
real venv+pip install a native/release build's setup_venv()
does).
- project.zip: the project file, main_file, and everything
under the asset/code directories, laid out at the exact same
relative paths DevFileSystem already expects on every other
platform - unpacked to /project inside Pyodide, so
open()/tomllib-reading code needs zero changes to work
there unmodified.
Both are rebuilt fresh on every fb run --web (a real rebuild,
not a live dev-server proxy to the actual project files) - editing
a project file needs rerunning fb run --web and reloading the
page to see the change, unlike build_for_dev()'s native path
where DevFileSystem reads current file content straight off disk
every time. A follow-up could fetch project files individually
instead of zipping them, to get that live-edit convenience back;
deferred for now in favor of the simpler, more robust archive
approach for a first working web dev loop.
Loads the Pyodide runtime itself from jsdelivr's CDN
(PYODIDE_CDN_URL below), not the lib/pyodide/ files bundled
with this engine - those turned out to be only pyodide.js/
.asm.wasm, a partial/experimental copy missing pyodide.asm.js,
pyodide-lock.json (needed to resolve loadPackage(["numpy",
"Pillow"]) to real wheel URLs) and python_stdlib.zip (needed to
boot the standard library at all) - loadPyodide() against it
hung indefinitely on "Loading Pyodide runtime..." instead of
erroring, since the missing files are fetched lazily rather than
checked up front. A real offline-capable vendor copy is a
reasonable follow-up (mirror the CDN's whole full/ directory
for the pinned version instead of two loose files), but pulling a
known-complete distribution from the CDN is what actually works
today.
Pre-bakes project fonts straight into the project's own asset directory first, same as build_for_dev() does for every other platform, so project.zip picks up the baked files like any other asset.
build_for_release()
#
Runs the full release pipeline: scans project and engine assets,
resets the output directories, builds the shared texture atlas,
pre-bakes fonts and models, bundles everything into data/images/
mesh .paks, and finally packages the project's code.
build_for_web()
#
Placeholder for a release web build target - packaging a project's code+assets into a standalone, deployable static bundle (minified/optimized, no dev server involved) isn't implemented yet. Dev-mode web builds are a separate, already-implemented path - see build_for_dev_web().
build_models(meshes, output_dir)
#
Pre-bakes every project .glb/.gltf into a flat .fbmesh (see
core/files/loaders/model.py's bake_gltf_to_fbmesh()) plus whatever
textures it embeds, extracted to real files alongside it - a
release build's runtime (AssetPackFileSystem) then never parses raw
glTF JSON/accessors at all, only np.frombuffer()s the baked
arrays straight out of the file.
Content-hash cached exactly like build_fonts() (same manifest + cache-file shape, same "rebuild only what actually changed" behavior) - baking a large/high-poly model isn't free, and most builds change a handful of assets, not all of them.
.fbx meshes (MESH_FILE_TYPES) are left alone here - only
MODEL_FILE_TYPES (glTF) has a baked path today - and stay in
meshes for the caller's own raw mesh.pak bundling.
bundle_assets(paths, name)
#
Writes a .pak: a "FBAP" magic + version + entry count, followed
by an entry table (path, absolute data offset, data length) laid out
entirely before the data section - see core/files/asset_pack.py's
AssetPack for the matching reader. An upfront table with absolute
offsets (vs. the old interleaved path, data, path, data, ...
layout, which had no header/magic at all) lets the reader index the
whole pack from just the header+table and fetch any entry in O(1).
compile_cpp_scripts(venv_executable)
#
Compiles the project's .cpp/.hpp scripts (see cli/cpp/compile.py)
using the release venv's own Python, so the resulting extension's
ABI matches whatever CPython PyInstaller ends up embedding - always
for self.platform, i.e. whatever platform this build is actually
running on. Cross-compiling a CPython extension for a different
OS/arch than the host isn't attempted (nothing in this engine's
build pipeline cross-compiles - PyInstaller itself only ever
packages for the host platform too), so a Windows/macOS/Linux
release still has to be built from that platform, same as before
this existed.
create_build_cache()
#
Writes the current dependency list out to self.cache_path.
get_build_cache()
#
Loads the build cache JSON at self.cache_path, or an empty dict
if it doesn't exist yet.
get_build_platform(args)
#
Determines which platform to build for: "web" if --web is in
args, "android" if --android is, otherwise the detected host
platform ("windows" for win32, else sys.platform itself if
it's one of SUPPORTED_PLATFORMS). Prints a message and returns
None if the host platform isn't supported.
--android is checked here rather than by inspecting the host
platform (like the plain-linux branches below) because an
Android build is always cross-compiled from the dev machine's own
OS - unlike web, there's no "running under Android already" case
for this method to ever detect on its own.
get_engine_assets()
#
Like locate_assets, but over the engine's own bundled
engine_assets package directory rather than the project's asset
directory - every returned relative path is prefixed with
'engine/', matching the engine:// asset namespace.
get_out_path(path, root_dir)
#
Converts a system path into an output path.
get_platform_dependencies(platform)
#
Returns the pip dependency list for platform: the global
requirements plus whichever of windows/darwin/linux's platform-
specific requirements apply.
Android is deliberately NOT built on top of GLOBAL - see requirements.ANDROID's own comment for why the two lists diverge instead of one extending the other.
get_user_setting(name, default=None)
#
Looks up name in the project's fbproject.toml settings.
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
get_venv_python(venv_path)
#
Returns the path to the Python executable inside the venv at
venv_path, using the platform-appropriate layout (Scripts/ on
Windows, bin/ elsewhere).
install_dependencies(venv_executable)
#
Installs self.dependencies (the project's declared dependencies
plus platform requirements) into the build venv.
install_fbusl(venv_executable)
#
Installs FBUSL into the build venv - from self.fbusl_source_path
if a local checkout was found (see _find_local_fbusl_source),
otherwise a no-op (FBUSL is left as a normal PyPI dependency, already
covered by install_dependencies).
install_freebody(venv_executable)
#
Installs this local FreeBodyEngine checkout into the build venv, so the packaged game ships whatever engine version is actually being developed against rather than a published PyPI release.
locate_assets()
#
Walks the project's asset directory and buckets every file by
type (image/data/mesh-or-model/font, per the *_FILE_TYPES
constants). Returns (images, data, meshes, fonts), each an
{absolute_path: path_relative_to_the_asset_dir} map.
reset_dirs()
#
Resets the build, temp, and dist directories.
run_pyinstaller(venv_executable)
#
Packages the project's main_file into a single-file, windowed
executable with PyInstaller, then moves the resulting binary into
self.output_path. Explicitly bundles the engine's native lib
directory, every OpenGL submodule (PyOpenGL's platform backend is
chosen via a dynamic import PyInstaller's static analysis can't
trace), and any compiled C++ script extensions/shims under
self.code_path.
setup_venv()
#
Creates a fresh build-only virtual environment and runs the rest of the release code pipeline inside it: installing dependencies, FBUSL, and this engine itself, compiling the project's C++ scripts, and packaging everything with PyInstaller.
build(path='./', dev=False)
#
Runs a full project build at path (dev or release - see Builder).
get_relative_path(path, folder)
#
Returns path relative to folder, as a forward-slash-separated
(POSIX-style) string regardless of host OS - matching the "/"-separated
convention virtual asset paths use throughout the FileSystem abstraction.
load_json(path)
#
Reads the file at path and parses it as JSON.
load_text(path)
#
Reads and returns the full text contents of the file at path.
load_toml(path)
#
Reads the file at path and parses it as TOML.