Skip to content

feat: add beet.contrib.mcbuild #464

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
147 changes: 147 additions & 0 deletions beet/contrib/mcbuild.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
"""Plugin that builds a MCBuild project."""

__all__ = ["beet_default", "MCBuildOptions"]


from typing import Optional
from beet import Context, configurable
from pydantic.v1 import BaseModel
from beet.core.utils import FileSystemPath
import subprocess, shutil, json, requests, colorama, hashlib
from pathlib import Path


DEFAULT_CONFIG_URL = "https://raw.githubusercontent.com/mc-build/mcb/refs/heads/main/template/mcb.config.js"
CONFIG_FILE = "mcb.config.js"


def log(message: str) -> None:
"""Print with a custom prefix."""
print(
f"{colorama.Fore.LIGHTBLACK_EX}[{colorama.Fore.GREEN}MCB{colorama.Fore.WHITE}-{colorama.Fore.LIGHTRED_EX}BEET{colorama.Fore.LIGHTBLACK_EX}]{colorama.Fore.RESET}",
message,
)


def create_default_config(ctx: Context):
res = requests.get(DEFAULT_CONFIG_URL)
res.raise_for_status()

with open(ctx.directory / CONFIG_FILE, "wb") as f:
f.write(res.content)


def create_source_hash(source: Path, config: Path) -> str:
hasher = hashlib.sha256()

def update_from_file(path: Path):
assert path.is_file()
hasher.update(path.as_posix().encode())
hasher.update(path.read_bytes())
with open(path, "rb") as f:
hasher.update(f.read())

def update_from_directory(path: Path):
assert path.is_dir()
hasher.update(path.as_posix().encode())
for path in sorted(path.iterdir()):
if path.is_file():
update_from_file(path)
elif path.is_dir():
update_from_directory(path)

update_from_file(config)

if source.is_file():
update_from_file(source)
elif source.is_dir():
update_from_directory(source)

return hasher.hexdigest()


def filter_stdout_for_soft_errors(stdout: str) -> Optional[str]:
lines = stdout.split("\n")

def capture_error(errorTitle: str, index: int):
error = colorama.Fore.RED + errorTitle
for i in range(index + 1, len(lines)):
if not lines[i][0] in ["\t", " "]:
return error
if len(error) == 0:
error = lines[i].strip()
else:
error += "\n" + lines[i]
return error + colorama.Fore.RESET

for line in lines:
if line.startswith("[MCB] Parser Error:"):
return capture_error("Parser Error:", lines.index(line))
elif line.startswith("[MCB] Compiler Error:"):
return capture_error("Compiler Error:", lines.index(line))


class MCBuildOptions(BaseModel):
force_rebuild: bool = False
source: FileSystemPath = "./mcbuild"


def beet_default(ctx: Context):
ctx.require(mcbuild)


@configurable(validator=MCBuildOptions)
def mcbuild(ctx: Context, opts: MCBuildOptions):
config = ctx.directory / CONFIG_FILE
source = ctx.directory / opts.source
build_dir = ctx.cache["mcbuild"].directory / "datapack"
previous_source_hash = ctx.cache["mcbuild"].json.get("source_hash", None)

# Create default config if it doesn't exist
if not config.exists():
log("Default config not found, creating one...")
create_default_config(ctx)

source_hash = create_source_hash(source, config)

# Check if source has changed
if not opts.force_rebuild and source_hash == previous_source_hash:
# If the build directory doesn't exist, then somehow the cache was lost. We need to rebuild.
if (build_dir / "data").exists():
# Load the cached datapack
ctx.data.load(build_dir)
return

# Clear previous build
shutil.rmtree(build_dir, ignore_errors=True)
# Copy source
shutil.copytree(source, build_dir / "src", dirs_exist_ok=True)
# Copy config
shutil.copy(config, build_dir / CONFIG_FILE)
# Create dummy pack.mcmeta
with open(build_dir / "pack.mcmeta", "w") as f:
meta = {"pack": {"pack_format": ctx.data.pack_format}}
json.dump(meta, f)
# Run mcb
command = ["mcb", "build"]
try:
app = subprocess.run(
command,
cwd=build_dir,
check=True,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
)
errors = filter_stdout_for_soft_errors(app.stdout)
if errors:
log(errors)
except subprocess.CalledProcessError as e:
log(f"Error while running MCB: {e.stderr.decode()}")

# Load the built datapack
ctx.data.load(build_dir)

# Update source hash
ctx.cache["mcbuild"].json["source_hash"] = source_hash
Loading