use std::env; use std::error::Error; use std::fs; use std::os::unix::fs::symlink; use std::path::{Path, PathBuf}; use std::process::{Command, ExitCode}; type Result = std::result::Result>; /// The AddOns directory for the WoW Forever beta client, relative to `$HOME`. /// /// Override with the `WOW_ADDONS_DIR` environment variable. const DEFAULT_ADDONS_DIR: &str = ".local/share/bottles/bottles/Battle.net/drive_c/Program Files (x86)/World of Warcraft/_forever_/Interface/AddOns"; const ANNOTATIONS_REPO: &str = "https://github.com/Ketho/vscode-wow-api.git"; const USAGE: &str = "\ Usage: cargo xtask Commands: link Symlink each addon in addons/ into the WoW AddOns directory unlink Remove symlinks in the WoW AddOns directory that point into this repo annotations Download or update the WoW API annotations used by LuaLS Environment: WOW_ADDONS_DIR Overrides the WoW AddOns directory"; fn main() -> ExitCode { let command = env::args().nth(1); let result = match command.as_deref() { Some("link") => link(), Some("unlink") => unlink(), Some("annotations") => annotations(), Some("help" | "-h" | "--help") => { println!("{USAGE}"); Ok(()) } _ => { eprintln!("{USAGE}"); return ExitCode::FAILURE; } }; match result { Ok(()) => ExitCode::SUCCESS, Err(err) => { eprintln!("error: {err}"); ExitCode::FAILURE } } } fn repo_root() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) .parent() .expect("xtask should live inside the repo") .to_path_buf() } fn addons_dir() -> Result { if let Some(dir) = env::var_os("WOW_ADDONS_DIR") { return Ok(PathBuf::from(dir)); } let home = env::var_os("HOME").ok_or("$HOME is not set")?; Ok(Path::new(&home).join(DEFAULT_ADDONS_DIR)) } /// Returns the addon directories in `addons_root`. /// /// A directory is an addon if it contains a TOC file named after it, either /// `.toc` or a flavor-specific `_.toc`. fn find_addons(addons_root: &Path) -> Result> { let mut addons = Vec::new(); for entry in fs::read_dir(addons_root)? { let path = entry?.path(); if !path.is_dir() { continue; } let Some(name) = path.file_name().and_then(|name| name.to_str()) else { continue; }; let has_toc = fs::read_dir(&path)? .filter_map(|entry| entry.ok()) .any(|entry| { let file_name = entry.file_name(); let Some(file_name) = file_name.to_str() else { return false; }; let Some(stem) = file_name.strip_suffix(".toc") else { return false; }; stem == name || stem .strip_prefix(name) .is_some_and(|rest| rest.starts_with('_')) }); if has_toc { addons.push(path); } } addons.sort(); Ok(addons) } fn ensure_addons_dir_exists(addons_dir: &Path) -> Result<()> { if addons_dir.is_dir() { return Ok(()); } Err(format!( "AddOns directory not found: {}\nSet WOW_ADDONS_DIR to point at your client's Interface/AddOns directory.", addons_dir.display() ) .into()) } fn link() -> Result<()> { let addons_root = repo_root().join("addons"); let addons_dir = addons_dir()?; ensure_addons_dir_exists(&addons_dir)?; let addons = find_addons(&addons_root)?; if addons.is_empty() { println!("No addons found in {}", addons_root.display()); return Ok(()); } println!("Linking addons into {}", addons_dir.display()); for addon in addons { let name = addon.file_name().unwrap(); let dest = addons_dir.join(name); let name = name.to_string_lossy(); match fs::read_link(&dest) { Ok(target) if target == addon => { println!(" {name}: already linked"); } Ok(target) => { println!( " {name}: skipped, already a symlink to {}", target.display() ); } Err(_) if dest.symlink_metadata().is_ok() => { println!(" {name}: skipped, a non-symlink already exists at the destination"); } Err(_) => { symlink(&addon, &dest)?; println!(" {name}: linked"); } } } Ok(()) } fn unlink() -> Result<()> { let root = repo_root(); let addons_dir = addons_dir()?; ensure_addons_dir_exists(&addons_dir)?; let mut removed = 0; for entry in fs::read_dir(&addons_dir)? { let path = entry?.path(); let Ok(target) = fs::read_link(&path) else { continue; }; if target.starts_with(&root) { fs::remove_file(&path)?; println!( " {}: unlinked", path.file_name().unwrap().to_string_lossy() ); removed += 1; } } if removed == 0 { println!("No linked addons found in {}", addons_dir.display()); } Ok(()) } fn annotations() -> Result<()> { let dest = repo_root().join(".annotations/vscode-wow-api"); let status = if dest.join(".git").is_dir() { println!("Updating annotations in {}", dest.display()); Command::new("git") .args(["pull", "--ff-only", "--depth", "1"]) .current_dir(&dest) .status()? } else { println!("Cloning annotations into {}", dest.display()); Command::new("git") .args(["clone", "--depth", "1", ANNOTATIONS_REPO]) .arg(&dest) .status()? }; if !status.success() { return Err(format!("git exited with {status}").into()); } Ok(()) }