use std::collections::HashSet;
use std::io::Read;
use std::path::Path;
use anyhow::{Context, Result};
use flate2::read::GzDecoder;
use crate::config::GrammarSource;
use super::build;
include!(concat!(env!("OUT_DIR"), "/bundled_grammars.rs"));
const MARKER: &str = ".bundled_version";
pub fn bootstrap(
grammar_dir: &Path,
query_dir: &Path,
user_grammars: &[GrammarSource],
) -> Result<usize> {
if BUNDLED_GRAMMARS.is_empty() {
return Ok(0);
}
let current = env!("CARGO_PKG_VERSION");
let marker_path = grammar_dir.join(MARKER);
let refresh = std::fs::read_to_string(&marker_path)
.map(|v| v.trim() != current)
.unwrap_or(true);
let overridden: HashSet<&str> = user_grammars.iter().map(|g| g.name.as_str()).collect();
std::fs::create_dir_all(grammar_dir)
.with_context(|| format!("creating grammar dir {}", grammar_dir.display()))?;
let mut written = 0;
for (name, ext, gz) in BUNDLED_GRAMMARS {
if overridden.contains(name) {
continue;
}
if !refresh && build::is_fully_installed(name, grammar_dir, query_dir) {
continue;
}
if let Err(e) = extract_one(name, ext, gz, grammar_dir, query_dir) {
crate::vlog!("bundled grammar `{name}` extract failed: {e:#}");
continue;
}
written += 1;
}
let _ = std::fs::write(&marker_path, current);
Ok(written)
}
fn extract_one(
name: &str,
ext: &str,
gz: &[u8],
grammar_dir: &Path,
query_dir: &Path,
) -> Result<()> {
let mut decoder = GzDecoder::new(gz);
let mut bytes = Vec::new();
decoder
.read_to_end(&mut bytes)
.with_context(|| format!("decompressing bundled grammar `{name}`"))?;
let lib_path = grammar_dir.join(format!("{name}.{ext}"));
let tmp = grammar_dir.join(format!(".{name}.{ext}.tmp"));
std::fs::write(&tmp, &bytes).with_context(|| format!("writing {}", tmp.display()))?;
std::fs::rename(&tmp, &lib_path)
.with_context(|| format!("installing {}", lib_path.display()))?;
build::write_vendored_queries(query_dir, name)
.with_context(|| format!("writing queries for `{name}`"))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp(label: &str) -> std::path::PathBuf {
std::env::temp_dir().join(format!(
"vorto-bundled-test-{}-{}",
label,
std::process::id()
))
}
#[test]
fn bootstrap_extracts_then_is_idempotent() {
assert!(
!BUNDLED_GRAMMARS.is_empty(),
"run `vorto grammar bundle` for this target before testing with --features bundled-grammars"
);
let root = tmp("extract");
let _ = std::fs::remove_dir_all(&root);
let gdir = root.join("grammars");
let qdir = root.join("queries");
let n = bootstrap(&gdir, &qdir, &[]).unwrap();
assert_eq!(n, BUNDLED_GRAMMARS.len(), "first run extracts everything");
let (name, ext, _) = BUNDLED_GRAMMARS[0];
let lib = gdir.join(format!("{name}.{ext}"));
assert!(lib.exists(), "{} should exist", lib.display());
assert!(std::fs::metadata(&lib).unwrap().len() > 1024);
assert!(qdir.join(name).join("highlights.scm").exists());
assert!(gdir.join(MARKER).exists());
let again = bootstrap(&gdir, &qdir, &[]).unwrap();
assert_eq!(again, 0, "steady state extracts nothing");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn user_override_is_not_clobbered() {
let root = tmp("override");
let _ = std::fs::remove_dir_all(&root);
let gdir = root.join("grammars");
let qdir = root.join("queries");
let (name, ext, _) = BUNDLED_GRAMMARS[0];
let overrides = vec![GrammarSource {
name: name.to_string(),
source: "https://example.com/fork".into(),
rev: None,
subpath: None,
}];
bootstrap(&gdir, &qdir, &overrides).unwrap();
assert!(
!gdir.join(format!("{name}.{ext}")).exists(),
"bundle clobbered a user-overridden grammar"
);
let _ = std::fs::remove_dir_all(&root);
}
}