use crate::infra::{FileSystem, RealFileSystem};
use std::path::{Path, PathBuf};
use thiserror::Error;
use toml_edit::{value, Array, DocumentMut, Item, Table};
#[derive(Error, Debug)]
pub enum BuildStdError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Failed to parse TOML: {0}")]
TomlParse(#[from] toml_edit::TomlError),
#[error("Invalid config.toml structure: {0}")]
InvalidStructure(String),
#[error("Build-std requires nightly Rust toolchain (detected stable/beta)")]
NightlyRequired,
}
#[derive(Debug, Clone)]
pub struct BuildStdConfig {
pub enabled: bool,
pub std_components: Vec<String>,
pub features: Vec<String>,
pub target: Option<String>,
pub rustflags: Vec<String>,
}
impl Default for BuildStdConfig {
fn default() -> Self {
Self {
enabled: false, std_components: vec![
"std".to_string(),
"panic_abort".to_string(),
"core".to_string(),
"alloc".to_string(),
],
features: vec!["panic_immediate_abort".to_string()],
target: None, rustflags: Vec::new(), }
}
}
impl BuildStdConfig {
pub fn with_ssr(target: String) -> Self {
Self {
enabled: true,
target: Some(target),
rustflags: vec!["--cfg=has_std".to_string()],
..Default::default()
}
}
pub fn minimal() -> Self {
Self {
enabled: true,
..Default::default()
}
}
}
pub struct BuildStdOptimizer<FS: FileSystem = RealFileSystem> {
project_root: PathBuf,
fs: FS,
}
impl BuildStdOptimizer<RealFileSystem> {
pub fn new(project_root: &Path) -> Self {
Self::with_fs(project_root, RealFileSystem)
}
}
impl<FS: FileSystem> BuildStdOptimizer<FS> {
pub fn with_fs(project_root: &Path, fs: FS) -> Self {
Self {
project_root: project_root.to_path_buf(),
fs,
}
}
fn config_path(&self) -> PathBuf {
self.project_root.join(".cargo").join("config.toml")
}
pub fn apply_build_std(
&self,
config: &BuildStdConfig,
dry_run: bool,
) -> Result<Vec<String>, BuildStdError> {
if !config.enabled {
return Ok(Vec::new());
}
let cargo_dir = self.project_root.join(".cargo");
let config_path = self.config_path();
if !dry_run && !cargo_dir.exists() {
self.fs.create_dir_all(&cargo_dir)?;
}
let content = self
.fs
.read_to_string(&config_path)
.unwrap_or_else(|_| String::new());
let mut doc = if content.is_empty() {
DocumentMut::new()
} else {
content.parse::<DocumentMut>()?
};
let mut changes = Vec::new();
self.apply_unstable_section(&mut doc, config, &mut changes)?;
if config.target.is_some() || !config.rustflags.is_empty() {
self.apply_build_section(&mut doc, config, &mut changes)?;
}
if !dry_run && !changes.is_empty() {
self.fs.write(&config_path, doc.to_string())?;
}
Ok(changes)
}
fn apply_unstable_section(
&self,
doc: &mut DocumentMut,
config: &BuildStdConfig,
changes: &mut Vec<String>,
) -> Result<(), BuildStdError> {
let unstable = doc
.entry("unstable")
.or_insert(Item::Table(Table::new()))
.as_table_mut()
.ok_or_else(|| {
BuildStdError::InvalidStructure("unstable is not a table".to_string())
})?;
if !unstable.contains_key("build-std") {
let mut arr = Array::new();
for component in &config.std_components {
arr.push(component.as_str());
}
unstable["build-std"] = value(arr);
changes.push(format!(
"Set build-std = {:?} (10-20% reduction)",
config.std_components
));
}
if !config.features.is_empty() && !unstable.contains_key("build-std-features") {
let mut arr = Array::new();
for feature in &config.features {
arr.push(feature.as_str());
}
unstable["build-std-features"] = value(arr);
changes.push(format!(
"Set build-std-features = {:?} (smaller panic handler)",
config.features
));
}
Ok(())
}
fn apply_build_section(
&self,
doc: &mut DocumentMut,
config: &BuildStdConfig,
changes: &mut Vec<String>,
) -> Result<(), BuildStdError> {
let build = doc
.entry("build")
.or_insert(Item::Table(Table::new()))
.as_table_mut()
.ok_or_else(|| BuildStdError::InvalidStructure("build is not a table".to_string()))?;
if let Some(ref target) = config.target {
if !build.contains_key("target") {
build["target"] = value(target.as_str());
changes.push(format!("Set target = \"{}\" (SSR support)", target));
}
}
if !config.rustflags.is_empty() && !build.contains_key("rustflags") {
let mut arr = Array::new();
for flag in &config.rustflags {
arr.push(flag.as_str());
}
build["rustflags"] = value(arr);
changes.push(format!(
"Set rustflags = {:?} (SSR compatibility)",
config.rustflags
));
}
Ok(())
}
pub fn is_configured(&self) -> Result<bool, BuildStdError> {
let config_path = self.config_path();
if !config_path.exists() {
return Ok(false);
}
let content = self.fs.read_to_string(&config_path)?;
let doc = content.parse::<DocumentMut>()?;
if let Some(unstable) = doc.get("unstable").and_then(|i| i.as_table()) {
if unstable.contains_key("build-std") {
return Ok(true);
}
}
Ok(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_build_std_config_default_has_expected_values() {
let config = BuildStdConfig::default();
assert!(!config.enabled); assert_eq!(config.std_components.len(), 4);
assert!(config.std_components.contains(&"std".to_string()));
assert!(config.std_components.contains(&"panic_abort".to_string()));
assert_eq!(config.features, vec!["panic_immediate_abort"]);
}
#[test]
fn test_build_std_config_with_ssr_includes_target_and_rustflags() {
let config = BuildStdConfig::with_ssr("x86_64-unknown-linux-gnu".to_string());
assert!(config.enabled);
assert_eq!(config.target, Some("x86_64-unknown-linux-gnu".to_string()));
assert_eq!(config.rustflags, vec!["--cfg=has_std"]);
}
#[test]
fn test_build_std_config_minimal_enables_feature() {
let config = BuildStdConfig::minimal();
assert!(config.enabled);
assert!(config.target.is_none());
assert!(config.rustflags.is_empty());
}
#[test]
fn test_optimizer_creates_config_toml_with_build_std() {
let temp_dir = TempDir::new().unwrap();
let optimizer = BuildStdOptimizer::new(temp_dir.path());
let config = BuildStdConfig {
enabled: true,
..Default::default()
};
let changes = optimizer.apply_build_std(&config, false).unwrap();
assert!(!changes.is_empty());
assert!(changes.iter().any(|c| c.contains("build-std")));
let config_path = temp_dir.path().join(".cargo").join("config.toml");
assert!(config_path.exists());
let content = fs::read_to_string(&config_path).unwrap();
assert!(content.contains("[unstable]"));
assert!(content.contains("build-std"));
assert!(content.contains("panic_immediate_abort"));
}
#[test]
fn test_optimizer_with_ssr_adds_build_section() {
let temp_dir = TempDir::new().unwrap();
let optimizer = BuildStdOptimizer::new(temp_dir.path());
let config = BuildStdConfig::with_ssr("x86_64-unknown-linux-gnu".to_string());
let changes = optimizer.apply_build_std(&config, false).unwrap();
assert!(changes.iter().any(|c| c.contains("target")));
assert!(changes.iter().any(|c| c.contains("rustflags")));
let config_path = temp_dir.path().join(".cargo").join("config.toml");
let content = fs::read_to_string(&config_path).unwrap();
assert!(content.contains("[build]"));
assert!(content.contains("x86_64-unknown-linux-gnu"));
assert!(content.contains("--cfg=has_std"));
}
#[test]
fn test_optimizer_preserves_existing_config() {
let temp_dir = TempDir::new().unwrap();
let cargo_dir = temp_dir.path().join(".cargo");
fs::create_dir_all(&cargo_dir).unwrap();
let config_path = cargo_dir.join("config.toml");
fs::write(
&config_path,
r#"
[some-existing]
setting = "value"
"#,
)
.unwrap();
let optimizer = BuildStdOptimizer::new(temp_dir.path());
let config = BuildStdConfig {
enabled: true,
..Default::default()
};
optimizer.apply_build_std(&config, false).unwrap();
let content = fs::read_to_string(&config_path).unwrap();
assert!(content.contains("[some-existing]"));
assert!(content.contains("setting = \"value\""));
assert!(content.contains("[unstable]"));
}
#[test]
fn test_optimizer_dry_run_does_not_modify_files() {
let temp_dir = TempDir::new().unwrap();
let optimizer = BuildStdOptimizer::new(temp_dir.path());
let config = BuildStdConfig {
enabled: true,
..Default::default()
};
let changes = optimizer.apply_build_std(&config, true).unwrap();
assert!(!changes.is_empty());
let config_path = temp_dir.path().join(".cargo").join("config.toml");
assert!(!config_path.exists());
}
#[test]
fn test_optimizer_is_configured_detects_existing_setup() {
let temp_dir = TempDir::new().unwrap();
let cargo_dir = temp_dir.path().join(".cargo");
fs::create_dir_all(&cargo_dir).unwrap();
let config_path = cargo_dir.join("config.toml");
fs::write(
&config_path,
r#"
[unstable]
build-std = ["std", "panic_abort"]
"#,
)
.unwrap();
let optimizer = BuildStdOptimizer::new(temp_dir.path());
assert!(optimizer.is_configured().unwrap());
}
#[test]
fn test_optimizer_is_configured_returns_false_when_missing() {
let temp_dir = TempDir::new().unwrap();
let optimizer = BuildStdOptimizer::new(temp_dir.path());
assert!(!optimizer.is_configured().unwrap());
}
#[test]
fn test_optimizer_disabled_config_returns_no_changes() {
let temp_dir = TempDir::new().unwrap();
let optimizer = BuildStdOptimizer::new(temp_dir.path());
let config = BuildStdConfig::default(); let changes = optimizer.apply_build_std(&config, false).unwrap();
assert!(changes.is_empty());
}
#[test]
fn test_invalid_toml_structure_returns_error() {
use std::fs;
let temp_dir = TempDir::new().unwrap();
let cargo_dir = temp_dir.path().join(".cargo");
fs::create_dir_all(&cargo_dir).unwrap();
let config_path = cargo_dir.join("config.toml");
fs::write(&config_path, "unstable = \"not a table\"").unwrap();
let optimizer = BuildStdOptimizer::new(temp_dir.path());
let config = BuildStdConfig {
enabled: true,
..Default::default()
};
let result = optimizer.apply_build_std(&config, false);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, BuildStdError::InvalidStructure(_)));
}
#[test]
fn test_toml_parse_error_returns_error() {
use std::fs;
let temp_dir = TempDir::new().unwrap();
let cargo_dir = temp_dir.path().join(".cargo");
fs::create_dir_all(&cargo_dir).unwrap();
let config_path = cargo_dir.join("config.toml");
fs::write(&config_path, "[unstable\ninvalid toml {{").unwrap();
let optimizer = BuildStdOptimizer::new(temp_dir.path());
let config = BuildStdConfig {
enabled: true,
..Default::default()
};
let result = optimizer.apply_build_std(&config, false);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, BuildStdError::TomlParse(_)));
}
#[test]
fn test_build_section_invalid_structure_returns_error() {
use std::fs;
let temp_dir = TempDir::new().unwrap();
let cargo_dir = temp_dir.path().join(".cargo");
fs::create_dir_all(&cargo_dir).unwrap();
let config_path = cargo_dir.join("config.toml");
fs::write(&config_path, "[build]\nbuild = \"not a table\"").unwrap();
let optimizer = BuildStdOptimizer::new(temp_dir.path());
let config = BuildStdConfig {
enabled: true,
target: Some("x86_64-unknown-linux-gnu".to_string()),
..Default::default()
};
let config_path = cargo_dir.join("config.toml");
fs::write(&config_path, "build = \"not a table\"").unwrap();
let result = optimizer.apply_build_std(&config, false);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, BuildStdError::InvalidStructure(_)));
}
#[test]
fn test_readonly_directory_returns_io_error() {
let temp_dir = TempDir::new().unwrap();
let cargo_dir = temp_dir.path().join(".cargo");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::create_dir_all(&cargo_dir).unwrap();
let mut perms = fs::metadata(&cargo_dir).unwrap().permissions();
perms.set_mode(0o444); fs::set_permissions(&cargo_dir, perms).unwrap();
let optimizer = BuildStdOptimizer::new(temp_dir.path());
let config = BuildStdConfig {
enabled: true,
..Default::default()
};
let result = optimizer.apply_build_std(&config, false);
let mut perms = fs::metadata(&cargo_dir).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&cargo_dir, perms).unwrap();
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), BuildStdError::Io(_)));
}
#[cfg(not(unix))]
{
}
}
}