use crate::error::{CrankError, Result};
use colored::Colorize;
use std::env;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
#[derive(Debug, Clone)]
pub struct Sdk {
pub path: PathBuf,
}
impl Sdk {
pub fn detect() -> Result<Self> {
if let Some(path) = Self::from_env() {
return Ok(Self { path });
}
if let Some(path) = Self::from_default_location() {
return Ok(Self { path });
}
Err(CrankError::SdkNotFound)
}
fn from_env() -> Option<PathBuf> {
env::var("PLAYDATE_SDK_PATH")
.ok()
.map(PathBuf::from)
.filter(|p| p.exists() && p.is_dir())
}
fn from_default_location() -> Option<PathBuf> {
let home = env::var("HOME").or_else(|_| env::var("USERPROFILE")).ok()?;
let home_path = PathBuf::from(home);
let candidates = if cfg!(target_os = "macos") {
vec![
home_path.join("Developer").join("PlaydateSDK"),
PathBuf::from("/usr/local/share/PlaydateSDK"),
]
} else if cfg!(target_os = "windows") {
vec![
home_path.join("Documents").join("PlaydateSDK"),
home_path.join("PlaydateSDK"),
]
} else {
vec![
home_path.join("PlaydateSDK"),
PathBuf::from("/opt/PlaydateSDK"),
PathBuf::from("/usr/local/PlaydateSDK"),
]
};
candidates.into_iter().find(|p| p.exists() && p.is_dir())
}
pub fn pdc_path(&self) -> Result<PathBuf> {
let pdc = if cfg!(target_os = "windows") {
self.path.join("bin").join("pdc.exe")
} else {
self.path.join("bin").join("pdc")
};
if pdc.exists() {
Ok(pdc)
} else {
Err(CrankError::SdkComponentNotFound {
component: "pdc".to_string(),
})
}
}
pub fn simulator_path(&self) -> Result<PathBuf> {
let simulator = if cfg!(target_os = "macos") {
self.path
.join("bin")
.join("Playdate Simulator.app")
.join("Contents")
.join("MacOS")
.join("Playdate Simulator")
} else if cfg!(target_os = "windows") {
self.path.join("bin").join("PlaydateSimulator.exe")
} else {
self.path.join("bin").join("PlaydateSimulator")
};
if simulator.exists() || (cfg!(target_os = "macos") && simulator.parent().unwrap().exists())
{
Ok(simulator)
} else {
Err(CrankError::SdkComponentNotFound {
component: "PlaydateSimulator".to_string(),
})
}
}
pub fn compile<P: AsRef<Path>, Q: AsRef<Path>>(
&self,
source_dir: P,
output_path: Q,
verbose: bool,
) -> Result<()> {
let pdc = self.pdc_path()?;
let source = source_dir.as_ref();
let output = output_path.as_ref();
if !source.exists() {
return Err(CrankError::InvalidProject(format!(
"Source directory not found: {}",
source.display()
)));
}
if let Some(parent) = output.parent() {
std::fs::create_dir_all(parent)?;
}
let mut cmd = Command::new(&pdc);
cmd.arg(source).arg(output);
if verbose {
cmd.stdout(Stdio::inherit()).stderr(Stdio::inherit());
let status = cmd.status()?;
if !status.success() {
return Err(CrankError::BuildFailed(
"Compilation failed. See output above for details.".to_string(),
));
}
} else {
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
let output_result = cmd.output()?;
let stdout = String::from_utf8_lossy(&output_result.stdout);
let stderr = String::from_utf8_lossy(&output_result.stderr);
if !stderr.is_empty() && stderr.contains("warning") {
eprintln!("{} Build warnings:", "⚠".yellow().bold());
for line in stderr.lines() {
if !line.trim().is_empty() {
eprintln!(" {}", line);
}
}
}
if !output_result.status.success() {
let error_msg = if !stderr.is_empty() {
stderr.trim().to_string()
} else if !stdout.is_empty() {
stdout.trim().to_string()
} else {
"Compilation failed with unknown error".to_string()
};
return Err(CrankError::BuildFailed(error_msg));
}
}
Ok(())
}
pub fn launch_simulator<P: AsRef<Path>>(&self, pdx_path: P) -> Result<()> {
let simulator = self.simulator_path()?;
let pdx = pdx_path.as_ref();
if !pdx.exists() {
return Err(CrankError::InvalidProject(format!(
"PDX bundle not found: {}",
pdx.display()
)));
}
if !pdx.is_dir() {
return Err(CrankError::InvalidProject(format!(
"Invalid PDX bundle (not a directory): {}",
pdx.display()
)));
}
let mut cmd = if cfg!(target_os = "macos") {
let mut c = Command::new("open");
c.arg("-a");
let app_bundle = self.path.join("bin").join("Playdate Simulator.app");
c.arg(&app_bundle);
c.arg(pdx);
c
} else {
let mut c = Command::new(&simulator);
c.arg(pdx);
c
};
cmd.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
cmd.spawn().map_err(|e| {
CrankError::SimulatorFailed(format!(
"Failed to launch simulator: {}. \n\
Simulator path: {}\n\
Try running it manually to verify it works.",
e,
simulator.display()
))
})?;
Ok(())
}
pub fn validate(&self) -> Result<()> {
if !self.path.exists() || !self.path.is_dir() {
return Err(CrankError::SdkNotFound);
}
self.pdc_path()?;
self.simulator_path()?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sdk_detection() {
if let Ok(sdk) = Sdk::detect() {
assert!(sdk.path.exists());
assert!(sdk.pdc_path().is_ok());
}
}
}