Skip to content

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 fbproject.toml.

required
dev bool

If True, builds loose assets for local development (build_for_dev); if False, produces a packaged release build (build_for_release).

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 name isn't set and no default was given.

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.