The launch pipeline ties everything together: fetch loader metadata, ensure Java is present, install the 8 buckets in parallel, build the argv, spawn the JVM, register the PID, stream stdio.
.launch(profile, java).run()
│
▼
1. Prepare metadata (loader-specific HTTP fetch + parse)
2. Ensure Java installed (download if missing; lighty-java)
3. Install dependencies (8 parallel buckets; see installation.md)
4. Forge/NeoForge post-hook (install_profile libs + processors)
5. Build argv (placeholders + JVM + game args)
6. Spawn JVM (Command::spawn; register PID)
7. Stream stdio + lifecycle (see instance-lifecycle.md)
Adds .launch(...) to any installable instance:
pub trait Launch {
fn launch<'a>(
&'a mut self,
profile: &'a UserProfile,
java_distribution: JavaDistribution,
) -> LaunchBuilder<'a, Self>
where Self: Sized;
}Blanket-implemented for every type that satisfies the pipeline's
bounds (VersionInfo<LoaderType = Loader> + LoaderExtensions + Arguments + Installer + WithMods) — that's VersionBuilder and
LightyVersionBuilder out of the box. WithMods returns an empty
slice on vanilla instances, so it's free.
pub struct LaunchBuilder<'a, T> { /* … */ }
impl<'a, T> LaunchBuilder<'a, T>
where T: VersionInfo<LoaderType = Loader> + LoaderExtensions + Arguments + Installer + WithMods
{
pub fn with_jvm_options(self) -> JvmOptionsBuilder<'a, T>;
pub fn with_arguments (self) -> ArgumentsBuilder<'a, T>;
#[cfg(feature = "events")]
pub fn with_event_bus(self, bus: &'a EventBus) -> Self;
pub async fn run(self) -> InstallerResult<()>;
}with_jvm_options() / with_arguments() open sub-builders that take
.set(key, value) / .remove(key) / .done() to return to the
parent. See arguments.md for the placeholder catalogue.
Dispatches to the loader's get_metadata() (Vanilla, Fabric, Quilt,
Forge, NeoForge, …). With events enabled, emits
LoaderEvent::FetchingData then LoaderEvent::DataFetched. Per-loader
detail and the actual HTTP endpoints live in
crates/loaders/docs/loaders/.
Extracts version.java_version.major_version from the metadata, asks
lighty-java for a matching JRE. If
missing, downloads from the requested distribution (Temurin, Zulu,
Graal, Liberica) and extracts to java_dirs()/jre/java-<v>/. Emits
JavaEvent::{JavaAlreadyInstalled, JavaNotFound, …}.
8-way parallel tokio::try_join! across libraries, natives, client
JAR, assets, mods, resource packs, shader packs, datapacks. Optional
modpack pre-step runs before user-mod resolution. Full pipeline +
SHA1 verification + per-bucket layout in
installation.md.
For Loader::Forge (modern, 1.13+) and Loader::NeoForge: download
the install_profile.json libraries through the shared library
installer, then run the install processors. Legacy Forge (1.7.10 –
1.12.2) skips processors — the universal JAR ships inside the
installer and is extracted to its Maven path. Per-loader detail in
crates/loaders/docs/loaders/forge.md
and neoforge.md.
The
forgeCargo feature covers both modern and legacy Forge in a single switch — there's no separateforge_legacyfeature.
Variable map → JVM args (defaults if metadata is empty) → critical
JVM injections (-Djava.library.path=..., launcher brand / version,
-cp ...) → game args. Detailed in arguments.md;
the access-token routing (in particular how the keyring feature
keeps the secret out of process memory) is documented there too.
Command::new(java_path)
.args(arguments)
.current_dir(builder.game_dirs())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;Emits LaunchEvent::Launched { version, pid } on success (or
NotLaunched { error } on failure). InstallerError::NoPid is
returned if child.id() is None.
The new GameInstance is registered in the global
InstanceManager keyed by
PID. A dedicated task takes ownership of the Child and:
- streams stdout/stderr line-by-line as
LaunchEvent::ProcessOutput { pid, stream, line }, - waits on the exit code,
- emits
LaunchEvent::ProcessExited { pid, exit_code }, - unregisters the instance from the manager.
Full state-machine + manager internals: instance-lifecycle.md.
| Platform | Java exec | Classpath sep | Process kill | Natives |
|---|---|---|---|---|
| Windows | java.exe |
; |
taskkill /PID {pid} /F |
…-natives-windows.jar |
| Linux | java |
: |
kill -SIGTERM {pid} |
…-natives-linux.jar |
| macOS | java |
: |
kill -SIGTERM {pid} |
…-natives-macos.jar |
-XstartOnFirstThread is auto-injected on macOS (LWJGL / GLFW
requirement).
use lighty_auth::{offline::OfflineAuth, Authenticator};
use lighty_core::AppState;
use lighty_java::JavaDistribution;
use lighty_launch::launch::Launch;
use lighty_launch::InstanceControl;
use lighty_loaders::types::Loader;
use lighty_version::VersionBuilder;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
AppState::init("MyLauncher")?;
let mut instance = VersionBuilder::new(
"fabric-1.21",
Loader::Fabric,
"0.16.9",
"1.21.1",
);
let mut auth = OfflineAuth::new("Player123");
let profile = auth.authenticate(
#[cfg(feature = "events")] None,
).await?;
instance.launch(&profile, JavaDistribution::Temurin)
.with_jvm_options()
.set("Xmx", "4G")
.set("XX:+UseG1GC", "")
.done()
.with_arguments()
.set("width", "1920")
.set("height", "1080")
.done()
.run()
.await?;
if let Some(pid) = instance.get_pid() {
println!("Running with PID {pid}");
}
Ok(())
}pub enum InstallerError {
DownloadFailed(String),
VerificationFailed(String),
ExtractionFailed(String),
InvalidMetadata,
NoPid,
IOError(std::io::Error),
// …
}InstanceError is separate (manager-level), documented in
instance-control.md.
- How to use — short patterns
- Installation — the 8 buckets, SHA1, modpack
- Instance lifecycle — manager + console
- Instance control — PID / close / delete API
- Arguments — placeholders, JVM / game args, token routing
- Events —
LaunchEvent+ModloaderEvent - Exports