OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux / blender / build_addon.py

103 lines · python ref:
"""
Packages the add-on into a zip Blender will install.

    python3 blender/build_addon.py --out /tmp/ifs_fractal.zip

Blender installs a zip whose TOP LEVEL is a single directory containing
`__init__.py`, and the directory name becomes the module name. This repo's folder
is called `blender/`, which would be a terrible module name and would collide with
anything else named the same, so the archive renames it to `ifs_fractal/`.

The zip is SELF-CONTAINED: it carries the 46 transform plugins, the kernel
template, the includes and the templates under `assets/`, because an installed
add-on sits in Blender's addons directory where the repo is nowhere to be found.
Run from a checkout the add-on finds no bundled `assets/` and reads the repo's
live files instead, which is what keeps development honest about the plugins it
actually compiles.

Longer term the cleaner boundary is baking the generated kernel at build time and
shipping only that - see the note in ifs_codegen. Shipping the sources keeps the
add-on able to recompile, which is worth more while the kernel is still moving.
"""

from __future__ import annotations

import argparse
import os
import zipfile

MODULE = "ifs_fractal"

# Everything the add-on needs at runtime. Tests are excluded: they are for the
# repo, not the installed artifact, and they import a .NET-free corpus that will
# not be present beside an installed add-on.
RUNTIME_FILES = (
    "__init__.py",
    "ifs_codegen.py",
    "ifs_layout.py",
    "ifs_host.py",
    "ifs_document.py",
    "ifs_render.py",
    "ifs_viewport.py",
    "ifs_volume.py",
    "ifs_points.py",
    "ifs_hdri.py",
    "ifs_splat.py",
    "ifs_nodes.py",
)


# Repo-relative asset trees, copied under `assets/` in the archive. The kernel
# template must come too: the add-on compiles it at load time.
ASSET_DIRS = (
    ("Transforms", ".ifstf"),
    (os.path.join("WpfDisplay", "Library", "Includes"), None),
    (os.path.join("WpfDisplay", "Library", "Templates"), ".ifsjson"),
    (os.path.join("IFSEngine", "Rendering", "Shaders"), ".shader"),
)


def build(source_dir: str, out_path: str) -> str:
    missing = [f for f in RUNTIME_FILES
               if not os.path.isfile(os.path.join(source_dir, f))]
    if missing:
        raise FileNotFoundError(f"missing runtime files: {missing}")

    repo = os.path.dirname(source_dir)
    count = 0
    with zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED) as archive:
        for name in RUNTIME_FILES:
            archive.write(os.path.join(source_dir, name), f"{MODULE}/{name}")
            count += 1
        for rel, suffix in ASSET_DIRS:
            absolute = os.path.join(repo, rel)
            if not os.path.isdir(absolute):
                raise FileNotFoundError(f"asset directory missing: {absolute}")
            for entry in sorted(os.listdir(absolute)):
                path = os.path.join(absolute, entry)
                if not os.path.isfile(path):
                    continue
                if suffix and not entry.endswith(suffix):
                    continue
                archive.write(path, f"{MODULE}/assets/{rel}/{entry}")
                count += 1
    print(f"packed {count} files")
    return out_path


def main(argv: list[str] | None = None) -> int:
    here = os.path.dirname(os.path.abspath(__file__))
    parser = argparse.ArgumentParser(description=__doc__.strip().splitlines()[0])
    parser.add_argument("--out", default=os.path.join(here, "ifs_fractal.zip"))
    args = parser.parse_args(argv)

    path = build(here, args.out)
    size = os.path.getsize(path)
    print(f"wrote {path} ({size / 1024:.1f} KB, {len(RUNTIME_FILES)} files)")
    print(f"install: Edit > Preferences > Add-ons > Install from Disk -> {path}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())