Skip to content
CybersharpXPublic

About

GraalPy for Minecraft

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

GraalPy — Python Mods for Minecraft Fabric

GraalPy is a Fabric mod that embeds the Truffle Python runtime once, shared across the whole game instance, so other mods can write gameplay logic in Python without bundling a runtime themselves.


⚠ Security notice

GraalPy v1 has no sandboxing. Python mods execute with allowAllAccess(true) — full JVM host trust, identical to a Java mod. If you install a Python mod you are granting it the same trust as any other Fabric mod. This will be stated on every startup in the log. Do not assume isolation.


Project structure

graalpy/                          ← root multi-project
├── graalpy-core/                 ← the mod that ships the Truffle runtime
│   └── src/main/java/dev/graalpy/core/
│       ├── GraalPyMod.java           Fabric entrypoint, wires all events
│       ├── api/
│       │   ├── GraalPyApi.java       Public service interface (other mods use this)
│       │   └── GraalPyApiHolder.java Internal singleton holder
│       ├── engine/
│       │   ├── SharedEngine.java     Singleton Engine + Context, cold-start timer
│       │   └── LoggerOutputStream.java  Routes Python print() into MC logs
│       ├── loader/
│       │   ├── PythonModLoader.java  Classpath discovery, topo-sort, eval, hooks
│       │   ├── PythonModManifest.java Parsed graalpy.mod.toml data class
│       │   ├── PythonModInstance.java Runtime state + cached hook Value handles
│       │   └── TopologicalSort.java  Kahn's algorithm dep ordering
│       └── bridge/
│           └── PythonTracebackFormatter.java  Clean Python-only tracebacks
│
└── example-python-mod/           ← ships ZERO Truffle runtime
    └── src/main/resources/
        └── graalpy/mods/example_hello_block/
            ├── graalpy.mod.toml      Python mod manifest
            ├── main.py               Entry-point + lifecycle hooks
            ├── hello_block.py        Custom block (Python subclass of Java Block)
            └── broken_handler.py     Intentional error — traceback demo

Phase status

Phase Goal Status
0 Embedding spike — prove Truffle Python runs in a Fabric jar ✅ Done
1 Loader Core — shared engine, mod discovery, lifecycle hooks ✅ Done (this branch)
2 Pythonic SDK v0 — graalpy.blocks, graalpy.items, events 🔲 Next
3 Entities, commands, GUIs 🔲 Planned
4 Dev experience — hot reload, type stubs, REPL 🔲 Planned
5 Packaging, build, docs 🔲 Planned
6 NeoForge port, sandboxed mode, curated pip packages 🔲 Stretch

Phase 0 findings (resolved)

JDK version

Truffle Python (GraalPy Community Edition) runs on stock OpenJDK — no custom GraalVM JDK required. Since GraalVM Polyglot API 23.1, all language artifacts are published to Maven Central as plain jars. This project targets Java 21 (dropped from the spike's Java 25 to reduce MC version risk).

License

GraalPy Community Edition is Universal Permissive License (UPL) v1.0 — OSI-approved, permissive, no copyleft, explicit patent protection. Bundling and redistributing inside a third-party mod jar is explicitly permitted. The -community Maven artifacts are used specifically to stay on UPL. (Oracle's commercial GraalPy distribution uses GFTC instead — do not use those artifacts.)

Cold-start latency

Not yet measured — run ./gradlew :graalpy-core:runServer and look for:

[GraalPy] Cold-start (Engine + Context init + python warmup): <N> ms

Community benchmarks suggest several hundred ms to low seconds on JVM-mode HotSpot (JIT warmup cost). A few hundred ms added to server/client startup is acceptable; multiple seconds on every world load would not be (but the context is kept alive across world loads — it only initialises once).

Classloader / module path risk

Fabric Loader uses its own classloader, not a plain JPMS application. Whether --add-modules=org.graalvm.polyglot is needed is genuinely unconfirmed for a distributed (non-dev-env) mod. If Context.create() throws NoClassDefFoundError or a module-access error in your log, that is a real finding. The build.gradle has commented-out jvmArgs lines for runClient/runServer ready to uncomment.


Phase 1 design

How mod discovery works

PythonModLoader.discoverAndLoad() does the following at game init time:

  1. Calls ClassLoader.getResources("graalpy.mod.toml") on the context classloader, which returns every graalpy.mod.toml found across all jars on the classpath — including jars from other Fabric mods.
  2. Filters to only those whose URL path contains graalpy/mods/ (to avoid false matches from unrelated jars).
  3. Parses each manifest with toml4j into a PythonModManifest.
  4. Topologically sorts manifests by [dependencies].python_mods using Kahn's algorithm (TopologicalSort). Cycles and missing deps raise loud errors.
  5. For each manifest in order: adds the mod's resource base to sys.path, evaluates the entry-point script, caches on_load / on_world_load / on_tick / on_unload function handles, calls on_load().

Python mod jar layout

A Python mod jar only needs resources — no Java code is required unless the mod author also wants a Java entrypoint (e.g. for Fabric API access outside the Python lifecycle):

your-python-mod.jar
└── graalpy/
    └── mods/
        └── <your-mod-id>/
            ├── graalpy.mod.toml   ← required: manifest
            ├── main.py            ← required: entry-point (configurable in manifest)
            └── *.py               ← optional: modules imported by main.py

Lifecycle hooks

Define any of these functions at module level in your entry-point script. All are optional — GraalPy checks with canExecute() before calling.

def on_load():
    """Runs once after your script is evaluated. Register blocks/items here."""

def on_world_load(world):
    """Runs when any world/dimension is loaded. `world` is a Java ServerWorld."""

def on_tick(server):
    """Runs every server tick (20/s). Rate-limit heavy logic."""

def on_unload():
    """Runs during server shutdown, in reverse load order."""

GraalPyApi — for Java mods that need the shared context

import dev.graalpy.core.api.GraalPyApi;
import org.graalvm.polyglot.Context;

GraalPyApi api = GraalPyApi.getInstance(); // null if called before GraalPyMod.onInitialize
Context ctx = api.getSharedContext();
ctx.eval("python", "print('hello from a Java mod!')");

How to add a third Python mod (zero Truffle runtime)

  1. Create a new Fabric mod project (or a resources-only jar).

  2. Add graalpy as a dependency in fabric.mod.json:

    "depends": {
      "graalpy": "*"
    }
  3. Create the manifest at src/main/resources/graalpy/mods/<your-mod-id>/graalpy.mod.toml:

    [mod]
    id      = "your_mod_id"
    name    = "Your Mod Name"
    version = "1.0.0"
    main    = "main.py"
    
    [dependencies]
    python_mods = []   # list other Python mod IDs you depend on
  4. Write your entry-point at src/main/resources/graalpy/mods/<your-mod-id>/main.py:

    import java
    
    def on_load():
        print("Hello from my Python mod!")
  5. Build and drop both jars (graalpy-core-*.jar and your-mod-*.jar) into the mods/ folder. No extra runtime jars needed.

That's it. GraalPy discovers your graalpy.mod.toml automatically.


Building

# Build everything
./gradlew build

# Run the dev client (graalpy-core + example mod on classpath)
./gradlew :graalpy-core:runClient

# Run the dev server
./gradlew :graalpy-core:runServer

Gradle wrapper: If gradlew is missing, run gradle wrapper --gradle-version latest once, or open the project in IntelliJ IDEA and let it generate the wrapper.

What to look for in the logs

[GraalPy] SECURITY: Python mods have FULL JVM HOST TRUST. No sandboxing in v1.
[GraalPy] Cold-start (Engine + Context init + python warmup): <N> ms
[GraalPy] Discovered 1 Python mod(s): [example_hello_block]
[GraalPy] Load order: [example_hello_block]
[GraalPy] Loading Python mod 'example_hello_block' v0.1.0
[Python]  [HelloBlock] Module loaded. Running on Minecraft 26.1.2
[Python]  [HelloBlock] on_load() — registering Hello Block via Fabric registry
[Python]  [HelloBlock] Registered block: example_hello_block:hello_block → ...
[GraalPy] Python mod 'example_hello_block' loaded successfully.
[GraalPy] 1 Python mod(s) loaded.

Clean Python traceback demo

To see PythonTracebackFormatter in action, temporarily add this to the example mod's main.py on_load():

from broken_handler import trigger_error
trigger_error()

You will see this in the log (no Java frames):

[GraalPy] Unhandled exception in Python mod 'example_hello_block'
Traceback (most recent call last):
  File "main.py", line N, in on_load
  File "broken_handler.py", line 32, in trigger_error
  File "broken_handler.py", line 40, in _inner
ZeroDivisionError: integer division or modulo by zero

Open questions for Phase 2

  • Whether Truffle Python's java.extend() for Block subclasses requires any special Fabric mixin or AT to override protected methods — needs a running test.
  • Exact [dependencies] TOML schema for declaring Java mod dependencies (currently only Python mod deps are supported).
  • Whether sys.path injection via resource base is sufficient for all classpath layouts (fat jar, dev classpath, etc.) or whether a custom importlib finder is needed.

About

GraalPy for Minecraft

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages