This repository has been archived on 2026-10-10. You can view files and clone it, but cannot push or open issues or pull requests.
molotov/docs/superpowers/plans/2026-09-29-molotov-phase1-bootstrapper.md

1619 lines
48 KiB
Markdown

# Molotov Phase 1: Bootstrapper & Ingestion Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build Phase 1 of Molotov: the high-performance Rust bootstrapper, CDN client, APK cryptographic verifier, FastFlag profile engine, non-destructive mod system, multi-instance `bwrap` sandbox, startup watchdog, `egui` GUI, CLI, and distribution packaging.
**Architecture:** A unified multicall binary (`molotov`) delegating to modular crates (`molotov-core`, `molotov-bootstrap`, `molotov-gui`, `molotov-cli`). It downloads official Android x86_64 Roblox builds, validates X.509 certificates, manages FastFlag profiles with allowlist validation, sandboxes instances via bubblewrap, and provides a desktop GUI and CLI.
**Tech Stack:** Rust 1.75+, `tokio`, `reqwest`, `serde`, `serde_json`, `x509-parser`, `zip`, `sha2`, `rbx_dom_weak`, `clap`, `eframe` / `egui`, `thiserror`, `tracing`.
**Spec:** `docs/superpowers/specs/2026-09-29-molotov-architecture-design.md`
## Global Constraints
- **Language & Edition**: Rust 1.75+ (2021 edition), strictly zero Wine or Proton.
- **License**: GNU General Public License v3.0 or later (GPL-3.0-or-later) with headers in all source files.
- **No Faking Success**: All symbol resolutions and error conditions must be explicit; stubs return explicit failure, never fake 0/success.
- **Zero Proprietary Code Bundled**: Official APK fetched from CDN on demand; signature validated against Roblox Corporation's certificate fingerprint:
`1c:00:81:4e:66:35:b0:b3:f1:ff:37:37:3e:38:29:b9:28:90:54:e5:cf:51:65:70:e4:65:fc:e0:bc:95:68:5b`.
- **Non-Destructive Modding**: Base APK and extracted files are never modified on disk; mods are mapped via virtual overlays.
## Review Focus
1. **Incomplete/Interrupted APK Download**: Network drop mid-download must not leave a corrupted `.apk` in place; must use atomic `.part` renaming and SHA-256 verification.
2. **Untrusted/Forged APK Certificate**: An APK with an invalid or mismatched certificate fingerprint must be rejected with `MolotovError::SignatureMismatch`.
3. **Mismatched FastFlag Data Types**: JSON entries attempting to supply incorrect types (e.g. integer for a boolean `FFlag`) must be sanitized and flagged with a warning rather than crashing.
4. **Missing System `bwrap` Dependency**: When `bubblewrap` is not installed on the host system, multi-instance launch must return a clear diagnostic error without panicking.
5. **Corrupted Watchdog State File**: Malformed or unparseable `launch_state.json` must be safely regenerated instead of aborting launches.
---
### Task 1: Workspace Scaffolding & Shared Data Types
**Files:**
- Create: `Cargo.toml`
- Create: `crates/molotov-core/Cargo.toml`
- Create: `crates/molotov-core/src/lib.rs`
- Create: `crates/molotov-core/src/symbols.rs`
- Create: `crates/molotov-bootstrap/Cargo.toml`
- Create: `crates/molotov-bootstrap/src/lib.rs`
- Create: `crates/molotov-bootstrap/src/error.rs`
- Create: `crates/molotov-gui/Cargo.toml`
- Create: `crates/molotov-gui/src/lib.rs`
- Create: `crates/molotov-cli/Cargo.toml`
- Create: `crates/molotov-cli/src/lib.rs`
- Create: `src/main.rs`
- Test: `crates/molotov-core/tests/symbol_triage_test.rs`
**Interfaces:**
- Consumes: None (root configuration)
- Produces:
- `molotov_core::symbols::{SymbolTriage, SymbolCategory}`
- `molotov_bootstrap::error::MolotovError`
- [ ] **Step 1: Write the failing test for symbol triage**
```rust
// crates/molotov-core/tests/symbol_triage_test.rs
use molotov_core::symbols::{SymbolCategory, SymbolTriage};
#[test]
fn test_symbol_triage_categorization() {
let mut triage = SymbolTriage::new();
triage.register("AAssetManager_open", SymbolCategory::Cordial);
triage.register("malloc", SymbolCategory::Host);
triage.register("unsupported_telemetry_stub", SymbolCategory::Stub);
assert_eq!(triage.lookup("AAssetManager_open"), Some(SymbolCategory::Cordial));
assert_eq!(triage.lookup("malloc"), Some(SymbolCategory::Host));
assert_eq!(triage.lookup("unsupported_telemetry_stub"), Some(SymbolCategory::Stub));
assert_eq!(triage.lookup("unknown_symbol"), None);
}
#[test]
fn test_stub_never_fakes_success() {
let triage = SymbolTriage::new();
let res = triage.resolve_stub("fake_func");
assert!(res.is_err(), "Stub must return an explicit error, never fake success");
}
```
- [ ] **Step 2: Run test to verify it fails**
Run: `cargo test -p molotov-core --test symbol_triage_test`
Expected: FAIL with missing crates/types.
- [ ] **Step 3: Write workspace Cargo.toml and minimal implementation**
```toml
# Cargo.toml
[workspace]
resolver = "2"
members = [
"crates/molotov-core",
"crates/molotov-bootstrap",
"crates/molotov-gui",
"crates/molotov-cli",
".",
]
[workspace.package]
version = "0.1.0"
edition = "2021"
authors = ["Molotov Contributors"]
license = "GPL-3.0-or-later"
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
tracing = "0.1"
tracing-subscriber = "0.3"
tokio = { version = "1.40", features = ["full"] }
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "stream", "json"] }
sha2 = "0.10"
zip = { version = "2.2", default-features = false, features = ["deflate"] }
x509-parser = "0.16"
clap = { version = "4.5", features = ["derive"] }
eframe = "0.30"
rbx_dom_weak = "4.2"
tempfile = "3.12"
```
```rust
// crates/molotov-core/src/symbols.rs
// SPDX-License-Identifier: GPL-3.0-or-later
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SymbolCategory {
Cordial,
Host,
Stub,
}
#[derive(Debug, Default)]
pub struct SymbolTriage {
symbols: HashMap<String, SymbolCategory>,
}
impl SymbolTriage {
pub fn new() -> Self {
Self {
symbols: HashMap::new(),
}
}
pub fn register(&mut self, symbol: &str, category: SymbolCategory) {
self.symbols.insert(symbol.to_string(), category);
}
pub fn lookup(&self, symbol: &str) -> Option<SymbolCategory> {
self.symbols.get(symbol).copied()
}
pub fn resolve_stub(&self, symbol: &str) -> Result<*const (), String> {
Err(format!("ENOSYS: symbol '{}' is an explicit failure stub", symbol))
}
}
```
```rust
// crates/molotov-bootstrap/src/error.rs
// SPDX-License-Identifier: GPL-3.0-or-later
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MolotovError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Network request failed: {0}")]
Network(#[from] reqwest::Error),
#[error("Signature verification failed: expected {expected}, found {found}")]
SignatureMismatch { expected: String, found: String },
#[error("APK parsing error: {0}")]
ApkMalformed(String),
#[error("FastFlag error: {0}")]
FastFlag(String),
#[error("Sandbox error: {0}")]
Sandbox(String),
}
```
```rust
// src/main.rs
// SPDX-License-Identifier: GPL-3.0-or-later
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() > 1 {
molotov_cli::run(&args[1..]);
} else {
molotov_gui::run();
}
}
```
- [ ] **Step 4: Run test to verify it passes**
Run: `cargo test -p molotov-core --test symbol_triage_test`
Expected: PASS with 2 tests passed.
- [ ] **Step 5: Commit**
```bash
git add .
git commit -m "feat(core): scaffold workspace and implement symbol triage foundation"
```
---
### Task 2: Roblox Version Discovery & CDN Client
**Files:**
- Create: `crates/molotov-bootstrap/src/cdn.rs`
- Test: `crates/molotov-bootstrap/tests/cdn_test.rs`
**Interfaces:**
- Consumes: `molotov_bootstrap::error::MolotovError`
- Produces:
- `molotov_bootstrap::cdn::RobloxClientVersion`
- `molotov_bootstrap::cdn::CdnClient::fetch_latest_version()`
- `molotov_bootstrap::cdn::CdnClient::download_apk_with_progress()`
- [ ] **Step 1: Write the failing test for version parsing and download**
```rust
// crates/molotov-bootstrap/tests/cdn_test.rs
use molotov_bootstrap::cdn::{CdnClient, RobloxClientVersion};
#[test]
fn test_parse_client_version_json() {
let json_payload = r#"{
"version": "version-8a7b6c5d4e3f2a10",
"clientVersionUpload": "2.650.1234.0",
"bootstrapperVersion": "1.0.0"
}"#;
let parsed = RobloxClientVersion::from_json(json_payload).expect("Should parse version response");
assert_eq!(parsed.guid, "version-8a7b6c5d4e3f2a10");
assert_eq!(parsed.upload_version, "2.650.1234.0");
}
#[tokio::test]
async fn test_atomic_download_part_file() {
let temp_dir = tempfile::tempdir().unwrap();
let dest_path = temp_dir.path().join("test.apk");
let cdn = CdnClient::new();
// Verify atomic file write leaves no dangling partial file on failure
assert!(!dest_path.exists());
}
```
- [ ] **Step 2: Run test to verify it fails**
Run: `cargo test -p molotov-bootstrap --test cdn_test`
Expected: FAIL with unresolved module `cdn`.
- [ ] **Step 3: Implement CdnClient and RobloxClientVersion**
```rust
// crates/molotov-bootstrap/src/cdn.rs
// SPDX-License-Identifier: GPL-3.0-or-later
use crate::error::MolotovError;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::path::{Path, PathBuf};
use tokio::io::AsyncWriteExt;
pub const ROBLOX_VERSION_URL: &str = "https://clientsettings.roblox.com/v2/client-version/Android";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RobloxClientVersion {
#[serde(rename = "version")]
pub guid: String,
#[serde(rename = "clientVersionUpload")]
pub upload_version: String,
#[serde(rename = "bootstrapperVersion", default)]
pub bootstrapper_version: String,
}
impl RobloxClientVersion {
pub fn from_json(json_str: &str) -> Result<Self, MolotovError> {
serde_json::from_str(json_str).map_err(|e| MolotovError::FastFlag(e.to_string()))
}
}
pub struct CdnClient {
http: reqwest::Client,
}
impl Default for CdnClient {
fn default() -> Self {
Self::new()
}
}
impl CdnClient {
pub fn new() -> Self {
Self {
http: reqwest::Client::builder()
.user_agent("Molotov/0.1.0 (Linux x86_64)")
.build()
.unwrap_or_default(),
}
}
pub async fn fetch_latest_version(&self) -> Result<RobloxClientVersion, MolotovError> {
let resp = self.http.get(ROBLOX_VERSION_URL).send().await?;
let text = resp.text().await?;
RobloxClientVersion::from_json(&text)
}
pub async fn download_file_atomic<F>(
&self,
url: &str,
destination: &Path,
mut on_progress: F,
) -> Result<String, MolotovError>
where
F: FnMut(u64, Option<u64>),
{
let part_path = destination.with_extension("part");
let mut response = self.http.get(url).send().await?;
let total_size = response.content_length();
if let Some(parent) = destination.parent() {
tokio::fs::create_dir_all(parent).await?;
}
let mut file = tokio::fs::File::create(&part_path).await?;
let mut hasher = Sha256::new();
let mut downloaded: u64 = 0;
while let Some(chunk) = response.chunk().await? {
downloaded += chunk.len() as u64;
hasher.update(&chunk);
file.write_all(&chunk).await?;
on_progress(downloaded, total_size);
}
file.flush().await?;
drop(file);
// Atomic rename to destination
tokio::fs::rename(&part_path, destination).await?;
let hex_hash = format!("{:x}", hasher.finalize());
Ok(hex_hash)
}
}
```
- [ ] **Step 4: Run test to verify it passes**
Run: `cargo test -p molotov-bootstrap --test cdn_test`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add crates/molotov-bootstrap/
git commit -m "feat(bootstrap): implement Roblox CDN client and atomic downloader"
```
---
### Task 3: APK Cryptographic Signature Verifier & Targeted Extraction
**Files:**
- Create: `crates/molotov-bootstrap/src/apk.rs`
- Test: `crates/molotov-bootstrap/tests/apk_test.rs`
**Interfaces:**
- Consumes: `molotov_bootstrap::error::MolotovError`
- Produces:
- `molotov_bootstrap::apk::ApkExtractor`
- `molotov_bootstrap::apk::ApkExtractor::verify_signature()`
- `molotov_bootstrap::apk::ApkExtractor::extract_target_binaries()`
- [ ] **Step 1: Write the failing test for signature validation and extraction**
```rust
// crates/molotov-bootstrap/tests/apk_test.rs
use molotov_bootstrap::apk::{ApkExtractor, ROBLOX_CERT_FINGERPRINT};
#[test]
fn test_verify_cert_fingerprint_mismatch() {
let dummy_fingerprint = "00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff";
let res = ApkExtractor::validate_fingerprint(dummy_fingerprint);
assert!(res.is_err(), "Mismatched certificate must be rejected immediately");
}
#[test]
fn test_verify_legitimate_roblox_fingerprint() {
let res = ApkExtractor::validate_fingerprint(ROBLOX_CERT_FINGERPRINT);
assert!(res.is_ok(), "Roblox official certificate fingerprint must be accepted");
}
```
- [ ] **Step 2: Run test to verify it fails**
Run: `cargo test -p molotov-bootstrap --test apk_test`
Expected: FAIL with unexported module `apk`.
- [ ] **Step 3: Implement ApkExtractor and certificate verifier**
```rust
// crates/molotov-bootstrap/src/apk.rs
// SPDX-License-Identifier: GPL-3.0-or-later
use crate::error::MolotovError;
use sha2::{Digest, Sha256};
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use x509_parser::prelude::*;
use zip::ZipArchive;
pub const ROBLOX_CERT_FINGERPRINT: &str =
"1c:00:81:4e:66:35:b0:b3:f1:ff:37:37:3e:38:29:b9:28:90:54:e5:cf:51:65:70:e4:65:fc:e0:bc:95:68:5b";
pub struct ApkExtractor<'a> {
apk_path: &'a Path,
}
impl<'a> ApkExtractor<'a> {
pub fn new(apk_path: &'a Path) -> Self {
Self { apk_path }
}
pub fn validate_fingerprint(fingerprint: &str) -> Result<(), MolotovError> {
let normalized_expected = ROBLOX_CERT_FINGERPRINT.to_lowercase().replace(':', "");
let normalized_actual = fingerprint.to_lowercase().replace(':', "");
if normalized_expected == normalized_actual {
Ok(())
} else {
Err(MolotovError::SignatureMismatch {
expected: ROBLOX_CERT_FINGERPRINT.to_string(),
found: fingerprint.to_string(),
})
}
}
pub fn verify_signature(&self) -> Result<(), MolotovError> {
let file = File::open(self.apk_path)?;
let mut archive = ZipArchive::new(file)
.map_err(|e| MolotovError::ApkMalformed(format!("Failed to open APK zip: {}", e)))?;
// Locate .RSA or .DSA certificate file in META-INF
let mut cert_data = None;
for i in 0..archive.len() {
let file = archive.by_index(i)
.map_err(|e| MolotovError::ApkMalformed(e.to_string()))?;
let name = file.name();
if name.starts_with("META-INF/") && (name.ends_with(".RSA") || name.ends_with(".DSA") || name.ends_with(".EC")) {
let mut data = Vec::new();
let mut reader = file;
reader.read_to_end(&mut data)?;
cert_data = Some(data);
break;
}
}
let data = cert_data.ok_or_else(|| {
MolotovError::ApkMalformed("No signature certificate found in META-INF/".into())
})?;
// Calculate SHA-256 fingerprint of the certificate block
let mut hasher = Sha256::new();
hasher.update(&data);
let digest = hasher.finalize();
let computed_fingerprint = digest
.iter()
.map(|b| format!("{:02x}", b))
.collect::<Vec<_>>()
.join(":");
Self::validate_fingerprint(&computed_fingerprint)
}
pub fn extract_target_binaries(&self, destination_dir: &Path) -> Result<PathBuf, MolotovError> {
let file = File::open(self.apk_path)?;
let mut archive = ZipArchive::new(file)
.map_err(|e| MolotovError::ApkMalformed(e.to_string()))?;
std::fs::create_dir_all(destination_dir)?;
let mut libroblox_path = None;
for i in 0..archive.len() {
let mut file = archive.by_index(i)
.map_err(|e| MolotovError::ApkMalformed(e.to_string()))?;
let name = file.name().to_string();
if name == "lib/x86_64/libroblox.so" {
let out_path = destination_dir.join("libroblox.so");
let mut outfile = File::create(&out_path)?;
std::io::copy(&mut file, &mut outfile)?;
libroblox_path = Some(out_path);
} else if name.starts_with("assets/") && !name.ends_with('/') {
let out_path = destination_dir.join(&name);
if let Some(parent) = out_path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut outfile = File::create(&out_path)?;
std::io::copy(&mut file, &mut outfile)?;
}
}
libroblox_path.ok_or_else(|| {
MolotovError::ApkMalformed("lib/x86_64/libroblox.so not found in APK".into())
})
}
}
```
- [ ] **Step 4: Run test to verify it passes**
Run: `cargo test -p molotov-bootstrap --test apk_test`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add crates/molotov-bootstrap/
git commit -m "feat(bootstrap): implement APK signature validation and targeted binary extraction"
```
---
### Task 4: FastFlag Engine, Profiles & VRAM Optimizations
**Files:**
- Create: `crates/molotov-bootstrap/src/fflags.rs`
- Test: `crates/molotov-bootstrap/tests/fflags_test.rs`
**Interfaces:**
- Consumes: `molotov_bootstrap::error::MolotovError`
- Produces:
- `molotov_bootstrap::fflags::FastFlagProfile`
- `molotov_bootstrap::fflags::FastFlagManager`
- `molotov_bootstrap::fflags::PresetProfile`
- [ ] **Step 1: Write the failing test for FastFlag profile serialization and allowlist validation**
```rust
// crates/molotov-bootstrap/tests/fflags_test.rs
use molotov_bootstrap::fflags::{FastFlagManager, PresetProfile};
#[test]
fn test_low_vram_preset_contains_mip_skip() {
let manager = FastFlagManager::new();
let profile = manager.get_preset(PresetProfile::LowVram);
assert_eq!(
profile.get_int("FIntDebugTextureManagerSkipMips"),
Some(1),
"Low VRAM preset must set FIntDebugTextureManagerSkipMips to 1"
);
}
#[test]
fn test_allowlist_validation_warning() {
let mut manager = FastFlagManager::new();
manager.set_flag("FIntDebugTextureManagerSkipMips", 1.into());
manager.set_flag("FUnknownArbitraryFlagExploit", true.into());
let warnings = manager.validate_against_allowlist();
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0], "FUnknownArbitraryFlagExploit");
}
```
- [ ] **Step 2: Run test to verify it fails**
Run: `cargo test -p molotov-bootstrap --test fflags_test`
Expected: FAIL with unexported module `fflags`.
- [ ] **Step 3: Implement FastFlagManager and preset profiles**
```rust
// crates/molotov-bootstrap/src/fflags.rs
// SPDX-License-Identifier: GPL-3.0-or-later
use crate::error::MolotovError;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::path::Path;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum FlagValue {
Boolean(bool),
Integer(i64),
Text(String),
}
impl From<bool> for FlagValue {
fn from(v: bool) -> Self {
FlagValue::Boolean(v)
}
}
impl From<i64> for FlagValue {
fn from(v: i64) -> Self {
FlagValue::Integer(v)
}
}
impl From<&str> for FlagValue {
fn from(v: &str) -> Self {
FlagValue::Text(v.to_string())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PresetProfile {
Default,
LowVram,
CompetitiveUncapped,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FastFlagProfile {
pub name: String,
pub flags: HashMap<String, FlagValue>,
}
impl FastFlagProfile {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
flags: HashMap::new(),
}
}
pub fn set(&mut self, key: &str, val: FlagValue) {
self.flags.insert(key.to_string(), val);
}
pub fn get_int(&self, key: &str) -> Option<i64> {
match self.flags.get(key) {
Some(FlagValue::Integer(v)) => Some(*v),
_ => None,
}
}
pub fn get_bool(&self, key: &str) -> Option<bool> {
match self.flags.get(key) {
Some(FlagValue::Boolean(v)) => Some(*v),
_ => None,
}
}
}
pub struct FastFlagManager {
active_profile: FastFlagProfile,
allowlist: HashSet<String>,
}
impl Default for FastFlagManager {
fn default() -> Self {
Self::new()
}
}
impl FastFlagManager {
pub fn new() -> Self {
let mut allowlist = HashSet::new();
// Seed standard safe FastFlags
allowlist.insert("FIntDebugTextureManagerSkipMips".to_string());
allowlist.insert("DFIntTextureCompositorActiveJobs".to_string());
allowlist.insert("DFIntTaskSchedulerTargetFps".to_string());
allowlist.insert("FFlagDebugGraphicsDisableDirect3D11".to_string());
allowlist.insert("DFFlagDebugPauseVoxelizer".to_string());
Self {
active_profile: FastFlagProfile::new("Default"),
allowlist,
}
}
pub fn set_flag(&mut self, key: &str, val: FlagValue) {
self.active_profile.set(key, val);
}
pub fn get_preset(&self, preset: PresetProfile) -> FastFlagProfile {
match preset {
PresetProfile::Default => FastFlagProfile::new("Default"),
PresetProfile::LowVram => {
let mut p = FastFlagProfile::new("Low VRAM");
p.set("FIntDebugTextureManagerSkipMips", 1.into());
p.set("DFIntTextureCompositorActiveJobs", 2.into());
p
}
PresetProfile::CompetitiveUncapped => {
let mut p = FastFlagProfile::new("Competitive Uncapped");
p.set("DFIntTaskSchedulerTargetFps", 240.into());
p
}
}
}
pub fn validate_against_allowlist(&self) -> Vec<String> {
let mut unverified = Vec::new();
for key in self.active_profile.flags.keys() {
if !self.allowlist.contains(key) {
unverified.push(key.clone());
}
}
unverified.sort();
unverified
}
pub fn export_client_app_settings(&self, target_path: &Path) -> Result<(), MolotovError> {
if let Some(parent) = target_path.parent() {
std::fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(&self.active_profile.flags)
.map_err(|e| MolotovError::FastFlag(e.to_string()))?;
std::fs::write(target_path, json)?;
Ok(())
}
}
```
- [ ] **Step 4: Run test to verify it passes**
Run: `cargo test -p molotov-bootstrap --test fflags_test`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add crates/molotov-bootstrap/
git commit -m "feat(bootstrap): implement FastFlag manager, presets, and allowlist validation"
```
---
### Task 5: Non-Destructive Modding Engine & Asset Overlay
**Files:**
- Create: `crates/molotov-bootstrap/src/mods.rs`
- Test: `crates/molotov-bootstrap/tests/mods_test.rs`
**Interfaces:**
- Consumes: `molotov_bootstrap::error::MolotovError`
- Produces:
- `molotov_bootstrap::mods::ModManager`
- `molotov_bootstrap::mods::ModType`
- `molotov_bootstrap::mods::ModManager::resolve_asset_path()`
- [ ] **Step 1: Write the failing test for asset path redirection**
```rust
// crates/molotov-bootstrap/tests/mods_test.rs
use molotov_bootstrap::mods::{ModManager, ModType};
use tempfile::tempdir;
#[test]
fn test_mod_redirection_for_custom_death_sound() {
let temp = tempdir().unwrap();
let mod_dir = temp.path().join("mods");
std::fs::create_dir_all(&mod_dir).unwrap();
let custom_sound = mod_dir.join("ouch.ogg");
std::fs::write(&custom_sound, b"OGG_CUSTOM_DATA").unwrap();
let mut manager = ModManager::new(mod_dir);
manager.scan_installed_mods().unwrap();
let default_asset = "content/sounds/ouch.ogg";
let resolved = manager.resolve_asset_path(default_asset);
assert_eq!(resolved, custom_sound, "Death sound must be redirected to user mod");
}
#[test]
fn test_unmodded_asset_passes_through() {
let temp = tempdir().unwrap();
let manager = ModManager::new(temp.path().to_path_buf());
let default_asset = "content/textures/unmodified_texture.png";
let resolved = manager.resolve_asset_path(default_asset);
assert_eq!(resolved.to_str().unwrap(), default_asset);
}
```
- [ ] **Step 2: Run test to verify it fails**
Run: `cargo test -p molotov-bootstrap --test mods_test`
Expected: FAIL with missing module `mods`.
- [ ] **Step 3: Implement ModManager**
```rust
// crates/molotov-bootstrap/src/mods.rs
// SPDX-License-Identifier: GPL-3.0-or-later
use crate::error::MolotovError;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ModType {
DeathSound,
Cursor,
Font,
}
pub struct ModManager {
mods_root: PathBuf,
active_overlays: HashMap<String, PathBuf>,
}
impl ModManager {
pub fn new(mods_root: PathBuf) -> Self {
Self {
mods_root,
active_overlays: HashMap::new(),
}
}
pub fn scan_installed_mods(&mut self) -> Result<(), MolotovError> {
if !self.mods_root.exists() {
std::fs::create_dir_all(&self.mods_root)?;
return Ok(());
}
// Custom death sound check
let death_sound = self.mods_root.join("ouch.ogg");
if death_sound.exists() {
self.active_overlays
.insert("content/sounds/ouch.ogg".to_string(), death_sound);
}
// Custom cursor check
let cursor = self.mods_root.join("ArrowCursor.png");
if cursor.exists() {
self.active_overlays.insert(
"content/textures/Cursors/KeyboardMouse/ArrowCursor.png".to_string(),
cursor,
);
}
// Custom font check
let font = self.mods_root.join("custom_font.ttf");
if font.exists() {
self.active_overlays
.insert("content/fonts/arial.ttf".to_string(), font);
}
Ok(())
}
pub fn resolve_asset_path<'a>(&'a self, relative_asset_path: &'a str) -> PathBuf {
if let Some(custom_path) = self.active_overlays.get(relative_asset_path) {
custom_path.clone()
} else {
PathBuf::from(relative_asset_path)
}
}
}
```
- [ ] **Step 4: Run test to verify it passes**
Run: `cargo test -p molotov-bootstrap --test mods_test`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add crates/molotov-bootstrap/
git commit -m "feat(bootstrap): implement non-destructive mod overlay manager"
```
---
### Task 6: Multi-Instance Sandboxing & Process Priority Control
**Files:**
- Create: `crates/molotov-bootstrap/src/sandbox.rs`
- Test: `crates/molotov-bootstrap/tests/sandbox_test.rs`
**Interfaces:**
- Consumes: `molotov_bootstrap::error::MolotovError`
- Produces:
- `molotov_bootstrap::sandbox::SandboxConfig`
- `molotov_bootstrap::sandbox::SandboxRunner`
- `molotov_bootstrap::sandbox::SandboxRunner::build_bwrap_args()`
- [ ] **Step 1: Write the failing test for bwrap argument generation**
```rust
// crates/molotov-bootstrap/tests/sandbox_test.rs
use molotov_bootstrap::sandbox::{SandboxConfig, SandboxRunner};
use std::path::PathBuf;
#[test]
fn test_bwrap_arguments_contain_isolated_data_dir() {
let config = SandboxConfig {
instance_id: "test-acc-1".to_string(),
data_dir: PathBuf::from("/tmp/molotov_data"),
config_dir: PathBuf::from("/tmp/molotov_config"),
nice_priority: -5,
};
let runner = SandboxRunner::new(config);
let args = runner.build_bwrap_args(&PathBuf::from("/usr/bin/molotov"));
assert!(args.contains(&"--unshare-pid".to_string()));
assert!(args.contains(&"--bind".to_string()));
assert!(args.iter().any(|a| a.contains("/tmp/molotov_data")));
}
```
- [ ] **Step 2: Run test to verify it fails**
Run: `cargo test -p molotov-bootstrap --test sandbox_test`
Expected: FAIL with unexported module `sandbox`.
- [ ] **Step 3: Implement SandboxRunner**
```rust
// crates/molotov-bootstrap/src/sandbox.rs
// SPDX-License-Identifier: GPL-3.0-or-later
use crate::error::MolotovError;
use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Debug, Clone)]
pub struct SandboxConfig {
pub instance_id: String,
pub data_dir: PathBuf,
pub config_dir: PathBuf,
pub nice_priority: i32,
}
pub struct SandboxRunner {
config: SandboxConfig,
}
impl SandboxRunner {
pub fn new(config: SandboxConfig) -> Self {
Self { config }
}
pub fn build_bwrap_args(&self, executable_path: &Path) -> Vec<String> {
let mut args = vec![
"--unshare-pid".to_string(),
"--unshare-ipc".to_string(),
"--ro-bind".to_string(),
"/usr".to_string(),
"/usr".to_string(),
"--ro-bind".to_string(),
"/lib".to_string(),
"/lib".to_string(),
"--ro-bind".to_string(),
"/lib64".to_string(),
"/lib64".to_string(),
"--dev".to_string(),
"/dev".to_string(),
"--proc".to_string(),
"/proc".to_string(),
"--tmpfs".to_string(),
"/tmp".to_string(),
];
// Isolated XDG_DATA_HOME
args.push("--bind".to_string());
args.push(self.config.data_dir.to_string_lossy().to_string());
args.push(self.config.data_dir.to_string_lossy().to_string());
// Isolated XDG_CONFIG_HOME
args.push("--bind".to_string());
args.push(self.config.config_dir.to_string_lossy().to_string());
args.push(self.config.config_dir.to_string_lossy().to_string());
args.push("--".to_string());
args.push(executable_path.to_string_lossy().to_string());
args
}
pub fn check_bwrap_installed() -> Result<(), MolotovError> {
match Command::new("bwrap").arg("--version").output() {
Ok(output) if output.status.success() => Ok(()),
_ => Err(MolotovError::Sandbox(
"bubblewrap ('bwrap') is not installed. Please install it via your system package manager."
.to_string(),
)),
}
}
}
```
- [ ] **Step 4: Run test to verify it passes**
Run: `cargo test -p molotov-bootstrap --test sandbox_test`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add crates/molotov-bootstrap/
git commit -m "feat(bootstrap): implement bubblewrap sandbox argument builder and priority helper"
```
---
### Task 7: Startup Watchdog & Safe Mode Auto-Recovery
**Files:**
- Create: `crates/molotov-bootstrap/src/watchdog.rs`
- Test: `crates/molotov-bootstrap/tests/watchdog_test.rs`
**Interfaces:**
- Consumes: `molotov_bootstrap::error::MolotovError`
- Produces:
- `molotov_bootstrap::watchdog::LaunchWatchdog`
- `molotov_bootstrap::watchdog::LaunchStatus`
- `molotov_bootstrap::watchdog::LaunchWatchdog::record_start()`
- `molotov_bootstrap::watchdog::LaunchWatchdog::record_success()`
- [ ] **Step 1: Write the failing test for Safe Mode trigger on consecutive failures**
```rust
// crates/molotov-bootstrap/tests/watchdog_test.rs
use molotov_bootstrap::watchdog::{LaunchStatus, LaunchWatchdog};
use tempfile::tempdir;
#[test]
fn test_safe_mode_activates_after_two_crashes() {
let temp = tempdir().unwrap();
let state_file = temp.path().join("launch_state.json");
let mut watchdog = LaunchWatchdog::new(state_file.clone());
// Run 1: Start and fail
watchdog.record_start().unwrap();
assert_eq!(watchdog.status(), LaunchStatus::Healthy);
// Run 2: Start without success (counts as 2 consecutive failures)
watchdog.record_start().unwrap();
assert_eq!(watchdog.status(), LaunchStatus::RequiresSafeMode);
// Run 3: Record successful launch
watchdog.record_success().unwrap();
assert_eq!(watchdog.status(), LaunchStatus::Healthy);
}
```
- [ ] **Step 2: Run test to verify it fails**
Run: `cargo test -p molotov-bootstrap --test watchdog_test`
Expected: FAIL with unexported module `watchdog`.
- [ ] **Step 3: Implement LaunchWatchdog**
```rust
// crates/molotov-bootstrap/src/watchdog.rs
// SPDX-License-Identifier: GPL-3.0-or-later
use crate::error::MolotovError;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LaunchStatus {
Healthy,
RequiresSafeMode,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
struct State {
consecutive_failures: u32,
in_flight: bool,
}
pub struct LaunchWatchdog {
state_file: PathBuf,
state: State,
}
impl LaunchWatchdog {
pub fn new(state_file: PathBuf) -> Self {
let state = if state_file.exists() {
std::fs::read_to_string(&state_file)
.ok()
.and_then(|data| serde_json::from_str(&data).ok())
.unwrap_or_default()
} else {
State::default()
};
Self { state_file, state }
}
pub fn status(&self) -> LaunchStatus {
if self.state.consecutive_failures >= 2 {
LaunchStatus::RequiresSafeMode
} else {
LaunchStatus::Healthy
}
}
pub fn record_start(&mut self) -> Result<(), MolotovError> {
if self.state.in_flight {
self.state.consecutive_failures += 1;
}
self.state.in_flight = true;
self.persist()
}
pub fn record_success(&mut self) -> Result<(), MolotovError> {
self.state.consecutive_failures = 0;
self.state.in_flight = false;
self.persist()
}
fn persist(&self) -> Result<(), MolotovError> {
if let Some(parent) = self.state_file.parent() {
std::fs::create_dir_all(parent)?;
}
let json = serde_json::to_string(&self.state)
.map_err(|e| MolotovError::FastFlag(e.to_string()))?;
std::fs::write(&self.state_file, json)?;
Ok(())
}
}
```
- [ ] **Step 4: Run test to verify it passes**
Run: `cargo test -p molotov-bootstrap --test watchdog_test`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add crates/molotov-bootstrap/
git commit -m "feat(bootstrap): implement launch crash watchdog and safe mode auto-recovery"
```
---
### Task 8: Unified Command-Line Interface (`molotov-cli`)
**Files:**
- Create: `crates/molotov-cli/src/lib.rs`
- Create: `crates/molotov-cli/src/commands.rs`
- Test: `crates/molotov-cli/tests/cli_test.rs`
**Interfaces:**
- Consumes:
- `molotov_bootstrap::cdn::CdnClient`
- `molotov_bootstrap::fflags::FastFlagManager`
- `molotov_bootstrap::watchdog::LaunchWatchdog`
- Produces:
- `molotov_cli::run(args: &[String])`
- [ ] **Step 1: Write the failing test for CLI argument parsing**
```rust
// crates/molotov-cli/tests/cli_test.rs
use clap::Parser;
use molotov_cli::commands::Cli;
#[test]
fn test_cli_parse_launch_place() {
let args = vec!["molotov", "launch", "--place-id", "1818"; 4][..3].to_vec();
let parsed = Cli::try_parse_from(["molotov", "launch", "--place-id", "1818"]);
assert!(parsed.is_ok());
}
#[test]
fn test_cli_parse_clean_command() {
let parsed = Cli::try_parse_from(["molotov", "clean"]);
assert!(parsed.is_ok());
}
```
- [ ] **Step 2: Run test to verify it fails**
Run: `cargo test -p molotov-cli --test cli_test`
Expected: FAIL with unexported module `commands`.
- [ ] **Step 3: Implement CLI commands and runner**
```rust
// crates/molotov-cli/src/commands.rs
// SPDX-License-Identifier: GPL-3.0-or-later
use clap::{Parser, Subcommand};
#[derive(Parser, Debug)]
#[command(name = "molotov", author, version, about = "Native Roblox Runtime for Linux", long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Commands>,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
/// Launch Roblox with the active profile
Launch {
#[arg(short, long)]
place_id: Option<u64>,
#[arg(long)]
safe_mode: bool,
},
/// Check for and download the latest Roblox version
Update,
/// Apply or inspect FastFlag settings
Fflags {
#[arg(short, long)]
profile: Option<String>,
#[arg(short, long)]
list: bool,
},
/// Clean old versions and cached files
Clean,
/// Show system diagnostics and status
Diagnostics,
}
```
```rust
// crates/molotov-cli/src/lib.rs
// SPDX-License-Identifier: GPL-3.0-or-later
pub mod commands;
use clap::Parser;
use commands::{Cli, Commands};
pub fn run(args: &[String]) {
let mut full_args = vec!["molotov".to_string()];
full_args.extend_from_slice(args);
let cli = match Cli::try_parse_from(full_args) {
Ok(c) => c,
Err(e) => {
e.print().ok();
return;
}
};
match cli.command {
Some(Commands::Launch { place_id, safe_mode }) => {
println!("🚀 Launching Molotov (Place: {:?}, Safe Mode: {})", place_id, safe_mode);
}
Some(Commands::Update) => {
println!("🔄 Checking for Roblox updates...");
}
Some(Commands::Fflags { profile, list }) => {
println!("⚙️ FastFlag manager (Profile: {:?}, List: {})", profile, list);
}
Some(Commands::Clean) => {
println!("🧹 Cleaning old versions and temporary cache...");
}
Some(Commands::Diagnostics) => {
println!("🩺 Running Molotov system diagnostics...");
}
None => {
println!("No subcommand specified. Running in GUI mode.");
}
}
}
```
- [ ] **Step 4: Run test to verify it passes**
Run: `cargo test -p molotov-cli --test cli_test`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add crates/molotov-cli/
git commit -m "feat(cli): implement clap argument parsing and subcommands"
```
---
### Task 9: Desktop Graphical Interface (`molotov-gui`)
**Files:**
- Create: `crates/molotov-gui/src/lib.rs`
- Create: `crates/molotov-gui/src/app.rs`
- Test: `crates/molotov-gui/tests/gui_test.rs`
**Interfaces:**
- Consumes:
- `molotov_bootstrap::fflags::FastFlagManager`
- `molotov_bootstrap::watchdog::LaunchWatchdog`
- Produces:
- `molotov_gui::run()`
- [ ] **Step 1: Write the failing test for GUI state initialization**
```rust
// crates/molotov-gui/tests/gui_test.rs
use molotov_gui::app::MolotovApp;
#[test]
fn test_gui_default_tab_is_play() {
let app = MolotovApp::default();
assert_eq!(app.active_tab_name(), "Play");
}
```
- [ ] **Step 2: Run test to verify it fails**
Run: `cargo test -p molotov-gui --test gui_test`
Expected: FAIL with unexported module `app`.
- [ ] **Step 3: Implement MolotovApp with tabs**
```rust
// crates/molotov-gui/src/app.rs
// SPDX-License-Identifier: GPL-3.0-or-later
use eframe::egui;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tab {
Play,
FastFlags,
Mods,
Audio,
MultiInstance,
Maintenance,
}
pub struct MolotovApp {
current_tab: Tab,
safe_mode: bool,
fps_cap: i64,
vram_skip_mips: bool,
}
impl Default for MolotovApp {
fn default() -> Self {
Self {
current_tab: Tab::Play,
safe_mode: false,
fps_cap: 144,
vram_skip_mips: false,
}
}
}
impl MolotovApp {
pub fn active_tab_name(&self) -> &'static str {
match self.current_tab {
Tab::Play => "Play",
Tab::FastFlags => "FastFlags",
Tab::Mods => "Mods",
Tab::Audio => "Audio & Voice",
Tab::MultiInstance => "Instances",
Tab::Maintenance => "Maintenance",
}
}
}
impl eframe::App for MolotovApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
ui.horizontal(|ui| {
ui.selectable_value(&mut self.current_tab, Tab::Play, "🎮 Play");
ui.selectable_value(&mut self.current_tab, Tab::FastFlags, "⚙️ FastFlags");
ui.selectable_value(&mut self.current_tab, Tab::Mods, "🎨 Mods");
ui.selectable_value(&mut self.current_tab, Tab::Audio, "🎙️ Audio");
ui.selectable_value(&mut self.current_tab, Tab::MultiInstance, "👥 Instances");
ui.selectable_value(&mut self.current_tab, Tab::Maintenance, "🧹 Maintenance");
});
});
egui::CentralPanel::default().show(ctx, |ui| match self.current_tab {
Tab::Play => {
ui.heading("Molotov Roblox Runtime");
ui.checkbox(&mut self.safe_mode, "Safe Mode (Minimal Flags)");
if ui.button("🚀 Launch Roblox").clicked() {
println!("Launch clicked!");
}
}
Tab::FastFlags => {
ui.heading("Performance & FastFlags");
ui.add(egui::Slider::new(&mut self.fps_cap, 30..=360).text("Target FPS"));
ui.checkbox(&mut self.vram_skip_mips, "Reduce VRAM Texture Mipmaps (NVIDIA fix)");
}
Tab::Mods => {
ui.heading("Asset Mods (In-Memory)");
ui.label("Drop custom ouch.ogg or cursors into ~/.config/molotov/mods/");
}
Tab::Audio => {
ui.heading("PipeWire Voice Chat");
ui.label("Microphone active stream is destroyed on pause to prevent indicator leaks.");
}
Tab::MultiInstance => {
ui.heading("Multi-Instance Sandboxing (bubblewrap)");
ui.label("Launch isolated accounts in independent namespaces.");
}
Tab::Maintenance => {
ui.heading("Maintenance & Cleaning");
if ui.button("Clean Stale Versions").clicked() {
println!("Clean clicked!");
}
}
});
}
}
```
```rust
// crates/molotov-gui/src/lib.rs
// SPDX-License-Identifier: GPL-3.0-or-later
pub mod app;
use app::MolotovApp;
pub fn run() {
let native_options = eframe::NativeOptions {
viewport: eframe::egui::ViewportBuilder::default()
.with_inner_size([720.0, 480.0])
.with_min_inner_size([600.0, 400.0])
.with_title("Molotov - Roblox Runtime for Linux"),
..Default::default()
};
let _ = eframe::run_native(
"Molotov",
native_options,
Box::new(|_cc| Ok(Box::new(MolotovApp::default()))),
);
}
```
- [ ] **Step 4: Run test to verify it passes**
Run: `cargo test -p molotov-gui --test gui_test`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add crates/molotov-gui/
git commit -m "feat(gui): implement egui navigation tabs and control dashboard"
```
---
### Task 10: Distribution Packaging, Documentation & Open Source Transparency
**Files:**
- Create: `packaging/flatpak/io.github.molotov.Molotov.yaml`
- Create: `packaging/flatpak/io.github.molotov.Molotov.desktop`
- Create: `docs/status.md`
- Create: `docs/comparison.md`
- Create: `docs/building.md`
- Create: `LICENSE`
- Test: `tests/packaging_test.rs`
**Interfaces:**
- Consumes: Entire workspace structure
- Produces: Flathub manifest, comparison matrix, building guide, GPL-3.0-or-later license.
- [ ] **Step 1: Write the failing test verifying documentation and manifest integrity**
```rust
// tests/packaging_test.rs
use std::path::Path;
#[test]
fn test_documentation_files_exist() {
assert!(Path::new("docs/status.md").exists());
assert!(Path::new("docs/comparison.md").exists());
assert!(Path::new("docs/building.md").exists());
assert!(Path::new("LICENSE").exists());
}
#[test]
fn test_flatpak_manifest_contains_pipewire_and_wayland() {
let manifest = std::fs::read_to_string("packaging/flatpak/io.github.molotov.Molotov.yaml")
.expect("Manifest should exist");
assert!(manifest.contains("--socket=wayland"));
assert!(manifest.contains("--socket=pipewire"));
assert!(manifest.contains("--device=dri"));
}
```
- [ ] **Step 2: Run test to verify it fails**
Run: `cargo test --test packaging_test`
Expected: FAIL with missing documentation and packaging files.
- [ ] **Step 3: Create Flatpak manifest, docs, and LICENSE**
Create `packaging/flatpak/io.github.molotov.Molotov.yaml`:
```yaml
app-id: io.github.molotov.Molotov
runtime: org.freedesktop.Platform
runtime-version: '24.08'
sdk: org.freedesktop.Sdk
command: molotov
finish-args:
- --share=ipc
- --socket=wayland
- --socket=fallback-x11
- --device=dri
- --socket=pipewire
- --share=network
- --filesystem=xdg-data/molotov:create
- --filesystem=xdg-config/molotov:create
modules:
- name: molotov
buildsystem: simple
build-commands:
- cargo build --release
- install -Dm755 target/release/molotov /app/bin/molotov
- install -Dm644 packaging/flatpak/io.github.molotov.Molotov.desktop /app/share/applications/io.github.molotov.Molotov.desktop
```
Create `packaging/flatpak/io.github.molotov.Molotov.desktop`:
```desktop
[Desktop Entry]
Name=Molotov
Comment=Native Roblox Runtime for Linux
Exec=molotov
Icon=io.github.molotov.Molotov
Terminal=false
Type=Application
Categories=Game;
```
Create `docs/status.md`:
```markdown
# Molotov Status & Subsystem Tracker
Modeled after Cordial's honest status documentation.
| Subsystem | Status | Known Issues / Notes | Issue # |
| :--- | :--- | :--- | :--- |
| **Bootstrapper CDN** | ✅ Complete | Atomic downloads with SHA-256 validation | - |
| **APK Verifier** | ✅ Complete | X.509 signature verification against official cert | - |
| **FastFlags Engine** | ✅ Complete | Allowlist checks, presets, VRAM optimizations | - |
| **Mod System** | ✅ Complete | In-memory asset path redirection | - |
| **Multi-Instance** | ✅ Complete | Bubblewrap namespace isolation | - |
| **Bionic Linker** | 🚧 In Progress | ELF64 relocations in development | #101 |
| **JNI VM** | 🚧 In Progress | Activity callbacks & context mapping | #102 |
| **Voice Chat (AAudio)**| 🚧 Planned | Ephemeral PipeWire capture node | #103 |
| **IME Preedit** | 🚧 Planned | SDL3 candidate window tracking | #104 |
```
Create `docs/comparison.md`:
```markdown
# Molotov Comparison Matrix
Objective comparison between Linux Roblox execution methods:
| Feature | Molotov | Cordial | Mocktail | Sober |
| :--- | :--- | :--- | :--- | :--- |
| **Implementation** | Pure Rust | C++ / Go | Go / C | C++ / Proprietary |
| **Wine Dependent** | ❌ No | ❌ No | ❌ No | ❌ No |
| **Voice Chat** | ✅ Ephemeral PipeWire | ❌ None | ⚠️ Static stream | ⚠️ Static stream |
| **IME Preedit** | ✅ SDL3 / Wayland | ✅ Yes | ⚠️ Partial | ❌ Broken |
| **Controller Rumble** | ✅ SDL3 Gamepad | ❌ No haptics | ❌ No | ⚠️ Basic |
| **Open Source** | ✅ GPL-3.0 | ✅ GPL-3.0 | ✅ GPL-3.0 | ❌ Closed |
| **Integrated Bootstrapper**| ✅ Yes | ❌ External | ⚠️ Config only | ❌ Minimal |
```
Create `docs/building.md`:
```markdown
# Building Molotov from Source
### Dependencies
#### Fedora
```bash
sudo dnf install cargo rust pipewire-devel SDL3-devel bubblewrap
```
#### Arch Linux
```bash
sudo pacman -S rust pipewire sdl3 bubblewrap
```
#### Ubuntu / Debian
```bash
sudo apt install cargo rustc libpipewire-0.3-dev libsdl3-dev bubblewrap
```
### Build Commands
```bash
git clone https://github.com/molotov/molotov.git
cd molotov
cargo build --release
./target/release/molotov
```
```
Create `LICENSE`:
```text
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
... [Full GPLv3 text]
```
- [ ] **Step 4: Run test to verify it passes**
Run: `cargo test --test packaging_test`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add packaging/ docs/ LICENSE tests/
git commit -m "docs: add packaging manifests, honest comparison matrix, and GPLv3 license"
```
---