Skip to content

Commit d8c192c

Browse files
committed
feat(apply): seed stdlib and store on a fresh install
A fresh config has no modules, so apply patched Spotify and left a client with no stdlib and no store: the store's whole point, installing modules without the CLI, was unreachable and the only way out was knowing the exact pkg install/enable incantation. Apply now installs and enables the system modules from the registry when they are absent, disk-backed so the store's later localStorage updates always have a working version to fall back to. Best-effort, like the classmap fetch beside it, and a no-op once they exist so it costs nothing after the first run.
1 parent 162b880 commit d8c192c

2 files changed

Lines changed: 83 additions & 0 deletions

File tree

rust/crates/spicetify/src/commands/apply.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,11 @@ fn stage_modules(ctx: &AppContext, dest: &Path) -> Result<()> {
255255

256256
refresh_classmap(ctx, &version);
257257

258+
// A fresh config has no modules, so without this the client boots with no
259+
// stdlib and no store and the user has no way in. Runs before staging so
260+
// anything seeded is staged in this same apply; a no-op once they exist.
261+
super::pkg::ensure_system_modules(ctx);
262+
258263
let updates_blocked = super::updates::is_blocked(ctx).unwrap_or(false);
259264

260265
match crate::module::stage::stage_modules(

rust/crates/spicetify/src/commands/pkg.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,67 @@ pub(crate) fn list(ctx: &AppContext) -> Result<()> {
9292
Ok(())
9393
}
9494

95+
/// The infrastructure a usable v3 client needs: stdlib is the foundation
96+
/// every module builds on, and the store is how a user installs anything else
97+
/// without the CLI. Not `manager`, which the store supersedes.
98+
const SYSTEM_MODULES: &[&str] = &["stdlib", "store"];
99+
100+
/// Which system modules are not present on disk. Absence is the signal: a
101+
/// disabled module keeps its directory, so this never re-seeds one the user
102+
/// turned off, and a deliberate `pkg delete` of stdlib or store leaves a
103+
/// client that cannot manage itself, so bringing it back is recovery.
104+
fn missing_system_modules(modules_root: &Path) -> Vec<&'static str> {
105+
SYSTEM_MODULES.iter().copied().filter(|id| !modules_root.join(id).exists()).collect()
106+
}
107+
108+
/// Installs and enables any absent system module from the registry, so a fresh
109+
/// `apply` produces a client that can manage itself rather than a patched
110+
/// Spotify with no stdlib and no store.
111+
///
112+
/// Best-effort by design, like the classmap fetch beside it: an unreachable
113+
/// vault or a failed download warns and leaves the rest of the apply to
114+
/// proceed, and a module already on disk is left alone for the store to
115+
/// update. Disk-backed on purpose, so the store's later updates (which shadow
116+
/// it through localStorage) always have a working version to fall back to.
117+
pub(crate) fn ensure_system_modules(ctx: &AppContext) {
118+
let modules_root = crate::module::modules_dir(&ctx.config_root);
119+
let missing = missing_system_modules(&modules_root);
120+
if missing.is_empty() {
121+
return;
122+
}
123+
let vault = match fetch_vault(DEFAULT_VAULT) {
124+
Ok(vault) => vault,
125+
Err(e) => {
126+
tracing::warn!("cannot seed system modules ({}): {e}", missing.join(", "));
127+
return;
128+
}
129+
};
130+
for id in missing {
131+
if let Err(e) = seed_system_module(ctx, &vault, id) {
132+
tracing::warn!("could not seed system module {id}: {e}");
133+
}
134+
}
135+
}
136+
137+
fn seed_system_module(ctx: &AppContext, vault: &Vault, id: &str) -> Result<()> {
138+
let module = vault.modules.get(id).ok_or_else(|| anyhow::anyhow!("not in the registry"))?;
139+
let version = resolve_version(module)?;
140+
let entry = module.v.get(&version).ok_or_else(|| anyhow::anyhow!("{version} is not in the registry"))?;
141+
if entry.artifacts.is_empty() {
142+
anyhow::bail!("{id}@{version} has no artifact");
143+
}
144+
let tag = format!("{id}@{version}");
145+
crate::module::install_from_vault(
146+
&ctx.config_root,
147+
&tag,
148+
entry.artifacts.clone(),
149+
entry.checksum.clone(),
150+
)?;
151+
crate::module::enable_module(&ctx.config_root, &tag)?;
152+
tracing::info!("seeded system module {tag}");
153+
Ok(())
154+
}
155+
95156
/// The checksum the registry recorded for `id@version`, if it carries that
96157
/// version at all.
97158
///
@@ -175,4 +236,21 @@ mod tests {
175236
fn rejects_an_empty_vault_entry() {
176237
assert!(resolve_version(&module("", &[])).is_err());
177238
}
239+
240+
#[test]
241+
fn seeds_only_the_system_modules_that_are_absent() {
242+
let dir = std::env::temp_dir().join(format!("spicetify-seed-{}", std::process::id()));
243+
let _ = std::fs::remove_dir_all(&dir);
244+
std::fs::create_dir_all(&dir).expect("temp dir");
245+
246+
assert_eq!(missing_system_modules(&dir), vec!["stdlib", "store"], "a fresh config needs both");
247+
248+
std::fs::create_dir_all(dir.join("stdlib")).expect("stdlib dir");
249+
assert_eq!(missing_system_modules(&dir), vec!["store"], "an installed module is left alone");
250+
251+
std::fs::create_dir_all(dir.join("store")).expect("store dir");
252+
assert!(missing_system_modules(&dir).is_empty(), "nothing to seed once both exist");
253+
254+
std::fs::remove_dir_all(&dir).expect("cleanup");
255+
}
178256
}

0 commit comments

Comments
 (0)