From c2e96c03bc11a1fa3506bb4dcd34efa898db9c6f Mon Sep 17 00:00:00 2001 From: SnaveSutit Date: Mon, 27 Jan 2025 14:40:15 -0500 Subject: [PATCH 1/7] feat: add beet.contrib.mcbuild --- beet/contrib/mcbuild.py | 115 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 beet/contrib/mcbuild.py diff --git a/beet/contrib/mcbuild.py b/beet/contrib/mcbuild.py new file mode 100644 index 00000000..94646def --- /dev/null +++ b/beet/contrib/mcbuild.py @@ -0,0 +1,115 @@ +"""Plugin that builds a MCBuild project.""" + +__all__ = ["beet_default", "MCBuildOptions"] + + +from typing import Optional, Any +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" +OG_PRINT = print + + +def log(message: str) -> None: + """Print with a custom prefix.""" + OG_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() + + +class MCBuildOptions(BaseModel): + forced_update: 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.forced_update and source_hash == previous_source_hash: + # log("Source has not changed, skipping build.") + 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: + subprocess.run( + command, cwd=build_dir, check=True, capture_output=True, shell=True + ) + 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 From b2b6a6dfbac576bd9b1fe1efad635366621f6222 Mon Sep 17 00:00:00 2001 From: SnaveSutit Date: Mon, 27 Jan 2025 14:42:08 -0500 Subject: [PATCH 2/7] chore: rename `forced_update` to `force_rebuild` --- beet/contrib/mcbuild.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beet/contrib/mcbuild.py b/beet/contrib/mcbuild.py index 94646def..466bdc7d 100644 --- a/beet/contrib/mcbuild.py +++ b/beet/contrib/mcbuild.py @@ -62,7 +62,7 @@ def update_from_directory(path: Path): class MCBuildOptions(BaseModel): - forced_update: bool = False + force_rebuild: bool = False source: FileSystemPath = "./mcbuild" @@ -85,7 +85,7 @@ def mcbuild(ctx: Context, opts: MCBuildOptions): source_hash = create_source_hash(source, config) # Check if source has changed - if not opts.forced_update and source_hash == previous_source_hash: + if not opts.force_rebuild and source_hash == previous_source_hash: # log("Source has not changed, skipping build.") return From 546fe9d10cc3f31a56f3beab34bf2ffbd65f7db7 Mon Sep 17 00:00:00 2001 From: SnaveSutit Date: Mon, 27 Jan 2025 14:52:03 -0500 Subject: [PATCH 3/7] fix: edgecase where cache was erased but source hash matches --- beet/contrib/mcbuild.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/beet/contrib/mcbuild.py b/beet/contrib/mcbuild.py index 466bdc7d..5e8d1d78 100644 --- a/beet/contrib/mcbuild.py +++ b/beet/contrib/mcbuild.py @@ -86,8 +86,11 @@ def mcbuild(ctx: Context, opts: MCBuildOptions): # Check if source has changed if not opts.force_rebuild and source_hash == previous_source_hash: - # log("Source has not changed, skipping build.") - return + # 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) From a62936427524ba49a624fd482a67fc8dd243068b Mon Sep 17 00:00:00 2001 From: SnaveSutit Date: Mon, 27 Jan 2025 15:24:08 -0500 Subject: [PATCH 4/7] fix: remove left-over OG_PRINT variable --- beet/contrib/mcbuild.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/beet/contrib/mcbuild.py b/beet/contrib/mcbuild.py index 5e8d1d78..0c72bc01 100644 --- a/beet/contrib/mcbuild.py +++ b/beet/contrib/mcbuild.py @@ -13,12 +13,11 @@ DEFAULT_CONFIG_URL = "https://raw.githubusercontent.com/mc-build/mcb/refs/heads/main/template/mcb.config.js" CONFIG_FILE = "mcb.config.js" -OG_PRINT = print def log(message: str) -> None: """Print with a custom prefix.""" - OG_PRINT( + 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, ) From 4f4561c57f7c5c1cd2348c63f74a475f2beda584 Mon Sep 17 00:00:00 2001 From: SnaveSutit Date: Mon, 27 Jan 2025 15:24:46 -0500 Subject: [PATCH 5/7] feat: add parser error logging --- beet/contrib/mcbuild.py | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/beet/contrib/mcbuild.py b/beet/contrib/mcbuild.py index 0c72bc01..3aa7b419 100644 --- a/beet/contrib/mcbuild.py +++ b/beet/contrib/mcbuild.py @@ -60,6 +60,25 @@ def update_from_directory(path: Path): 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)) + + class MCBuildOptions(BaseModel): force_rebuild: bool = False source: FileSystemPath = "./mcbuild" @@ -104,9 +123,18 @@ def mcbuild(ctx: Context, opts: MCBuildOptions): # Run mcb command = ["mcb", "build"] try: - subprocess.run( - command, cwd=build_dir, check=True, capture_output=True, shell=True + 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()}") From 3d309e0214b554f9d86c18693488fdca3bd5ff08 Mon Sep 17 00:00:00 2001 From: SnaveSutit Date: Mon, 27 Jan 2025 15:33:10 -0500 Subject: [PATCH 6/7] fix: add missing compiler error check --- beet/contrib/mcbuild.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/beet/contrib/mcbuild.py b/beet/contrib/mcbuild.py index 3aa7b419..7a52d6c5 100644 --- a/beet/contrib/mcbuild.py +++ b/beet/contrib/mcbuild.py @@ -77,6 +77,8 @@ def capture_error(errorTitle: str, index: int): 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): From 4cfa718861ef6457e323ff6b33f91603ce7cb8c4 Mon Sep 17 00:00:00 2001 From: SnaveSutit Date: Mon, 27 Jan 2025 15:47:14 -0500 Subject: [PATCH 7/7] chore: remove unused Any export --- beet/contrib/mcbuild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beet/contrib/mcbuild.py b/beet/contrib/mcbuild.py index 7a52d6c5..dc10089b 100644 --- a/beet/contrib/mcbuild.py +++ b/beet/contrib/mcbuild.py @@ -3,7 +3,7 @@ __all__ = ["beet_default", "MCBuildOptions"] -from typing import Optional, Any +from typing import Optional from beet import Context, configurable from pydantic.v1 import BaseModel from beet.core.utils import FileSystemPath