Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2022 Smithed
Copyright (c) 2026 Smithed

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
function list:functions:
for func_name in sorted(ctx.data.functions):
say func_name
3 changes: 3 additions & 0 deletions examples/basic_scripts/custom/pack.mcmeta
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"id": "custom"
}
Binary file added examples/basic_scripts/custom/pack.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
say "these bats are wildin'"
3 changes: 3 additions & 0 deletions examples/basic_scripts/fancy_bats/pack.mcmeta
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"id": "fancy_bats"
}
Binary file added examples/basic_scripts/fancy_bats/pack.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
say "the tastiest of tacos"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
give @a taco 64
3 changes: 3 additions & 0 deletions examples/basic_scripts/tasty_tacos/pack.mcmeta
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"id": "tasty_tacos"
}
Binary file added examples/basic_scripts/tasty_tacos/pack.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ dependencies = [
"rich>=13.6.0",
"pydantic>=2.5.2",
"lectern>=0.34.0",
"bolt>=0.38.5",
"bolt-expressions>=0.17.0",
]
description = "Smithed's Python client with CLI, Weld and more"
license = "MIT"
Expand Down
10 changes: 8 additions & 2 deletions src/smithed/weld/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
from .toolchain import cli, run_weld, weld, weld_handler
from .toolchain import cli, run_weld, weld_loader, weld_metadata, weld_handler, weld
from .scripts import WeldScript, WeldPyScript, ResourceDefinition

__version__ = "0.20.0"

__all__ = [
"cli",
"ResourceDefinition",
"run_weld",
"weld",
"weld_handler",
"weld_loader",
"weld_metadata",
"weld",
"WeldPyScript",
"WeldScript",
]
11 changes: 11 additions & 0 deletions src/smithed/weld/scripts/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from .plugins import process_scripting, clear_resources, load_resources
from .resources import WeldScript, WeldPyScript, ResourceDefinition

__all__ = [
"process_scripting",
"clear_resources",
"load_resources",
"ResourceDefinition",
"WeldPyScript",
"WeldScript",
]
10 changes: 10 additions & 0 deletions src/smithed/weld/scripts/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class WeldScriptError(Exception):
namespace: str

def __init__(self, *args, namespace: str, cause: Exception):
super().__init__(*args)
self.namespace = namespace
self.__cause__ = cause

def __str__(self) -> str:
return f"{self.namespace}: {self.args[0]}\n{self.__cause__}"
136 changes: 136 additions & 0 deletions src/smithed/weld/scripts/plugins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import logging
import traceback as tb

from beet import Context, DataPack, Draft, Pack, ResourcePack
import beet
import bolt_expressions
from bolt import Runtime
from bolt.contrib.sandbox import Sandbox, public_attrs
from mecha import CompilationError, Mecha

from .resources import RESOURCES, ResourceDefinition, WeldScript

logger = logging.getLogger("weld")

ALLOWED_TYPES = (DataPack, ResourcePack)
WELD_SCRIPTS_OVERLAY_NAME = "weld_generated"


def process_scripting(ctx: Context):
"""Load scripting stuff"""

if not any(
pack[WeldScript]
for root in ctx.packs
for pack in (root, *root.overlays.values())
):
return

mc = ctx.inject(Mecha)
runtime = ctx.inject(Runtime)

# TODO: move to new file, and write up comments for everything allowed. make more specific
sandbox = ctx.inject(Sandbox)
sandbox.allow_basics()
sandbox.allow_pipeline_context()

sandbox.allowed_type_attrs[Draft] |= {field for field in public_attrs(Draft)}

sandbox.allowed_obj_attrs[beet] |= {field for field in public_attrs(beet)}
sandbox.allowed_obj_attrs[bolt_expressions] |= {
field for field in public_attrs(bolt_expressions)
}

for _type in ALLOWED_TYPES:
sandbox.allowed_type_attrs[_type] |= {field for field in public_attrs(_type)}

sandbox.allowed_imports |= {"beet", "bolt_expressions"}
sandbox.allowed_builtins |= {"breakpoint"}
sandbox.activate()

# load all ctx content into a draft so that it doesn't merge
with ctx.generate.draft() as draft:
# we shallow copy everything into a draft so we can shadow ctx
for draft_pack, ctx_pack in zip((draft.assets, draft.data), ctx.packs):
for resource, file in ctx_pack.all():
draft_pack[resource] = file.copy()

# pretend draft is ctx so that people can't change ctx stuff
# the draft has shallow copies of all content
runtime.globals["ctx"] = draft
for pack in ctx.packs:
pack.overlays.setdefault(
WELD_SCRIPTS_OVERLAY_NAME,
supported_formats=pack.supported_formats or pack.pack_format,
min_format=pack.min_format,
max_format=pack.max_format,
)
overlay = ctx.generate.overlays[WELD_SCRIPTS_OVERLAY_NAME]

# processing packs for compliation
for pack in [
ctx.data,
*ctx.data.overlays.values(),
ctx.assets,
*ctx.assets.overlays.values(),
]:
for resource_location, script in list(pack[WeldScript].items()):
logger.info('Executing Weld Script: "%s"', resource_location)

# compile with recovery to ensure we can compile other modules
try:
with overlay.push():
mc.compile(script)

except CompilationError as err:
# TODO: better error reporting for weld script writers
logger.error(err.__cause__)

except Exception as err:
# if something errors (like mecha), log error AND traceback
logger.error(err)
logger.error("\n".join(tb.format_tb(err.__traceback__)))

# delete files that have not changed in our draft (if `._content` exists, it's changed)
for pack in [draft.assets, draft.data]:
for resource, file in list(pack.all()):
if hasattr(file, "_content") and not file._content:
del pack[type(file)][resource]

# merge all draft content into our global overlay. the draft needs to
# be cleared so that it doesn't merge into the actual pack.
for overlay_pack, draft_pack in zip(
(overlay.assets, overlay.data), (draft.assets, draft.data)
):
overlay_pack.merge(draft_pack) # type: ignore
draft_pack.clear()
draft_pack.extra.clear()

for pack in ctx.packs:
if not pack.overlays[WELD_SCRIPTS_OVERLAY_NAME]:
del pack.overlays[WELD_SCRIPTS_OVERLAY_NAME]


def clear_resources(ctx: Context):
"""Clear weld script resources"""

for pack in [
ctx.data,
*ctx.data.overlays.values(),
ctx.assets,
*ctx.assets.overlays.values(),
]:
for resource in [WeldScript, ResourceDefinition]:
pack[resource].clear()


def load_resources(ctx: Context | Pack):
"""Load all of the custom resource types for weld scripts"""

match ctx:
case Context():
for pack in ctx.packs:
pack.extend_namespace += RESOURCES

case DataPack() | ResourcePack() as pack:
pack.extend_namespace += RESOURCES
42 changes: 42 additions & 0 deletions src/smithed/weld/scripts/resources.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from typing import ClassVar

from beet import DataPack, Drop, NamespaceFileScope, TextFile


class WeldScript(TextFile):
scope: ClassVar[NamespaceFileScope] = {
0: ("weld", "scripts"),
45: ("weld", "script"),
}
extension: ClassVar[str] = ".bolt"


class WeldPyScript(TextFile):
scope: ClassVar[NamespaceFileScope] = {
0: ("weld", "scripts"),
45: ("weld", "script"),
}
extension: ClassVar[str] = ".py"

def bind(self, pack: DataPack, path: str):
"""Rebind file as a proper `Module` and drop current file."""

super().bind(pack, path)

if self.source_path:
pack[path] = WeldScript(source_path=self.source_path)
else:
pack[path] = WeldScript(self.text)

raise Drop()


class ResourceDefinition(TextFile):
scope: ClassVar[NamespaceFileScope] = {
0: ("weld", "resource_definitions"),
45: ("weld", "resource_definition"),
}
extension: ClassVar[str] = ".py"


RESOURCES = [WeldScript, WeldPyScript, ResourceDefinition]
4 changes: 2 additions & 2 deletions src/smithed/weld/toolchain/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from .cli import cli
from .helpers import run_weld
from .plugins import weld, weld_handler
from .plugins import weld_loader, weld_metadata, weld_handler, weld

__all__ = ["cli", "run_weld", "weld", "weld_handler"]
__all__ = ["cli", "run_weld", "weld_loader", "weld_metadata", "weld_handler", "weld"]
4 changes: 2 additions & 2 deletions src/smithed/weld/toolchain/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ def weld(

success = False

logger.debug("Using cache: '%s'", path_str(cache))
logger.info("Packs: %s", ", ".join(f"'{pack}'" for pack in packs))
logger.debug('Using cache: "%s"', path_str(cache))
logger.info("Packs: %s", ", ".join(f'"{pack}"' for pack in packs))

t0 = perf_counter()
with (
Expand Down
5 changes: 4 additions & 1 deletion src/smithed/weld/toolchain/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ def run_weld(
cache: bool | ProjectCache = True,
as_fabric_mod: bool = False,
):
"""Runs the beet toolchain alongside the weld machinery programmatically."""
"""Runs the beet toolchain alongside the weld machinery programmatically.

Matches the beet helper, `run_beet`.
"""

with run_beet(config, directory=directory, cache=cache) as ctx:
ctx.require(weld_handler)
Expand Down
49 changes: 43 additions & 6 deletions src/smithed/weld/toolchain/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
)
from jinja2 import Template

from smithed.weld import merging
from smithed.weld import merging, scripts
from .process import PackProcessor


Expand All @@ -31,14 +31,35 @@
).read_bytes()


class WeldOptions(PluginOptions, arbitrary_types_allowed=True):
class WeldLoaderOptions(PluginOptions, arbitrary_types_allowed=True):
"""Options to configure how weld loads packs before merge time.

packs - which packs (paths or ZipFile) to load
"""

# TODO: change `str` to `PathLike`?
packs: list[str] | list[ZipFile]


class WeldOptions(PluginOptions, arbitrary_types_allowed=True):
"""Options to configure how weld operates at merge time.

scripts - determines whether weld scripts run after merging.
"""

scripts: bool = True


def add_fabric_mod_json(ctx: Context, pack_names: list[str] | None = None):
"""Generates and adds a fabric mod json for producing value fabric jars.

Note: Fabric jar is compromised of a renamed `.zip` -> `.jar` and a `fabric.mod.json`.
"""

if pack_names is None:
processor = ctx.inject(PackProcessor)
pack_names = list(set(pack.name for pack in processor.packs))
# deduplicate names while preserving pack loading order for Fabric metadata.
pack_names = list(dict.fromkeys(pack.name for pack in processor.packs))

content = FABRIC_MOD_TEMPLATE.render(
pack_hash=hash("".join(pack_names)),
Expand All @@ -49,17 +70,27 @@ def add_fabric_mod_json(ctx: Context, pack_names: list[str] | None = None):
ctx.data.extra["fabric.mod.json"] = JsonFile(content)


@configurable(validator=WeldOptions)
def weld_loader(ctx: Context, opts: WeldOptions):
@configurable(validator=WeldLoaderOptions)
def weld_loader(ctx: Context, opts: WeldLoaderOptions):
"""Loads packs into weld via the `PackProcessor` class."""

processor = ctx.inject(PackProcessor)
processor.load_packs(opts.packs)


def weld_handler(ctx: Context):
"""Loads the merging pre-processing pipeline."""

ctx.require(merging.beet_default)
ctx.require(scripts.load_resources)


def weld_metadata(ctx: Context):
"""Attaches custom metadata to welded packs.

TODO: make configurable
"""

processor = ctx.inject(PackProcessor)

if ctx.data:
Expand All @@ -79,5 +110,11 @@ def weld_metadata(ctx: Context):
]


def weld(ctx: Context):
@configurable(validator=WeldOptions)
def weld(ctx: Context, opt: WeldOptions):
"""Beet plugin that runs the main weld processes (merging + scripting)"""

ctx.require(merging.process)

if opt.scripts:
ctx.require(scripts.process_scripting)
Loading
Loading