summaryrefslogtreecommitdiff
path: root/docs/handbook/meta/architecture.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/handbook/meta/architecture.md')
-rw-r--r--docs/handbook/meta/architecture.md624
1 files changed, 624 insertions, 0 deletions
diff --git a/docs/handbook/meta/architecture.md b/docs/handbook/meta/architecture.md
new file mode 100644
index 0000000000..6790a82fe5
--- /dev/null
+++ b/docs/handbook/meta/architecture.md
@@ -0,0 +1,624 @@
+# Meta — Architecture
+
+## Module Structure
+
+Meta is organized as a standard Python package with three sub-packages, each serving a distinct role in the pipeline:
+
+```
+meta/
+├── __init__.py # Package marker: """Meta package of meta"""
+├── common/ # Shared utilities, constants, and static data
+├── model/ # Pydantic data models for all upstream formats
+└── run/ # Executable scripts (update_*, generate_*, index)
+```
+
+### Dependency Flow
+
+```
+run/ ──depends-on──► model/ ──depends-on──► common/
+run/ ──depends-on──► common/
+```
+
+The `run/` modules import from both `model/` and `common/`. The `model/` package imports from `common/` for shared constants and utilities. There are **no** circular dependencies.
+
+---
+
+## The `common/` Package
+
+### `common/__init__.py` — Core Utilities
+
+This module provides the foundational infrastructure used by every other module:
+
+#### Path Resolution
+
+Three functions resolve working directories from environment variables with filesystem fallbacks:
+
+```python
+def cache_path():
+ if "META_CACHE_DIR" in os.environ:
+ return os.environ["META_CACHE_DIR"]
+ return "cache"
+
+def launcher_path():
+ if "META_LAUNCHER_DIR" in os.environ:
+ return os.environ["META_LAUNCHER_DIR"]
+ return "launcher"
+
+def upstream_path():
+ if "META_UPSTREAM_DIR" in os.environ:
+ return os.environ["META_UPSTREAM_DIR"]
+ return "upstream"
+```
+
+#### Directory Creation
+
+```python
+def ensure_upstream_dir(path):
+ """Create a subdirectory under the upstream root."""
+ path = os.path.join(upstream_path(), path)
+ if not os.path.exists(path):
+ os.makedirs(path)
+
+def ensure_component_dir(component_id: str):
+ """Create a component directory under the launcher root."""
+ path = os.path.join(launcher_path(), component_id)
+ if not os.path.exists(path):
+ os.makedirs(path)
+```
+
+#### HTTP Session Factory
+
+```python
+def default_session():
+ cache = FileCache(os.path.join(cache_path(), "http_cache"))
+ sess = CacheControl(requests.Session(), cache)
+ sess.headers.update({"User-Agent": "ProjectTickMeta/1.0"})
+ return sess
+```
+
+All HTTP requests are made through this cached session. The `CacheControl` wrapper stores responses in a disk-backed `FileCache`, honoring standard HTTP caching headers.
+
+#### Datetime Serialization
+
+```python
+def serialize_datetime(dt: datetime.datetime):
+ if dt.tzinfo is None:
+ return dt.replace(tzinfo=datetime.timezone.utc).isoformat()
+ return dt.isoformat()
+```
+
+Used as Pydantic's custom JSON encoder for `datetime` fields — naive datetimes are assumed UTC.
+
+#### File Hashing
+
+```python
+def file_hash(filename: str, hashtype: Callable[[], "hashlib._Hash"], blocksize: int = 65536) -> str:
+ hashtype = hashtype()
+ with open(filename, "rb") as f:
+ for block in iter(lambda: f.read(blocksize), b""):
+ hashtype.update(block)
+ return hashtype.hexdigest()
+```
+
+Used throughout the pipeline for SHA-1 and SHA-256 integrity checksums on installer JARs and version files.
+
+#### SHA-1 Caching
+
+```python
+def get_file_sha1_from_file(file_name: str, sha1_file: str) -> Optional[str]:
+```
+
+Reads a cached `.sha1` sidecar file if it exists; otherwise computes and writes the SHA-1 hash. Used by Forge/NeoForge update scripts to detect when an installer JAR needs re-downloading.
+
+#### Other Utilities
+
+| Function | Purpose |
+|---|---|
+| `transform_maven_key(key)` | Replaces `:` with `.` in Maven coordinates for filesystem paths |
+| `replace_old_launchermeta_url(url)` | Rewrites `launchermeta.mojang.com` → `piston-meta.mojang.com` |
+| `merge_dict(base, overlay)` | Deep-merges two dicts (base provides defaults, overlay wins) |
+| `get_all_bases(cls)` | Returns the complete MRO (method resolution order) for a class |
+| `remove_files(file_paths)` | Silently deletes a list of files |
+| `eprint(*args)` | Prints to stderr |
+| `LAUNCHER_MAVEN` | URL template: `"https://files.projecttick.org/maven/%s"` |
+
+### `common/http.py` — Binary Downloads
+
+A single function:
+
+```python
+def download_binary_file(sess, path, url):
+ with open(path, "wb") as f:
+ r = sess.get(url)
+ r.raise_for_status()
+ for chunk in r.iter_content(chunk_size=128):
+ f.write(chunk)
+```
+
+Used to download Forge/NeoForge installer JARs and Fabric/Quilt JAR files.
+
+### `common/mojang.py` — Mojang Constants
+
+```python
+BASE_DIR = "mojang"
+VERSION_MANIFEST_FILE = join(BASE_DIR, "version_manifest_v2.json")
+VERSIONS_DIR = join(BASE_DIR, "versions")
+ASSETS_DIR = join(BASE_DIR, "assets")
+
+STATIC_EXPERIMENTS_FILE = join(dirname(__file__), "mojang-minecraft-experiments.json")
+STATIC_OLD_SNAPSHOTS_FILE = join(dirname(__file__), "mojang-minecraft-old-snapshots.json")
+STATIC_OVERRIDES_FILE = join(dirname(__file__), "mojang-minecraft-legacy-override.json")
+STATIC_LEGACY_SERVICES_FILE = join(dirname(__file__), "mojang-minecraft-legacy-services.json")
+LIBRARY_PATCHES_FILE = join(dirname(__file__), "mojang-library-patches.json")
+
+MINECRAFT_COMPONENT = "net.minecraft"
+LWJGL_COMPONENT = "org.lwjgl"
+LWJGL3_COMPONENT = "org.lwjgl3"
+JAVA_MANIFEST_FILE = join(BASE_DIR, "java_all.json")
+```
+
+### `common/forge.py` — Forge Constants
+
+```python
+BASE_DIR = "forge"
+JARS_DIR = join(BASE_DIR, "jars")
+INSTALLER_INFO_DIR = join(BASE_DIR, "installer_info")
+INSTALLER_MANIFEST_DIR = join(BASE_DIR, "installer_manifests")
+VERSION_MANIFEST_DIR = join(BASE_DIR, "version_manifests")
+FILE_MANIFEST_DIR = join(BASE_DIR, "files_manifests")
+DERIVED_INDEX_FILE = join(BASE_DIR, "derived_index.json")
+LEGACYINFO_FILE = join(BASE_DIR, "legacyinfo.json")
+
+FORGE_COMPONENT = "net.minecraftforge"
+
+FORGEWRAPPER_LIBRARY = make_launcher_library(
+ GradleSpecifier("io.github.zekerzhayard", "ForgeWrapper", "projt-2026-04-04"),
+ "4c4653d80409e7e968d3e3209196ffae778b7b4e",
+ 29731,
+)
+
+BAD_VERSIONS = ["1.12.2-14.23.5.2851"]
+```
+
+The `FORGEWRAPPER_LIBRARY` is a pre-built `Library` object pointing to a custom ForgeWrapper build hosted on the ProjT Maven. ForgeWrapper acts as a shim layer to run modern Forge installers at launch time.
+
+### `common/neoforge.py` — NeoForge Constants
+
+```python
+BASE_DIR = "neoforge"
+NEOFORGE_COMPONENT = "net.neoforged"
+```
+
+Similar directory layout to Forge, but with its own `DERIVED_INDEX_FILE`.
+
+### `common/fabric.py` — Fabric Constants
+
+```python
+BASE_DIR = "fabric"
+JARS_DIR = join(BASE_DIR, "jars")
+INSTALLER_INFO_DIR = join(BASE_DIR, "loader-installer-json")
+META_DIR = join(BASE_DIR, "meta-v2")
+
+LOADER_COMPONENT = "net.fabricmc.fabric-loader"
+INTERMEDIARY_COMPONENT = "net.fabricmc.intermediary"
+
+DATETIME_FORMAT_HTTP = "%a, %d %b %Y %H:%M:%S %Z"
+```
+
+### `common/quilt.py` — Quilt Constants
+
+```python
+USE_QUILT_MAPPINGS = False # Quilt recommends using Fabric's intermediary
+
+BASE_DIR = "quilt"
+LOADER_COMPONENT = "org.quiltmc.quilt-loader"
+INTERMEDIARY_COMPONENT = "org.quiltmc.hashed"
+
+# If USE_QUILT_MAPPINGS is False, uses Fabric's intermediary instead
+if not USE_QUILT_MAPPINGS:
+ INTERMEDIARY_COMPONENT = FABRIC_INTERMEDIARY_COMPONENT
+
+DISABLE_BEACON_ARG = "-Dloader.disable_beacon=true"
+DISABLE_BEACON_VERSIONS = {
+ "0.19.2-beta.3", "0.19.2-beta.4", ..., "0.20.0-beta.14",
+}
+```
+
+The `DISABLE_BEACON_VERSIONS` set enumerates Quilt Loader versions that had a telemetry beacon, which is disabled via a JVM argument.
+
+### `common/java.py` — Java Runtime Constants
+
+```python
+BASE_DIR = "java_runtime"
+ADOPTIUM_DIR = join(BASE_DIR, "adoptium")
+OPENJ9_DIR = join(BASE_DIR, "ibm")
+AZUL_DIR = join(BASE_DIR, "azul")
+
+JAVA_MINECRAFT_COMPONENT = "net.minecraft.java"
+JAVA_ADOPTIUM_COMPONENT = "net.adoptium.java"
+JAVA_OPENJ9_COMPONENT = "com.ibm.java"
+JAVA_AZUL_COMPONENT = "com.azul.java"
+```
+
+---
+
+## The `model/` Package
+
+All data models inherit from `MetaBase`, which is a customized `pydantic.BaseModel`.
+
+### Inheritance Hierarchy
+
+```
+pydantic.BaseModel
+└── MetaBase
+ ├── Versioned (adds formatVersion field)
+ │ ├── MetaVersion # Primary output format
+ │ ├── MetaPackage # Package metadata
+ │ ├── MetaVersionIndex # Version list per package
+ │ └── MetaPackageIndex # Master package list
+ │
+ ├── MojangArtifactBase # sha1, sha256, size, url
+ │ ├── MojangAssets # Asset index metadata
+ │ ├── MojangArtifact # Library artifact with path
+ │ └── MojangLoggingArtifact # Logging config artifact
+ │
+ ├── Library # Minecraft library reference
+ │ ├── JavaAgent # Library with Java agent argument
+ │ ├── ForgeLibrary # Forge-specific library fields
+ │ └── NeoForgeLibrary # NeoForge-specific library fields
+ │
+ ├── GradleSpecifier # Maven coordinate (not a MetaBase)
+ ├── Dependency # Component dependency (uid + version)
+ │
+ ├── MojangVersion # Raw Mojang version JSON
+ │ ├── ForgeVersionFile # Forge version JSON (extends Mojang)
+ │ └── NeoForgeVersionFile # NeoForge version JSON
+ │
+ ├── OSRule / MojangRule / MojangRules # Platform rules
+ │
+ ├── ForgeEntry / NeoForgeEntry # Version index entries
+ ├── DerivedForgeIndex / DerivedNeoForgeIndex # Reconstructed indexes
+ ├── ForgeInstallerProfile / ForgeInstallerProfileV2 # Installer data
+ │
+ ├── FabricInstallerDataV1 # Fabric loader installer info
+ ├── FabricJarInfo # JAR release timestamp
+ │
+ ├── LiteloaderIndex # Full LiteLoader metadata
+ │
+ ├── JavaRuntimeMeta # Normalized Java runtime info
+ ├── JavaRuntimeVersion # MetaVersion with runtimes list
+ ├── JavaVersionMeta # Semver-style Java version
+ │
+ └── APIQuery # Base for API URL query builders
+ ├── AdoptxAPIFeatureReleasesQuery
+ └── AzulApiPackagesQuery
+```
+
+### `MetaBase` — The Foundation
+
+```python
+class MetaBase(pydantic.BaseModel):
+ def dict(self, **kwargs):
+ return super().dict(by_alias=True, **kwargs)
+
+ def json(self, **kwargs):
+ return super().json(
+ exclude_none=True, sort_keys=True, by_alias=True, indent=4, **kwargs
+ )
+
+ def write(self, file_path: str):
+ Path(file_path).parent.mkdir(parents=True, exist_ok=True)
+ with open(file_path, "w") as f:
+ f.write(self.json())
+
+ def merge(self, other: "MetaBase"):
+ """Merge other into self: concatenate lists, union sets, deep-merge dicts,
+ recurse on MetaBase fields, overwrite primitives."""
+
+ class Config:
+ allow_population_by_field_name = True
+ json_encoders = {datetime: serialize_datetime, GradleSpecifier: str}
+```
+
+Key design decisions:
+- **`by_alias=True`** everywhere: Field aliases like `mainClass`, `releaseTime`, `assetIndex` match the JSON format the launcher expects.
+- **`exclude_none=True`**: Optional fields that aren't set are omitted from output.
+- **`sort_keys=True`**: Deterministic output for diff-friendly Git commits.
+- **`write()`**: Every model can serialize itself to disk, creating parent directories as needed.
+
+### `MetaVersion` — The Core Output Model
+
+This is the primary data structure that Meta produces. Each component version (Minecraft 1.21.5, Forge 49.0.31, Fabric Loader 0.16.9, etc.) is represented as a `MetaVersion`:
+
+```python
+class MetaVersion(Versioned):
+ name: str # Human-readable name ("Minecraft", "Forge", etc.)
+ version: str # Version string
+ uid: str # Component UID ("net.minecraft", "net.minecraftforge")
+ type: Optional[str] # "release", "snapshot", "experiment", "old_snapshot"
+ order: Optional[int] # Load order (-2=MC, -1=LWJGL, 5=Forge, 10=Fabric)
+ volatile: Optional[bool] # If true, may change between runs (e.g., LWJGL, mappings)
+ requires: Optional[List[Dependency]] # Required components (with version constraints)
+ conflicts: Optional[List[Dependency]] # Conflicting components
+ libraries: Optional[List[Library]] # Runtime classpath libraries
+ asset_index: Optional[MojangAssets] # Asset index reference
+ maven_files: Optional[List[Library]] # Install-time Maven downloads
+ main_jar: Optional[Library] # Main game JAR
+ jar_mods: Optional[List[Library]] # Legacy JAR mod injection
+ main_class: Optional[str] # Java main class
+ applet_class: Optional[str] # Legacy applet class
+ minecraft_arguments: Optional[str] # Game launch arguments
+ release_time: Optional[datetime] # When this version was released
+ compatible_java_majors: Optional[List[int]] # Compatible Java major versions
+ compatible_java_name: Optional[str] # Mojang Java component name
+ java_agents: Optional[List[JavaAgent]] # Java agent libraries
+ additional_traits: Optional[List[str]] # Launcher behavior hints
+ additional_tweakers: Optional[List[str]]# Forge/LiteLoader tweaker classes
+ additional_jvm_args: Optional[List[str]]# Extra JVM arguments
+ logging: Optional[MojangLogging] # Log4j logging configuration
+```
+
+### `GradleSpecifier` — Maven Coordinates
+
+Not a Pydantic model but a core class used as a Pydantic-compatible type:
+
+```python
+class GradleSpecifier:
+ """Maven coordinate like 'org.lwjgl.lwjgl:lwjgl:2.9.0' or
+ 'com.mojang:minecraft:1.21.5:client'"""
+
+ def __init__(self, group, artifact, version, classifier=None, extension=None):
+ # extension defaults to "jar"
+
+ def filename(self): # e.g., "lwjgl-2.9.0.jar"
+ def base(self): # e.g., "org/lwjgl/lwjgl/lwjgl/2.9.0/"
+ def path(self): # e.g., "org/lwjgl/lwjgl/lwjgl/2.9.0/lwjgl-2.9.0.jar"
+ def is_lwjgl(self): # True for org.lwjgl, org.lwjgl.lwjgl, java.jinput, java.jutils
+ def is_log4j(self): # True for org.apache.logging.log4j
+
+ @classmethod
+ def from_string(cls, v: str):
+ # Parses "group:artifact:version[:classifier][@extension]"
+```
+
+This class supports Pydantic validators via `__get_validators__`, comparison operators for sorting, and hashing for use in sets.
+
+---
+
+## The `run/` Package
+
+### Module Naming Convention
+
+Every module follows a strict naming pattern:
+
+- `update_<loader>.py` — Phase 1: fetch upstream data
+- `generate_<loader>.py` — Phase 2: produce launcher metadata
+- `index.py` — Final step: build the master package index
+
+### Common Patterns Across Run Modules
+
+1. **Module-level initialization**: Upstream directories are created at import time:
+ ```python
+ UPSTREAM_DIR = upstream_path()
+ ensure_upstream_dir(JARS_DIR)
+ sess = default_session()
+ ```
+
+2. **`main()` entry point**: Every module exposes a `main()` function called by `update.sh` or the Poetry entrypoints.
+
+3. **Concurrent processing**: Most modules use `concurrent.futures.ThreadPoolExecutor`. The Fabric updater uniquely uses `multiprocessing.Pool`.
+
+4. **Error handling**: Errors during version processing skip individual versions rather than failing the entire pipeline (with `eprint()` logging).
+
+---
+
+## Pipeline Architecture
+
+### Data Flow Diagram
+
+```
+┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
+│ Upstream APIs │ │ upstream/ repo │ │ launcher/ repo │
+│ (Mojang, Forge, │────►│ (raw JSON, JARs, │────►│ (MetaVersion │
+│ NeoForge, etc.)│ │ manifests) │ │ JSON files) │
+└──────────────────┘ └──────────────────┘ └──────────────────┘
+ Phase 1: UPDATE Intermediate Phase 2: GENERATE
+ (fetch + cache) Storage (transform + write)
+```
+
+### Phase 1: Update Pipeline
+
+Each `update_*` module follows this pattern:
+
+1. **Fetch index**: GET the version manifest or release list from the upstream API.
+2. **Diff against local**: Compare remote version list with what's already in `upstream/`.
+3. **Download new/changed**: Fetch only what's new or modified.
+4. **Extract install data**: For Forge/NeoForge, extract `install_profile.json` and `version.json` from installer JARs.
+5. **Write to upstream**: Serialize all data to `upstream/<loader>/`.
+
+### Phase 2: Generate Pipeline
+
+Each `generate_*` module follows this pattern:
+
+1. **Load upstream data**: Parse the raw data from `upstream/`.
+2. **Transform**: Convert upstream-specific models into `MetaVersion` objects.
+3. **Apply patches**: Merge library patches, override legacy data, fix known issues.
+4. **Write to launcher**: Serialize `MetaVersion` and `MetaPackage` JSON into `launcher/<component_uid>/`.
+
+### Index Building
+
+The `index.py` module (Phase 2, final step):
+
+1. Walks every directory in `launcher/`.
+2. Reads each `package.json` to get package metadata.
+3. For each version file, computes SHA-256 and creates a `MetaVersionIndexEntry`.
+4. Sorts versions by `release_time` (descending).
+5. Writes per-package `index.json` files.
+6. Produces the master `index.json` with SHA-256 hashes of each package index.
+
+```python
+# From index.py — the core indexing logic:
+for package in sorted(os.listdir(LAUNCHER_DIR)):
+ sharedData = MetaPackage.parse_file(package_json_path)
+ versionList = MetaVersionIndex(uid=package, name=sharedData.name)
+
+ for filename in os.listdir(package_path):
+ filehash = file_hash(filepath, hashlib.sha256)
+ versionFile = MetaVersion.parse_file(filepath)
+ is_recommended = versionFile.version in recommendedVersions
+ versionEntry = MetaVersionIndexEntry.from_meta_version(
+ versionFile, is_recommended, filehash
+ )
+ versionList.versions.append(versionEntry)
+
+ versionList.versions = sorted(
+ versionList.versions, key=attrgetter("release_time"), reverse=True
+ )
+ versionList.write(outFilePath)
+```
+
+---
+
+## Component Dependency Graph
+
+Components declare dependencies via the `requires` and `conflicts` fields:
+
+```
+net.minecraft
+├── requires: org.lwjgl (or org.lwjgl3)
+│
+net.minecraftforge
+├── requires: net.minecraft (equals=<mc_version>)
+│
+net.neoforged
+├── requires: net.minecraft (equals=<mc_version>)
+│
+net.fabricmc.fabric-loader
+├── requires: net.fabricmc.intermediary
+│
+net.fabricmc.intermediary
+├── requires: net.minecraft (equals=<mc_version>)
+│
+org.quiltmc.quilt-loader
+├── requires: net.fabricmc.intermediary (or org.quiltmc.hashed)
+│
+org.lwjgl ◄──conflicts──► org.lwjgl3
+```
+
+---
+
+## Version Ordering
+
+The `order` field controls in what sequence the launcher processes components:
+
+| Order | Component |
+|---|---|
+| -2 | `net.minecraft` |
+| -1 | `org.lwjgl` / `org.lwjgl3` |
+| 5 | `net.minecraftforge` / `net.neoforged` |
+| 10 | `net.fabricmc.fabric-loader` / `org.quiltmc.quilt-loader` |
+| 11 | `net.fabricmc.intermediary` |
+
+Lower order = loaded first. Minecraft is always base, LWJGL provides native libraries, then mod loaders layer on top.
+
+---
+
+## Library Patching System
+
+The `LibraryPatches` system (`model/mojang.py`) allows surgically modifying or extending libraries in generated Minecraft versions:
+
+```python
+class LibraryPatch(MetaBase):
+ match: List[GradleSpecifier] # Which libraries to match
+ override: Optional[Library] # Fields to merge into matched lib
+ additionalLibraries: Optional[List[Library]] # Extra libs to add
+ patchAdditionalLibraries: bool = False # Recurse on additions?
+```
+
+The `patch_library()` function in `generate_mojang.py` applies patches:
+
+```python
+def patch_library(lib: Library, patches: LibraryPatches) -> List[Library]:
+ to_patch = [lib]
+ new_libraries = []
+ while to_patch:
+ target = to_patch.pop(0)
+ for patch in patches:
+ if patch.applies(target):
+ if patch.override:
+ target.merge(patch.override)
+ if patch.additionalLibraries:
+ additional_copy = copy.deepcopy(patch.additionalLibraries)
+ new_libraries += list(dict.fromkeys(additional_copy))
+ if patch.patchAdditionalLibraries:
+ to_patch += additional_copy
+ return new_libraries
+```
+
+This system is used to inject missing ARM64 natives, add supplementary libraries, and fix broken upstream metadata.
+
+---
+
+## LWJGL Version Selection
+
+One of the most complex parts of `generate_mojang.py` is LWJGL version deduplication. Mojang's version manifests include LWJGL libraries inline with every Minecraft version, but the launcher manages LWJGL as a separate component. Meta must:
+
+1. **Extract** LWJGL libraries from each Minecraft version.
+2. **Group** them into "variants" by hashing the library set (excluding release time).
+3. **Select** the correct variant using curated lists (`PASS_VARIANTS` and `BAD_VARIANTS`).
+4. **Write** each unique LWJGL version as its own component file.
+
+```python
+PASS_VARIANTS = [
+ "1fd0e4d1f0f7c97e8765a69d38225e1f27ee14ef", # 3.4.1
+ "2b00f31688148fc95dbc8c8ef37308942cf0dce0", # 3.3.6
+ ...
+]
+
+BAD_VARIANTS = [
+ "6442fc475f501fbd0fc4244fd1c38c02d9ebaf7e", # 3.3.3 (broken freetype)
+ ...
+]
+```
+
+Each LWJGL variant is identified by a SHA-1 hash of its serialized library list. Only variants in `PASS_VARIANTS` are accepted; those in `BAD_VARIANTS` are rejected; unknown variants raise an exception.
+
+---
+
+## Split Natives Workaround
+
+Modern Minecraft versions (1.19+) use "split natives" — native libraries are separate Maven artifacts with classifiers like `natives-linux`, `natives-windows`, etc. The launcher has a bug handling these, so Meta applies a workaround:
+
+```python
+APPLY_SPLIT_NATIVES_WORKAROUND = True
+
+if APPLY_SPLIT_NATIVES_WORKAROUND and lib_is_split_native(lib):
+ specifier.artifact += f"-{specifier.classifier}"
+ specifier.classifier = None
+```
+
+This merges the classifier into the artifact name, effectively renaming `lwjgl:3.3.3:natives-linux` to `lwjgl-natives-linux:3.3.3`.
+
+---
+
+## ForgeWrapper Integration
+
+Modern Forge (post-1.12.2) uses an installer-based system that runs processors at install time. The launcher cannot run these processors directly, so Meta injects ForgeWrapper as the main class:
+
+```python
+v.main_class = "io.github.zekerzhayard.forgewrapper.installer.Main"
+```
+
+ForgeWrapper runs the Forge installer's processors transparently when the game is first launched. The installer JAR itself is included under `mavenFiles` so the launcher downloads it alongside regular libraries.
+
+---
+
+## Error Recovery and Resilience
+
+The pipeline is designed to be resumable and fault-tolerant:
+
+1. **Incremental updates**: Only new or changed versions are downloaded.
+2. **SHA-1 verification**: Installer JARs are re-downloaded if their SHA-1 changes.
+3. **Cached intermediates**: Installer profiles and manifests are cached to disk.
+4. **Git reset on failure**: `update.sh` runs `git reset --hard HEAD` on the upstream/launcher repos before starting, and again on failure via `fail_in()`/`fail_out()`.
+5. **Per-version error handling**: A failing version logs an error but doesn't abort the entire pipeline.