diff --git a/docs/pageforge.yaml b/docs/pageforge.yaml index 2432cc1..495c504 100644 --- a/docs/pageforge.yaml +++ b/docs/pageforge.yaml @@ -14,7 +14,7 @@ repo: branch: dev banner: - content: 💗 CodeForge 25.0.3 已经发布, 如果喜欢我们的软件,请点击这里支持我们 ❤️ + content: 💗 CodeForge 25.0.5 已经发布, 如果喜欢我们的软件,请点击这里支持我们 ❤️ feature: lucide: diff --git a/package.json b/package.json index f9cec88..f657beb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "codeforge", "private": true, - "version": "25.0.5", + "version": "26.0.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index fcb865c..b8bd35f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "CodeForge" -version = "25.0.5" +version = "26.0.0" dependencies = [ "async-trait", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index cb41e95..85ecbf4 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "CodeForge" -version = "25.0.5" +version = "26.0.0" description = "CodeForge 是一款轻量级、高性能的桌面代码执行器,专为开发者、学生和编程爱好者设计。" authors = ["devlive-community"] edition = "2024" diff --git a/src-tauri/src/env_providers/mod.rs b/src-tauri/src/env_providers/mod.rs index 6f8fd61..fcdf101 100644 --- a/src-tauri/src/env_providers/mod.rs +++ b/src-tauri/src/env_providers/mod.rs @@ -1,8 +1,10 @@ pub mod clojure; pub mod go; pub mod metadata; +pub mod rust; pub mod scala; pub use clojure::ClojureEnvironmentProvider; pub use go::GoEnvironmentProvider; +pub use rust::RustEnvironmentProvider; pub use scala::ScalaEnvironmentProvider; diff --git a/src-tauri/src/env_providers/rust.rs b/src-tauri/src/env_providers/rust.rs new file mode 100644 index 0000000..b07de6c --- /dev/null +++ b/src-tauri/src/env_providers/rust.rs @@ -0,0 +1,597 @@ +use super::metadata::{fetch_metadata_from_cdn, is_cdn_enabled, is_fallback_enabled}; +use crate::env_manager::{DownloadStatus, EnvironmentProvider, EnvironmentVersion}; +use async_trait::async_trait; +use futures_util::StreamExt; +use log::{error, info, warn}; +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; +use std::time::{Duration, SystemTime}; +use tauri::AppHandle; + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct RustRelease { + version: String, + date: String, + download_url: String, +} + +#[derive(Debug, Deserialize, Serialize)] +struct CachedReleases { + releases: Vec, + cached_at: SystemTime, +} + +pub struct RustEnvironmentProvider { + install_dir: PathBuf, + cache_file: PathBuf, +} + +impl RustEnvironmentProvider { + pub fn new() -> Self { + let install_dir = Self::get_default_install_dir(); + let cache_file = install_dir.join("releases_cache.json"); + + if let Err(e) = std::fs::create_dir_all(&install_dir) { + error!("创建 Rust 安装目录失败: {}", e); + } + + Self { + install_dir, + cache_file, + } + } + + fn get_default_install_dir() -> PathBuf { + let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); + home_dir.join(".codeforge").join("plugins").join("rust") + } + + fn read_cache(&self) -> Option> { + if !self.cache_file.exists() { + return None; + } + + match std::fs::read_to_string(&self.cache_file) { + Ok(content) => match serde_json::from_str::(&content) { + Ok(cached) => { + if cached.releases.is_empty() { + warn!("缓存的版本列表为空,将重新获取"); + return None; + } + + if let Ok(elapsed) = SystemTime::now().duration_since(cached.cached_at) { + if elapsed < Duration::from_secs(3600) { + info!("使用缓存的 Rust 版本列表(缓存时间: {:?})", elapsed); + return Some(cached.releases); + } else { + info!("缓存已过期({:?}),将重新获取", elapsed); + } + } + } + Err(e) => { + warn!("解析缓存文件失败: {}", e); + } + }, + Err(e) => { + warn!("读取缓存文件失败: {}", e); + } + } + + None + } + + fn write_cache(&self, releases: &[RustRelease]) { + let cached = CachedReleases { + releases: releases.to_vec(), + cached_at: SystemTime::now(), + }; + + match serde_json::to_string_pretty(&cached) { + Ok(content) => { + if let Err(e) = std::fs::write(&self.cache_file, content) { + error!("写入缓存文件失败: {}", e); + } + } + Err(e) => { + error!("序列化缓存数据失败: {}", e); + } + } + } + + async fn fetch_rust_releases(&self) -> Result, String> { + if let Some(cached_releases) = self.read_cache() { + return Ok(cached_releases); + } + + info!("从官方 API 获取 Rust 版本列表"); + + let client = reqwest::Client::builder() + .user_agent("CodeForge") + .timeout(Duration::from_secs(30)) + .build() + .map_err(|e| format!("创建 HTTP 客户端失败: {}", e))?; + + let platform_target = Self::get_platform_target()?; + let mut all_releases = Vec::new(); + + let channels = vec!["stable", "beta", "nightly"]; + + for channel in channels { + let url = format!( + "https://static.rust-lang.org/dist/channel-rust-{}.toml", + channel + ); + + let response = match client.get(&url).send().await { + Ok(resp) if resp.status().is_success() => resp, + _ => continue, + }; + + let content = match response.text().await { + Ok(c) => c, + Err(_) => continue, + }; + + if let Some(release) = Self::parse_channel_toml(&content, &platform_target, channel) { + all_releases.push(release); + } + } + + if all_releases.is_empty() { + return Err(format!("未找到 {} 平台的 Rust 版本信息", platform_target)); + } + + self.write_cache(&all_releases); + Ok(all_releases) + } + + fn parse_channel_toml( + content: &str, + platform_target: &str, + channel: &str, + ) -> Option { + let mut date_str = String::new(); + let mut version_str = String::new(); + let mut download_url = String::new(); + let mut in_rust_section = false; + let mut in_target_section = false; + + for line in content.lines() { + let trimmed = line.trim(); + + if trimmed.starts_with("date = ") { + if let Some(date) = trimmed.split('"').nth(1) { + date_str = date.to_string(); + } + } else if trimmed == "[pkg.rust]" { + in_rust_section = true; + in_target_section = false; + } else if in_rust_section && trimmed.starts_with("version = ") { + if let Some(version) = trimmed.split('"').nth(1) { + version_str = version.split_whitespace().next().unwrap_or("").to_string(); + } + } else if in_rust_section && trimmed == format!("[pkg.rust.target.{}]", platform_target) + { + in_target_section = true; + } else if in_target_section && trimmed.starts_with("url = ") { + if let Some(url) = trimmed.split('"').nth(1) { + download_url = url.to_string(); + } + break; + } else if trimmed.starts_with("[pkg.") && !trimmed.starts_with("[pkg.rust") { + in_rust_section = false; + in_target_section = false; + } + } + + if !version_str.is_empty() && !download_url.is_empty() { + let display_version = if channel == "stable" { + version_str.clone() + } else { + format!("{} ({})", version_str, channel) + }; + + Some(RustRelease { + version: display_version, + date: date_str, + download_url, + }) + } else { + None + } + } + + fn get_platform_target() -> Result { + let (arch, os) = Self::get_platform_info()?; + Ok(format!("{}-{}", arch, os)) + } + + fn get_platform_info() -> Result<(&'static str, &'static str), String> { + let os = if cfg!(target_os = "windows") { + "pc-windows-msvc" + } else if cfg!(target_os = "macos") { + "apple-darwin" + } else if cfg!(target_os = "linux") { + "unknown-linux-gnu" + } else { + return Err("不支持的操作系统".to_string()); + }; + + let arch = if cfg!(target_arch = "x86_64") { + "x86_64" + } else if cfg!(target_arch = "aarch64") { + "aarch64" + } else { + return Err("不支持的架构".to_string()); + }; + + Ok((arch, os)) + } + + async fn get_download_url(&self, version: &str) -> Result { + let releases = self.fetch_rust_releases().await?; + + releases + .iter() + .find(|r| r.version == version) + .map(|r| r.download_url.clone()) + .ok_or_else(|| format!("未找到版本 {} 的下载地址", version)) + } + + fn get_version_install_path(&self, version: &str) -> PathBuf { + self.install_dir.join(version) + } + + fn is_version_installed(&self, version: &str) -> bool { + let install_path = self.get_version_install_path(version); + install_path.join("rustc").join("bin").exists() || install_path.join("bin").exists() + } + + async fn download_file( + &self, + url: &str, + dest_path: &Path, + app_handle: &AppHandle, + version: &str, + ) -> Result<(), String> { + info!("开始下载: {}", url); + + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(600)) + .build() + .map_err(|e| format!("创建 HTTP 客户端失败: {}", e))?; + + let response = client + .get(url) + .send() + .await + .map_err(|e| format!("下载失败: {}", e))?; + + if !response.status().is_success() { + return Err(format!("HTTP 请求失败: {}", response.status())); + } + + let total_size = response.content_length().unwrap_or(0); + + if let Some(parent) = dest_path.parent() { + std::fs::create_dir_all(parent).map_err(|e| format!("创建目录失败: {}", e))?; + } + + let mut file = + std::fs::File::create(dest_path).map_err(|e| format!("创建文件失败: {}", e))?; + let mut downloaded: u64 = 0; + let mut stream = response.bytes_stream(); + + crate::env_manager::emit_download_progress( + app_handle, + "rust", + version, + 0, + total_size, + DownloadStatus::Downloading, + ); + + while let Some(item) = stream.next().await { + let chunk = item.map_err(|e| format!("下载数据失败: {}", e))?; + std::io::Write::write_all(&mut file, &chunk) + .map_err(|e| format!("写入文件失败: {}", e))?; + downloaded += chunk.len() as u64; + + crate::env_manager::emit_download_progress( + app_handle, + "rust", + version, + downloaded, + total_size, + DownloadStatus::Downloading, + ); + } + + info!("下载完成: {}", dest_path.display()); + Ok(()) + } + + async fn extract_archive(&self, archive_path: &Path, extract_to: &Path) -> Result<(), String> { + info!( + "解压文件: {} 到 {}", + archive_path.display(), + extract_to.display() + ); + + std::fs::create_dir_all(extract_to).map_err(|e| format!("创建解压目录失败: {}", e))?; + + let tar_gz = + std::fs::File::open(archive_path).map_err(|e| format!("打开压缩文件失败: {}", e))?; + let tar = flate2::read::GzDecoder::new(tar_gz); + let mut archive = tar::Archive::new(tar); + archive + .unpack(extract_to) + .map_err(|e| format!("解压文件失败: {}", e))?; + + info!("解压完成"); + Ok(()) + } + + async fn update_plugin_config( + &self, + version: &str, + app_handle: AppHandle, + ) -> Result<(), String> { + use crate::config::{get_app_config_internal, update_app_config}; + + let mut config = get_app_config_internal().map_err(|e| format!("获取配置失败: {}", e))?; + + let install_path = self.get_version_install_path(version); + + if let Some(plugins) = config.plugins.as_mut() { + if let Some(rust_plugin) = plugins.iter_mut().find(|p| p.language == "rust") { + rust_plugin.execute_home = Some(install_path.to_string_lossy().to_string()); + + let rustc_bin = install_path.join("rustc").join("bin"); + let has_rustc_dir = rustc_bin.exists(); + + let run_command = if has_rustc_dir { + "rustc/bin/rustc $filename -o /tmp/main && /tmp/main".to_string() + } else { + "bin/rustc $filename -o /tmp/main && /tmp/main".to_string() + }; + + rust_plugin.run_command = Some(run_command); + + info!( + "已更新 Rust 插件配置: execute_home={}, run_command={}", + install_path.display(), + rust_plugin.run_command.as_ref().unwrap() + ); + } + } + + update_app_config(config, app_handle) + .await + .map_err(|e| format!("保存配置失败: {}", e))?; + + info!("成功更新 Rust 配置"); + Ok(()) + } +} + +#[async_trait] +impl EnvironmentProvider for RustEnvironmentProvider { + fn get_language(&self) -> &'static str { + "rust" + } + + async fn fetch_available_versions(&self) -> Result, String> { + let releases = self.fetch_rust_releases().await?; + + Ok(releases + .into_iter() + .map(|release| { + let is_installed = self.is_version_installed(&release.version); + let install_path = if is_installed { + Some( + self.get_version_install_path(&release.version) + .to_string_lossy() + .to_string(), + ) + } else { + None + }; + + EnvironmentVersion { + version: release.version.clone(), + download_url: release.download_url.clone(), + fallback_url: None, + install_path, + is_installed, + size: None, + release_date: Some(release.date), + } + }) + .collect()) + } + + async fn get_installed_versions(&self) -> Result, String> { + let mut installed = Vec::new(); + + if !self.install_dir.exists() { + return Ok(installed); + } + + let entries = + std::fs::read_dir(&self.install_dir).map_err(|e| format!("读取安装目录失败: {}", e))?; + + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + if let Some(version) = path.file_name().and_then(|n| n.to_str()) { + if version != "releases_cache.json" && self.is_version_installed(version) { + let download_url = self.get_download_url(version).await.unwrap_or_default(); + installed.push(EnvironmentVersion { + version: version.to_string(), + download_url, + fallback_url: None, + install_path: Some(path.to_string_lossy().to_string()), + is_installed: true, + size: None, + release_date: None, + }); + } + } + } + } + + Ok(installed) + } + + async fn download_and_install( + &self, + version: &str, + app_handle: AppHandle, + ) -> Result { + if self.is_version_installed(version) { + return Err(format!("版本 {} 已安装", version)); + } + + crate::env_manager::emit_download_progress( + &app_handle, + "rust", + version, + 0, + 100, + DownloadStatus::Downloading, + ); + + let download_url = self.get_download_url(version).await?; + let temp_file = self.install_dir.join(format!("rust-{}.tar.gz", version)); + + let should_download = if is_cdn_enabled() { + let metadata = fetch_metadata_from_cdn("rust").await; + if let Ok(meta) = metadata { + if let Some(release) = meta.releases.iter().find(|r| r.version == version) { + info!("使用 CDN 下载 Rust {}", version); + self.download_file(&release.download_url, &temp_file, &app_handle, version) + .await + } else if is_fallback_enabled() { + info!("CDN 中未找到版本,使用官方源"); + self.download_file(&download_url, &temp_file, &app_handle, version) + .await + } else { + Err("CDN 中未找到该版本且未启用回退".to_string()) + } + } else if is_fallback_enabled() { + info!("获取 CDN 元数据失败,使用官方源"); + self.download_file(&download_url, &temp_file, &app_handle, version) + .await + } else { + Err("获取 CDN 元数据失败且未启用回退".to_string()) + } + } else { + self.download_file(&download_url, &temp_file, &app_handle, version) + .await + }; + + should_download?; + + crate::env_manager::emit_download_progress( + &app_handle, + "rust", + version, + 0, + 100, + DownloadStatus::Extracting, + ); + + let temp_extract_dir = self.install_dir.join(format!("temp_{}", version)); + self.extract_archive(&temp_file, &temp_extract_dir).await?; + + let install_path = self.get_version_install_path(version); + + let extracted_dirs: Vec<_> = std::fs::read_dir(&temp_extract_dir) + .map_err(|e| format!("读取临时目录失败: {}", e))? + .filter_map(|e| e.ok()) + .filter(|e| e.path().is_dir()) + .collect(); + + if let Some(rust_dir) = extracted_dirs.first() { + std::fs::rename(rust_dir.path(), &install_path) + .map_err(|e| format!("移动安装目录失败: {}", e))?; + } else { + return Err("未找到 Rust 安装目录".to_string()); + } + + std::fs::remove_dir_all(&temp_extract_dir).ok(); + std::fs::remove_file(&temp_file).ok(); + + self.update_plugin_config(version, app_handle.clone()) + .await?; + + crate::env_manager::emit_download_progress( + &app_handle, + "rust", + version, + 100, + 100, + DownloadStatus::Completed, + ); + + Ok(install_path.to_string_lossy().to_string()) + } + + async fn switch_version(&self, version: &str, app_handle: AppHandle) -> Result<(), String> { + if !self.is_version_installed(version) { + return Err(format!("版本 {} 未安装", version)); + } + + self.update_plugin_config(version, app_handle).await?; + info!("已切换到 Rust {}", version); + Ok(()) + } + + async fn get_current_version(&self) -> Result, String> { + use crate::config::get_app_config_internal; + + let config = get_app_config_internal().map_err(|e| format!("获取配置失败: {}", e))?; + + if let Some(plugins) = config.plugins { + if let Some(rust_plugin) = plugins.iter().find(|p| p.language == "rust") { + if let Some(ref execute_home) = rust_plugin.execute_home { + let path = PathBuf::from(execute_home); + + if let Ok(relative) = path.strip_prefix(&self.install_dir) { + if let Some(version_component) = relative.components().next() { + if let Some(version) = version_component.as_os_str().to_str() { + info!("当前 Rust 版本: {}", version); + return Ok(Some(version.to_string())); + } + } + } + + if let Some(version) = path.file_name().and_then(|n| n.to_str()) { + info!("当前 Rust 版本: {}", version); + return Ok(Some(version.to_string())); + } + } + } + } + + Ok(None) + } + + fn get_install_dir(&self) -> PathBuf { + self.install_dir.clone() + } + + async fn uninstall_version(&self, version: &str) -> Result<(), String> { + let install_path = self.get_version_install_path(version); + + if !install_path.exists() { + return Err(format!("版本 {} 未安装", version)); + } + + std::fs::remove_dir_all(&install_path).map_err(|e| format!("删除安装目录失败: {}", e))?; + + info!("已卸载 Rust {}", version); + Ok(()) + } +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index fd78ded..d480735 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -25,7 +25,8 @@ use crate::env_commands::{ }; use crate::env_manager::EnvironmentManager; use crate::env_providers::{ - ClojureEnvironmentProvider, GoEnvironmentProvider, ScalaEnvironmentProvider, + ClojureEnvironmentProvider, GoEnvironmentProvider, RustEnvironmentProvider, + ScalaEnvironmentProvider, }; use crate::execution::{ ExecutionHistory, PluginManagerState as ExecutionPluginManagerState, clear_execution_history, @@ -51,6 +52,7 @@ fn main() { let mut env_manager = EnvironmentManager::new(); env_manager.register_provider(Box::new(ClojureEnvironmentProvider::new())); env_manager.register_provider(Box::new(GoEnvironmentProvider::new())); + env_manager.register_provider(Box::new(RustEnvironmentProvider::new())); env_manager.register_provider(Box::new(ScalaEnvironmentProvider::new())); tauri::Builder::default() diff --git a/src-tauri/src/plugins/rust.rs b/src-tauri/src/plugins/rust.rs index 3898a93..512932b 100644 --- a/src-tauri/src/plugins/rust.rs +++ b/src-tauri/src/plugins/rust.rs @@ -27,20 +27,30 @@ impl LanguagePlugin for RustPlugin { } fn get_path_command(&self) -> String { - "rustc --print sysroot".to_string() - } + if let Some(execute_home) = self.get_execute_home() { + let rustc_bin = std::path::Path::new(&execute_home) + .join("rustc") + .join("bin"); - fn get_execute_args(&self, file_path: &str) -> Vec { - let cmd = if self.get_execute_home().is_some() { - format!("./rustc {} -o ./main && ./main", file_path) + if rustc_bin.exists() { + format!("{}/rustc --print sysroot", rustc_bin.display()) + } else { + let bin_dir = std::path::Path::new(&execute_home).join("bin"); + format!("{}/rustc --print sysroot", bin_dir.display()) + } } else { - format!( - "export PATH=$PATH:$HOME/.cargo/bin && rustc {} -o /tmp/main && /tmp/main", - file_path - ) - }; + "rustc --print sysroot".to_string() + } + } - vec!["-c".to_string(), cmd] + fn get_execute_args(&self, file_path: &str) -> Vec { + if let Some(config) = self.get_config() { + if let Some(run_cmd) = &config.run_command { + let full_cmd = run_cmd.replace("$filename", file_path); + return vec!["-c".to_string(), full_cmd]; + } + } + vec![file_path.to_string()] } fn get_default_config(&self) -> PluginConfig { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 921429e..dc56685 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "CodeForge", - "version": "25.0.5", + "version": "26.0.0", "identifier": "org.devlive.codeforge", "build": { "beforeDevCommand": "pnpm dev",