use anyhow::{Context, Result};
use std::path::Path;
use std::process::Command;
pub fn validate_godot_version(godot_exe: &str) -> Result<()> {
let output = Command::new(godot_exe).arg("--version").output().map_err(|e| {
let suggestions = suggest_godot_paths();
anyhow::anyhow!(
"Failed to execute Godot at '{}': {}\n\
\n\
Godot does not appear to be installed or is not in PATH.\n\
\n\
Common Godot installation paths:\n\
{}\n\
\n\
Installation help:\n\
- macOS: brew install godot\n\
- Linux: Download from https://godotengine.org/download\n\
- Windows: Download from https://godotengine.org/download\n\
\n\
Or set the GODOT_BIN environment variable:\n\
export GODOT_BIN=/path/to/godot",
godot_exe,
e,
suggestions
)
})?;
if !output.status.success() {
anyhow::bail!(
"Failed to get Godot version from '{}'\n\
Exit code: {:?}",
godot_exe,
output.status.code()
);
}
let version_output = String::from_utf8_lossy(&output.stdout);
let version_str = version_output.trim();
let version_parts: Vec<&str> = version_str.split('.').collect();
if version_parts.len() < 2 {
anyhow::bail!(
"Could not parse Godot version from output: '{}'\n\
Expected format: X.Y.Z...",
version_str
);
}
let major: u32 = version_parts[0]
.parse()
.context(format!("Invalid major version number in '{}'", version_str))?;
let minor: u32 = version_parts[1]
.parse()
.context(format!("Invalid minor version number in '{}'", version_str))?;
const MIN_MAJOR: u32 = 4;
const MIN_MINOR: u32 = 3;
if major < MIN_MAJOR || (major == MIN_MAJOR && minor < MIN_MINOR) {
anyhow::bail!(
"Godot version {}.{} is not supported. Minimum required version is {}.{}.\n\
\n\
Current version: {}\n\
\n\
Please upgrade Godot:\n\
- macOS: brew upgrade godot\n\
- Linux/Windows: Download from https://godotengine.org/download",
major,
minor,
MIN_MAJOR,
MIN_MINOR,
version_str
);
}
println!(" ✓ Godot version: {}", version_str);
println!(" (Minimum {}.{} required)", MIN_MAJOR, MIN_MINOR);
Ok(())
}
pub fn validate_symlink(project_path: &Path, link_name: &str, expected_target: &str, description: &str) -> Result<()> {
let symlink_path = project_path.join(link_name);
if !symlink_path.exists() {
anyhow::bail!(
"Required symlink missing: {}\n\
Expected: {} -> {}\n\
Purpose: {}\n\
\n\
Fix this by creating the symlink:\n\
cd {} && ln -s {} {}",
link_name,
link_name,
expected_target,
description,
project_path.display(),
expected_target,
link_name
);
}
if !symlink_path.is_symlink() {
if symlink_path.is_dir() {
println!(" ✓ {}: {} (directory)", description, symlink_path.display());
return Ok(());
}
anyhow::bail!(
"Expected symlink but found file: {}\n\
Expected: {} -> {}",
symlink_path.display(),
link_name,
expected_target
);
}
match std::fs::read_link(&symlink_path) {
Ok(target) => {
let target_path = if target.is_absolute() {
target.clone()
} else {
symlink_path.parent().unwrap().join(&target)
};
if !target_path.exists() {
anyhow::bail!(
"Symlink is broken: {}\n\
Points to: {} (does not exist)\n\
Expected: {}\n\
\n\
Fix this by recreating the symlink:\n\
cd {} && rm {} && ln -s {} {}",
link_name,
target.display(),
expected_target,
project_path.display(),
link_name,
expected_target,
link_name
);
}
let canonical_target = std::fs::canonicalize(&target_path).ok();
let expected_full_path = project_path.join(expected_target);
let canonical_expected = std::fs::canonicalize(&expected_full_path).ok();
if let (Some(actual), Some(expected)) = (canonical_target, canonical_expected) {
if actual != expected {
println!(
" ⚠️ {}: {} -> {} (expected {})",
description,
link_name,
target.display(),
expected_target
);
} else {
println!(" ✓ {}: {} -> {}", description, link_name, target.display());
}
} else {
println!(" ✓ {}: {} -> {}", description, link_name, target.display());
}
Ok(())
}
Err(e) => {
anyhow::bail!(
"Failed to read symlink: {}\n\
Error: {}\n\
\n\
Try recreating the symlink:\n\
cd {} && rm {} && ln -s {} {}",
symlink_path.display(),
e,
project_path.display(),
link_name,
expected_target,
link_name
);
}
}
}
fn suggest_godot_paths() -> String {
use std::path::Path;
let mut suggestions = Vec::new();
#[cfg(target_os = "macos")]
{
let paths = vec![
"/Applications/Godot.app/Contents/MacOS/Godot",
"/Applications/Godot_mono.app/Contents/MacOS/Godot",
"/usr/local/bin/godot",
"/opt/homebrew/bin/godot",
"~/Applications/Godot.app/Contents/MacOS/Godot",
];
for path in paths {
let expanded_path = if path.starts_with("~/") {
if let Ok(home) = std::env::var("HOME") {
path.replace("~", &home)
} else {
path.to_string()
}
} else {
path.to_string()
};
let exists = Path::new(&expanded_path).exists();
let marker = if exists { "✓ FOUND" } else { " " };
suggestions.push(format!(" {} {}", marker, path));
}
}
#[cfg(target_os = "linux")]
{
let paths = vec![
"/usr/bin/godot",
"/usr/local/bin/godot",
"/snap/bin/godot",
"~/.local/bin/godot",
"/opt/godot/godot",
];
for path in paths {
let expanded_path = if path.starts_with("~/") {
if let Ok(home) = std::env::var("HOME") {
path.replace("~", &home)
} else {
path.to_string()
}
} else {
path.to_string()
};
let exists = Path::new(&expanded_path).exists();
let marker = if exists { "✓ FOUND" } else { " " };
suggestions.push(format!(" {} {}", marker, path));
}
}
#[cfg(target_os = "windows")]
{
let paths = vec![
"C:\\Program Files\\Godot\\Godot.exe",
"C:\\Program Files (x86)\\Godot\\Godot.exe",
"C:\\Godot\\Godot.exe",
"%LOCALAPPDATA%\\Godot\\Godot.exe",
];
for path in paths {
let expanded_path = if path.contains("%LOCALAPPDATA%") {
if let Ok(local_app_data) = std::env::var("LOCALAPPDATA") {
path.replace("%LOCALAPPDATA%", &local_app_data)
} else {
path.to_string()
}
} else {
path.to_string()
};
let exists = Path::new(&expanded_path).exists();
let marker = if exists { "✓ FOUND" } else { " " };
suggestions.push(format!(" {} {}", marker, path));
}
}
if suggestions.is_empty() {
" (No common paths found for this platform)".to_string()
} else {
suggestions.join("\n ")
}
}