|
5 | 5 | import os |
6 | 6 | import shutil |
7 | 7 | import argparse |
| 8 | +import re |
| 9 | +import sys |
8 | 10 |
|
9 | 11 | parser = argparse.ArgumentParser(description="Build plugins with CMake") |
10 | 12 | parser.add_argument( |
|
26 | 28 |
|
27 | 29 | args = parser.parse_args() |
28 | 30 |
|
| 31 | +# ── Sanity-check helpers ──────────────────────────────────────────────────── |
| 32 | + |
| 33 | +KNOWN_FORMATS = {"VST3", "AU", "LV2", "CLAP", "Standalone"} |
| 34 | +VERSION_RE = re.compile(r"^\d+\.\d+\.\d+$") |
| 35 | + |
| 36 | +errors = [] # fatal problems – abort after collecting all of them |
| 37 | +warnings = [] # non-fatal oddities |
| 38 | + |
| 39 | +def error(msg: str): |
| 40 | + errors.append(f" ERROR: {msg}") |
| 41 | + |
| 42 | +def warn(msg: str): |
| 43 | + warnings.append(f" WARNING: {msg}") |
| 44 | + |
| 45 | +def validate_config(path: str) -> list: |
| 46 | + """Load and validate config.json. Returns the parsed list or exits.""" |
| 47 | + if not os.path.isfile(path): |
| 48 | + print(f"FATAL: config.json not found at '{os.path.abspath(path)}'") |
| 49 | + sys.exit(1) |
| 50 | + |
| 51 | + try: |
| 52 | + with open(path) as f: |
| 53 | + data = json.load(f) |
| 54 | + except json.JSONDecodeError as e: |
| 55 | + print(f"FATAL: config.json is not valid JSON – {e}") |
| 56 | + sys.exit(1) |
| 57 | + |
| 58 | + if not isinstance(data, list): |
| 59 | + print("FATAL: config.json must contain a JSON array of plugin objects.") |
| 60 | + sys.exit(1) |
| 61 | + |
| 62 | + if len(data) == 0: |
| 63 | + warn("config.json contains no plugins – nothing to build.") |
| 64 | + |
| 65 | + return data |
| 66 | + |
| 67 | +def validate_plugin(plugin: dict, index: int): |
| 68 | + prefix = f"Plugin[{index}]" |
| 69 | + |
| 70 | + # ── Required fields ────────────────────────────────────────────────────── |
| 71 | + name = plugin.get("name") |
| 72 | + if not name: |
| 73 | + error(f"{prefix}: missing required field 'name'.") |
| 74 | + elif not isinstance(name, str) or not name.strip(): |
| 75 | + error(f"{prefix}: 'name' must be a non-empty string (got {name!r}).") |
| 76 | + |
| 77 | + path = plugin.get("path") |
| 78 | + if not path: |
| 79 | + error(f"{prefix} ({name!r}): missing required field 'path'.") |
| 80 | + else: |
| 81 | + resolved = Path(path).resolve() |
| 82 | + if not resolved.exists(): |
| 83 | + error(f"{prefix} ({name!r}): plugin path does not exist: '{resolved}'") |
| 84 | + elif not resolved.is_file(): |
| 85 | + error(f"{prefix} ({name!r}): plugin path exists but is not a file: '{resolved}'") |
| 86 | + |
| 87 | + # ── Optional but validated fields ──────────────────────────────────────── |
| 88 | + formats = plugin.get("formats", []) |
| 89 | + if not isinstance(formats, list): |
| 90 | + error(f"{prefix} ({name!r}): 'formats' must be a list, got {type(formats).__name__}.") |
| 91 | + else: |
| 92 | + if len(formats) == 0: |
| 93 | + warn(f"{prefix} ({name!r}): 'formats' is empty – no build targets will be produced.") |
| 94 | + for fmt in formats: |
| 95 | + if fmt not in KNOWN_FORMATS: |
| 96 | + warn(f"{prefix} ({name!r}): unknown format '{fmt}'. " |
| 97 | + f"Known formats are: {', '.join(sorted(KNOWN_FORMATS))}.") |
| 98 | + |
| 99 | + plugin_type = plugin.get("type", "") |
| 100 | + if plugin_type and plugin_type.lower() not in ("fx", "instrument", ""): |
| 101 | + warn(f"{prefix} ({name!r}): unexpected 'type' value '{plugin_type}'. " |
| 102 | + f"Expected 'fx' or 'instrument'.") |
| 103 | + |
| 104 | + version = plugin.get("version", "1.0.0") |
| 105 | + if not VERSION_RE.match(str(version)): |
| 106 | + warn(f"{prefix} ({name!r}): 'version' value '{version}' does not follow " |
| 107 | + f"MAJOR.MINOR.PATCH format.") |
| 108 | + |
| 109 | + for bool_field in ("enable_gem", "enable_sfizz", "enable_ffmpeg"): |
| 110 | + val = plugin.get(bool_field) |
| 111 | + if val is not None and not isinstance(val, bool): |
| 112 | + warn(f"{prefix} ({name!r}): '{bool_field}' should be a boolean, got {val!r}.") |
| 113 | + |
| 114 | +# ── Run validation ─────────────────────────────────────────────────────────── |
| 115 | + |
| 116 | +plugins_config = validate_config("config.json") |
| 117 | + |
| 118 | +for i, plugin in enumerate(plugins_config): |
| 119 | + if not isinstance(plugin, dict): |
| 120 | + error(f"Plugin[{i}]: expected an object, got {type(plugin).__name__}.") |
| 121 | + continue |
| 122 | + validate_plugin(plugin, i) |
| 123 | + |
| 124 | +if warnings: |
| 125 | + print("Build warnings:") |
| 126 | + for w in warnings: |
| 127 | + print(w) |
| 128 | + print() |
| 129 | + |
| 130 | +if errors: |
| 131 | + print("Build errors – cannot continue:") |
| 132 | + for e in errors: |
| 133 | + print(e) |
| 134 | + sys.exit(1) |
| 135 | + |
| 136 | +# ── Continue with the rest of the build ───────────────────────────────────── |
| 137 | + |
29 | 138 | system = platform.system() |
30 | 139 | if system == "Windows": |
31 | 140 | cmake_compiler = ["-DCMAKE_C_COMPILER=cl", "-DCMAKE_CXX_COMPILER=cl"] |
|
40 | 149 | else: |
41 | 150 | cmake_generator = ["-GNinja"] |
42 | 151 |
|
43 | | -# Load config.json |
44 | | -with open("config.json") as f: |
45 | | - plugins_config = json.load(f) |
46 | | - |
47 | 152 | plugdata_dir = Path("plugdata").resolve() |
48 | | -builds_parent_dir = plugdata_dir.parent # Build folders go here |
| 153 | +builds_parent_dir = plugdata_dir.parent |
49 | 154 |
|
50 | 155 | plugins_dir = os.path.join("plugdata", "Plugins") |
51 | 156 | build_output_dir = os.path.join("Build") |
52 | 157 | os.makedirs(build_output_dir, exist_ok=True) |
53 | 158 |
|
54 | 159 | if not plugdata_dir.is_dir(): |
55 | | - print(f"plugdata directory not found: {plugdata_dir}") |
56 | | - exit(1) |
| 160 | + print(f"FATAL: plugdata directory not found at '{plugdata_dir}'. " |
| 161 | + f"Make sure you're running this script from the repo root and that " |
| 162 | + f"the plugdata submodule has been initialised (git submodule update --init).") |
| 163 | + sys.exit(1) |
57 | 164 |
|
58 | 165 | for plugin in plugins_config: |
59 | 166 | name = plugin["name"] |
60 | 167 | zip_path = Path(plugin["path"]).resolve() |
| 168 | + patch = plugin["patch"] |
61 | 169 | formats = plugin.get("formats", []) |
62 | 170 | is_fx = plugin.get("type", "").lower() == "fx" |
63 | 171 |
|
|
77 | 185 | *cmake_compiler, |
78 | 186 | f"-B{build_dir}", |
79 | 187 | f"-DCUSTOM_PLUGIN_NAME={name}", |
| 188 | + f"-DCUSTOM_PLUGIN_PATCH={patch}", |
80 | 189 | f"-DCUSTOM_PLUGIN_PATH={zip_path}", |
81 | 190 | f"-DCUSTOM_PLUGIN_COMPANY={author}", |
82 | 191 | f"-DCUSTOM_PLUGIN_VERSION={version}", |
|
96 | 205 | print(f"Failed cmake configure for {name}") |
97 | 206 | continue |
98 | 207 |
|
99 | | - # Build all combinations of type + format |
100 | 208 | if not args.configure_only: |
101 | 209 | for fmt in formats: |
102 | 210 | if system != "Darwin" and fmt == "AU": |
|
136 | 244 | elif fmt == "CLAP": |
137 | 245 | extension = ".clap" |
138 | 246 |
|
139 | | - plugin_filename = name + extension; |
| 247 | + plugin_filename = name + extension |
140 | 248 | os.makedirs(target_dir, exist_ok=True) |
141 | | - src = os.path.join(format_path, plugin_filename); |
142 | | - dst = os.path.join(target_dir, plugin_filename); |
| 249 | + src = os.path.join(format_path, plugin_filename) |
| 250 | + dst = os.path.join(target_dir, plugin_filename) |
143 | 251 | if os.path.isdir(src): |
144 | 252 | if os.path.exists(dst): |
145 | 253 | shutil.rmtree(dst) |
|
0 commit comments