Skip to main content

fastc/scaffold/
mod.rs

1//! Project scaffolding for FastC
2//!
3//! This module provides functionality to create new FastC projects
4//! with appropriate directory structure and build files.
5
6mod templates;
7
8use miette::{bail, IntoDiagnostic, Result};
9use std::fs;
10use std::path::Path;
11
12/// Type of project to create
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ProjectType {
15    /// A binary application with main.fc
16    Binary,
17    /// A library with lib.fc
18    Library,
19    /// An FFI wrapper library with header generation
20    FfiWrapper,
21}
22
23impl std::str::FromStr for ProjectType {
24    type Err = String;
25
26    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
27        match s.to_lowercase().as_str() {
28            "binary" | "bin" => Ok(ProjectType::Binary),
29            "library" | "lib" => Ok(ProjectType::Library),
30            "ffi-wrapper" | "ffi" => Ok(ProjectType::FfiWrapper),
31            _ => Err(format!(
32                "Unknown project type: '{}'. Use 'binary', 'library', or 'ffi-wrapper'.",
33                s
34            )),
35        }
36    }
37}
38
39impl std::fmt::Display for ProjectType {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        match self {
42            ProjectType::Binary => write!(f, "binary"),
43            ProjectType::Library => write!(f, "library"),
44            ProjectType::FfiWrapper => write!(f, "ffi-wrapper"),
45        }
46    }
47}
48
49/// Build system template to use
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum BuildTemplate {
52    /// GNU Make
53    Make,
54    /// CMake
55    CMake,
56    /// Meson
57    Meson,
58}
59
60impl std::str::FromStr for BuildTemplate {
61    type Err = String;
62
63    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
64        match s.to_lowercase().as_str() {
65            "make" => Ok(BuildTemplate::Make),
66            "cmake" => Ok(BuildTemplate::CMake),
67            "meson" => Ok(BuildTemplate::Meson),
68            _ => Err(format!(
69                "Unknown build template: '{}'. Use 'make', 'cmake', or 'meson'.",
70                s
71            )),
72        }
73    }
74}
75
76impl std::fmt::Display for BuildTemplate {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        match self {
79            BuildTemplate::Make => write!(f, "make"),
80            BuildTemplate::CMake => write!(f, "cmake"),
81            BuildTemplate::Meson => write!(f, "meson"),
82        }
83    }
84}
85
86/// Create a new FastC project
87pub fn create_project(
88    name: &str,
89    path: &Path,
90    project_type: ProjectType,
91    build_template: BuildTemplate,
92) -> Result<()> {
93    // Validate project name
94    if name.is_empty() {
95        bail!("Project name cannot be empty");
96    }
97    if !name
98        .chars()
99        .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
100    {
101        bail!("Project name must contain only alphanumeric characters, underscores, and hyphens");
102    }
103
104    // Create project directory
105    let project_dir = path.join(name);
106    if project_dir.exists() {
107        bail!("Directory '{}' already exists", project_dir.display());
108    }
109
110    fs::create_dir_all(&project_dir).into_diagnostic()?;
111
112    // Create subdirectories
113    fs::create_dir_all(project_dir.join("src")).into_diagnostic()?;
114
115    if project_type == ProjectType::FfiWrapper {
116        fs::create_dir_all(project_dir.join("include")).into_diagnostic()?;
117    }
118
119    // Write files
120    write_manifest(&project_dir, name, project_type)?;
121    write_source_files(&project_dir, name, project_type)?;
122    write_build_files(&project_dir, name, project_type, build_template)?;
123    write_gitignore(&project_dir)?;
124    write_readme(&project_dir, name, project_type, build_template)?;
125
126    eprintln!("Created {} project '{}' at {}", project_type, name, project_dir.display());
127    eprintln!();
128    eprintln!("To get started:");
129    eprintln!("  cd {}", name);
130    match build_template {
131        BuildTemplate::Make => eprintln!("  make"),
132        BuildTemplate::CMake => {
133            eprintln!("  mkdir build && cd build");
134            eprintln!("  cmake ..");
135            eprintln!("  make");
136        }
137        BuildTemplate::Meson => {
138            eprintln!("  meson setup build");
139            eprintln!("  meson compile -C build");
140        }
141    }
142
143    Ok(())
144}
145
146/// Initialize a FastC project in an existing directory
147pub fn init_project(
148    path: &Path,
149    project_type: ProjectType,
150    build_template: BuildTemplate,
151) -> Result<()> {
152    let name = path
153        .file_name()
154        .and_then(|n| n.to_str())
155        .unwrap_or("project");
156
157    // Check if already initialized
158    if path.join("fastc.toml").exists() {
159        bail!("Directory already contains a fastc.toml file");
160    }
161
162    // Create subdirectories if needed
163    let src_dir = path.join("src");
164    if !src_dir.exists() {
165        fs::create_dir_all(&src_dir).into_diagnostic()?;
166    }
167
168    if project_type == ProjectType::FfiWrapper {
169        let include_dir = path.join("include");
170        if !include_dir.exists() {
171            fs::create_dir_all(&include_dir).into_diagnostic()?;
172        }
173    }
174
175    // Write files (skip if they exist)
176    write_manifest(path, name, project_type)?;
177
178    let main_file = match project_type {
179        ProjectType::Binary => src_dir.join("main.fc"),
180        ProjectType::Library | ProjectType::FfiWrapper => src_dir.join("lib.fc"),
181    };
182    if !main_file.exists() {
183        write_source_files(path, name, project_type)?;
184    }
185
186    // Write build files
187    write_build_files(path, name, project_type, build_template)?;
188
189    // Write .gitignore if not present
190    if !path.join(".gitignore").exists() {
191        write_gitignore(path)?;
192    }
193
194    // Write README if not present
195    if !path.join("README.md").exists() {
196        write_readme(path, name, project_type, build_template)?;
197    }
198
199    eprintln!("Initialized {} project in {}", project_type, path.display());
200
201    Ok(())
202}
203
204fn write_manifest(project_dir: &Path, name: &str, project_type: ProjectType) -> Result<()> {
205    let content = templates::manifest(name, project_type);
206    fs::write(project_dir.join("fastc.toml"), content).into_diagnostic()
207}
208
209fn write_source_files(project_dir: &Path, name: &str, project_type: ProjectType) -> Result<()> {
210    let (filename, content) = match project_type {
211        ProjectType::Binary => ("main.fc", templates::main_fc(name)),
212        ProjectType::Library => ("lib.fc", templates::lib_fc(name)),
213        ProjectType::FfiWrapper => ("lib.fc", templates::ffi_lib_fc(name)),
214    };
215
216    fs::write(project_dir.join("src").join(filename), content).into_diagnostic()
217}
218
219fn write_build_files(
220    project_dir: &Path,
221    name: &str,
222    project_type: ProjectType,
223    build_template: BuildTemplate,
224) -> Result<()> {
225    match build_template {
226        BuildTemplate::Make => {
227            let content = templates::makefile(name, project_type);
228            fs::write(project_dir.join("Makefile"), content).into_diagnostic()
229        }
230        BuildTemplate::CMake => {
231            let content = templates::cmakelists(name, project_type);
232            fs::write(project_dir.join("CMakeLists.txt"), content).into_diagnostic()
233        }
234        BuildTemplate::Meson => {
235            let content = templates::meson_build(name, project_type);
236            fs::write(project_dir.join("meson.build"), content).into_diagnostic()
237        }
238    }
239}
240
241fn write_gitignore(project_dir: &Path) -> Result<()> {
242    let content = templates::gitignore();
243    fs::write(project_dir.join(".gitignore"), content).into_diagnostic()
244}
245
246fn write_readme(
247    project_dir: &Path,
248    name: &str,
249    project_type: ProjectType,
250    build_template: BuildTemplate,
251) -> Result<()> {
252    let content = templates::readme(name, project_type, build_template);
253    fs::write(project_dir.join("README.md"), content).into_diagnostic()
254}