The launcher previously defaulted to fixtures and production generation used the source database directly. Generate a separate database and manifest set from package directories, validating staged output with the application loader before installation. Keep strict selection by default, with independent opt-ins for missing games and package version overrides in the copied database. Share database filtering and staging with the fixture publisher, and include source metadata and generation modes in the publication cache. Make normal runs consume existing production authority. Add an explicit local test recipe with separate resources, app settings, and a compiled startup game directory. Build-time gates exclude local authority from production and require Tauri development mode. Document the generation and launch workflows. Clear generated catalog copies before Tauri copies the selected resource tree so mode switches and reduced catalogs cannot retain stale manifests. Watch the copied files to repair deletion and preserve prior output mtimes only when the bytes are unchanged, allowing subsequent builds to become fresh. Test Plan: - `just fmt` -- passed. - `just clippy` -- passed with warnings denied. - `just test` -- workspace tests passed using fixture authority. - `just frontend-test` -- 94 passed. - `python3 -m unittest discover -s tools -p 'test_catalog_source_cache.py'` -- 4 passed. - `git diff --cached --check` -- passed. - Interactive GUI launches and production bundles were not exercised.
158 lines
6.1 KiB
Rust
158 lines
6.1 KiB
Rust
use std::{
|
|
env,
|
|
fs,
|
|
path::{Path, PathBuf},
|
|
};
|
|
|
|
use build_support::catalog_gate::{
|
|
CatalogBuildMode,
|
|
CatalogGateInput,
|
|
select_catalog_build_mode,
|
|
validate_production_catalog,
|
|
};
|
|
|
|
mod build_support {
|
|
#[path = "catalog_gate.rs"]
|
|
pub(crate) mod catalog_gate;
|
|
#[path = "catalog_resources.rs"]
|
|
pub(crate) mod catalog_resources;
|
|
}
|
|
|
|
const FIXTURE_DEVELOPMENT_ENV: &str = "LANSPREAD_USE_FIXTURE_CATALOG";
|
|
const LOCAL_DEVELOPMENT_ENV: &str = "LANSPREAD_USE_LOCAL_CATALOG";
|
|
const LOCAL_GAMES_DIR_ENV: &str = "LANSPREAD_LOCAL_GAMES_DIR";
|
|
const CATALOG_CONTENT_INDEX_NAME: &str = "catalog-content-index-v1.jsonl";
|
|
const EMBEDDED_CATALOG_INDEX_NAME: &str = "embedded-catalog-content-index-v1.jsonl";
|
|
const FIXTURE_CATALOG_ROOT: &str = "../../lanspread-peer-cli/catalogs/default";
|
|
const PRODUCTION_CATALOG_ROOT: &str = "production-catalog";
|
|
|
|
fn main() {
|
|
println!("cargo:rerun-if-env-changed=TAURI_CONFIG");
|
|
println!("cargo:rerun-if-env-changed={FIXTURE_DEVELOPMENT_ENV}");
|
|
println!("cargo:rerun-if-env-changed={LOCAL_DEVELOPMENT_ENV}");
|
|
println!("cargo:rerun-if-env-changed={LOCAL_GAMES_DIR_ENV}");
|
|
println!("cargo:rerun-if-changed=tauri.conf.json");
|
|
println!("cargo:rerun-if-changed={PRODUCTION_CATALOG_ROOT}");
|
|
|
|
let mode = catalog_build_mode().unwrap_or_else(|error| {
|
|
panic!("catalog packaging policy failed: {error}");
|
|
});
|
|
assert!(
|
|
mode != CatalogBuildMode::LocalDevelopment || tauri_build::is_dev(),
|
|
"local catalog testing is only available with tauri dev"
|
|
);
|
|
if mode != CatalogBuildMode::FixtureDevelopment
|
|
&& let Err(error) = validate_production_catalog(
|
|
catalog_root(mode).join("game.db"),
|
|
catalog_root(mode).join("manifests"),
|
|
)
|
|
{
|
|
panic!("production catalog authority gate failed: {error}");
|
|
}
|
|
embed_catalog_content_index(mode).unwrap_or_else(|error| {
|
|
panic!("failed to embed catalog launch authority: {error}");
|
|
});
|
|
embed_startup_game_directory(mode).unwrap_or_else(|error| {
|
|
panic!("failed to embed local test game directory: {error}");
|
|
});
|
|
let out_dir = env::var_os("OUT_DIR").expect("OUT_DIR is not set");
|
|
let resources = build_support::catalog_resources::CatalogResources::prepare(
|
|
Path::new(&out_dir),
|
|
catalog_root(mode),
|
|
)
|
|
.unwrap_or_else(|error| panic!("failed to reset generated catalog resources: {error}"));
|
|
tauri_build::build();
|
|
// Tauri watches source resources only. Watching the generated database and
|
|
// manifest tree also repairs deleted copies and notices another catalog
|
|
// mode overwriting this profile's shared resource destination.
|
|
for resource in resources
|
|
.finish()
|
|
.unwrap_or_else(|error| panic!("failed to track generated catalog resources: {error}"))
|
|
{
|
|
println!("cargo:rerun-if-changed={}", resource.display());
|
|
}
|
|
}
|
|
|
|
fn catalog_root(mode: CatalogBuildMode) -> &'static Path {
|
|
Path::new(match mode {
|
|
CatalogBuildMode::FixtureDevelopment => FIXTURE_CATALOG_ROOT,
|
|
CatalogBuildMode::LocalDevelopment => "local-catalog",
|
|
CatalogBuildMode::Production => PRODUCTION_CATALOG_ROOT,
|
|
})
|
|
}
|
|
|
|
fn embed_startup_game_directory(mode: CatalogBuildMode) -> Result<(), Box<dyn std::error::Error>> {
|
|
let directory = if mode == CatalogBuildMode::LocalDevelopment {
|
|
let path = env::var_os(LOCAL_GAMES_DIR_ENV)
|
|
.ok_or_else(|| format!("{LOCAL_GAMES_DIR_ENV} is required for local test builds"))?;
|
|
let path = PathBuf::from(path).canonicalize()?;
|
|
if !path.is_dir() {
|
|
return Err("local test games path must be a directory".into());
|
|
}
|
|
Some(
|
|
path.into_os_string()
|
|
.into_string()
|
|
.map_err(|_| "local test games path must be UTF-8")?,
|
|
)
|
|
} else {
|
|
None
|
|
};
|
|
let out_dir = env::var_os("OUT_DIR").ok_or("OUT_DIR is not set")?;
|
|
// Rust string escaping keeps arbitrary path characters out of Cargo's
|
|
// line-based build-script protocol. Production compiles a literal None.
|
|
fs::write(
|
|
PathBuf::from(out_dir).join("startup-game-directory.rs"),
|
|
format!("{directory:?}"),
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn embed_catalog_content_index(mode: CatalogBuildMode) -> Result<(), Box<dyn std::error::Error>> {
|
|
let manifest_root = catalog_root(mode).join("manifests");
|
|
let source = manifest_root.join(CATALOG_CONTENT_INDEX_NAME);
|
|
println!("cargo:rerun-if-changed={}", source.display());
|
|
let bytes = fs::read(&source)?;
|
|
let out_dir =
|
|
env::var_os("OUT_DIR").ok_or_else(|| std::io::Error::other("OUT_DIR is not set"))?;
|
|
let output = PathBuf::from(out_dir).join(EMBEDDED_CATALOG_INDEX_NAME);
|
|
fs::write(output, bytes)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn catalog_build_mode() -> Result<CatalogBuildMode, Box<dyn std::error::Error>> {
|
|
let base_config = fs::read_to_string("tauri.conf.json")?;
|
|
let config_override = env::var_os("TAURI_CONFIG")
|
|
.map(|value| {
|
|
value
|
|
.into_string()
|
|
.map_err(|_| "TAURI_CONFIG is not valid UTF-8".to_owned())
|
|
})
|
|
.transpose()?;
|
|
let fixture_development_opt_in = match env::var_os(FIXTURE_DEVELOPMENT_ENV) {
|
|
None => false,
|
|
Some(value) if value == "1" => true,
|
|
Some(_) => {
|
|
return Err(format!("{FIXTURE_DEVELOPMENT_ENV} must be exactly 1 when set").into());
|
|
}
|
|
};
|
|
let cargo_profile = env::var("PROFILE").ok();
|
|
let local_development_opt_in = match env::var_os(LOCAL_DEVELOPMENT_ENV) {
|
|
None => false,
|
|
Some(value) if value == "1" => true,
|
|
Some(_) => {
|
|
return Err(format!("{LOCAL_DEVELOPMENT_ENV} must be exactly 1 when set").into());
|
|
}
|
|
};
|
|
let out_dir = env::var_os("OUT_DIR").map(std::path::PathBuf::from);
|
|
|
|
select_catalog_build_mode(CatalogGateInput {
|
|
base_config: &base_config,
|
|
config_override: config_override.as_deref(),
|
|
fixture_development_opt_in,
|
|
local_development_opt_in,
|
|
cargo_profile: cargo_profile.as_deref(),
|
|
out_dir: out_dir.as_deref(),
|
|
})
|
|
.map_err(Into::into)
|
|
}
|