#![allow(dead_code)]
use std::fs::{self, File};
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
#[cfg(feature = "bundled-tsgo")]
include!(concat!(env!("OUT_DIR"), "/tsgo_version.rs"));
#[cfg(not(feature = "bundled-tsgo"))]
mod version_fallback {
pub const TSGO_COMMIT: &str = "unknown";
pub const TSGO_BUILD_HASH: &str = "unknown";
}
#[cfg(not(feature = "bundled-tsgo"))]
use version_fallback::*;
pub const TSGO_VERSION: &str = TSGO_COMMIT;
#[cfg(feature = "bundled-tsgo")]
static TSGO_BINARY_ZST: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/tsgo.zst"));
#[cfg(feature = "bundled-tsgo")]
static TSGO_LIBS_TAR_ZST: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/tsgo_libs.tar.zst"));
#[cfg(feature = "bundled-tsgo")]
pub fn is_embedded() -> bool {
!TSGO_BINARY_ZST.is_empty()
}
#[cfg(not(feature = "bundled-tsgo"))]
pub fn is_embedded() -> bool {
false
}
fn get_cache_dir() -> io::Result<PathBuf> {
#[cfg(feature = "bundled-tsgo")]
{
let base = std::env::var("XDG_CACHE_HOME")
.ok()
.map(PathBuf::from)
.filter(|p| p.is_absolute())
.or_else(dirs::cache_dir)
.unwrap_or_else(std::env::temp_dir);
let cache_dir = base
.join("monoripple")
.join(format!("tsgo-{}", TSGO_BUILD_HASH));
fs::create_dir_all(&cache_dir).map_err(|e| {
io::Error::new(
e.kind(),
format!(
"Failed to create cache directory {}: {}",
cache_dir.display(),
e
),
)
})?;
Ok(cache_dir)
}
#[cfg(not(feature = "bundled-tsgo"))]
{
Err(io::Error::new(
io::ErrorKind::NotFound,
"bundled-tsgo feature not enabled",
))
}
}
#[cfg(feature = "bundled-tsgo")]
pub fn extract_tsgo_binary() -> io::Result<PathBuf> {
if TSGO_BINARY_ZST.is_empty() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"No embedded tsgo binary (build-time download may have failed)",
));
}
let cache_dir = get_cache_dir()?;
let binary_name = if cfg!(windows) { "tsgo.exe" } else { "tsgo" };
let binary_path = cache_dir.join(binary_name);
let lib_marker = cache_dir.join("lib.d.ts");
if binary_path.exists() && lib_marker.exists() {
let metadata = fs::metadata(&binary_path)?;
if metadata.len() > 0 {
return Ok(binary_path);
}
}
let decompressed = zstd::decode_all(TSGO_BINARY_ZST).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Failed to decompress tsgo binary: {e}"),
)
})?;
let temp_suffix = format!(
"{}.{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
);
let temp_path = cache_dir.join(format!("{}.tmp.{}", binary_name, temp_suffix));
{
let mut file = File::create(&temp_path).map_err(|e| {
io::Error::new(
e.kind(),
format!("Failed to create temp file {}: {}", temp_path.display(), e),
)
})?;
file.write_all(&decompressed)?;
file.sync_all()?;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&temp_path, fs::Permissions::from_mode(0o755)).map_err(|e| {
io::Error::new(
e.kind(),
format!(
"Failed to set permissions on {}: {}",
temp_path.display(),
e
),
)
})?;
}
if let Err(rename_err) = fs::rename(&temp_path, &binary_path) {
fs::copy(&temp_path, &binary_path).map_err(|e| {
io::Error::new(
e.kind(),
format!(
"Failed to copy {} to {} (rename failed with: {}): {}",
temp_path.display(),
binary_path.display(),
rename_err,
e
),
)
})?;
let _ = fs::remove_file(&temp_path);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&binary_path, fs::Permissions::from_mode(0o755))?;
}
}
if !TSGO_LIBS_TAR_ZST.is_empty() {
extract_libs_to_dir(&cache_dir)?;
}
Ok(binary_path)
}
#[cfg(feature = "bundled-tsgo")]
fn extract_libs_to_dir(dir: &Path) -> io::Result<()> {
let decompressed = zstd::decode_all(TSGO_LIBS_TAR_ZST).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Failed to decompress lib files: {e}"),
)
})?;
let mut archive = tar::Archive::new(decompressed.as_slice());
for entry in archive.entries()? {
let mut entry = entry?;
let path = entry.path()?;
let file_name = path.file_name().unwrap_or_default();
let dest_path = dir.join(file_name);
let mut contents = Vec::new();
entry.read_to_end(&mut contents)?;
fs::write(&dest_path, &contents)?;
}
Ok(())
}
#[cfg(not(feature = "bundled-tsgo"))]
pub fn extract_tsgo_binary() -> io::Result<PathBuf> {
Err(io::Error::new(
io::ErrorKind::NotFound,
"bundled-tsgo feature not enabled",
))
}
#[cfg(feature = "bundled-tsgo")]
pub fn extract_tsgo_libs() -> io::Result<PathBuf> {
let binary_path = extract_tsgo_binary()?;
Ok(binary_path.parent().unwrap().to_path_buf())
}
#[cfg(not(feature = "bundled-tsgo"))]
pub fn extract_tsgo_libs() -> io::Result<PathBuf> {
Err(io::Error::new(
io::ErrorKind::NotFound,
"bundled-tsgo feature not enabled",
))
}
pub fn clean_cache() -> io::Result<()> {
let cache_dir = get_cache_dir()?;
if cache_dir.exists() {
fs::remove_dir_all(&cache_dir)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_embedded() {
let embedded = is_embedded();
println!("tsgo embedded: {}", embedded);
}
#[test]
#[cfg(feature = "bundled-tsgo")]
fn test_extract_binary() {
if !is_embedded() {
println!("Skipping test: no embedded binary");
return;
}
let path = extract_tsgo_binary().expect("Failed to extract tsgo binary");
assert!(path.exists(), "Extracted binary should exist");
let metadata = fs::metadata(&path).expect("Failed to get metadata");
assert!(metadata.len() > 0, "Binary should not be empty");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = metadata.permissions();
assert!(perms.mode() & 0o111 != 0, "Binary should be executable");
}
}
#[test]
#[cfg(feature = "bundled-tsgo")]
fn test_extract_libs() {
if !is_embedded() {
println!("Skipping test: no embedded binary");
return;
}
match extract_tsgo_libs() {
Ok(path) => {
assert!(path.exists(), "Lib directory should exist");
let lib_d_ts = path.join("lib.d.ts");
assert!(lib_d_ts.exists(), "lib.d.ts should exist");
}
Err(e) => {
println!("No lib files embedded: {}", e);
}
}
}
}