use std::path::{Path, PathBuf};
use std::process::Command;
use crate::deps::{Fetcher, LockedPackage, Lockfile, Manifest};
use crate::diag::CompileError;
pub struct BuildContext {
manifest: Manifest,
lockfile: Lockfile,
fetcher: Fetcher,
project_root: PathBuf,
}
#[derive(Debug)]
pub enum BuildError {
NoManifest,
Io(String),
ManifestError(String),
FetchError(String),
CompileError(CompileError),
CacheError,
CcError(String),
RuntimeError(i32),
}
impl std::fmt::Display for BuildError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BuildError::NoManifest => {
write!(f, "no fastc.toml found in current directory or parents")
}
BuildError::Io(msg) => write!(f, "IO error: {}", msg),
BuildError::ManifestError(msg) => write!(f, "manifest error: {}", msg),
BuildError::FetchError(msg) => write!(f, "fetch error: {}", msg),
BuildError::CompileError(e) => write!(f, "{}", e),
BuildError::CacheError => write!(f, "failed to initialize cache directory"),
BuildError::CcError(msg) => write!(f, "C compiler error: {}", msg),
BuildError::RuntimeError(code) => write!(f, "program exited with code {}", code),
}
}
}
impl std::error::Error for BuildError {}
impl From<CompileError> for BuildError {
fn from(e: CompileError) -> Self {
BuildError::CompileError(e)
}
}
impl BuildContext {
pub fn new(working_dir: &Path) -> Result<Self, BuildError> {
let manifest_path = Manifest::find(working_dir).ok_or(BuildError::NoManifest)?;
let project_root = manifest_path
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("."));
let manifest =
Manifest::load(&manifest_path).map_err(|e| BuildError::ManifestError(e.to_string()))?;
let lockfile_path = project_root.join("fastc.lock");
let lockfile = if lockfile_path.exists() {
Lockfile::load(&lockfile_path).map_err(|e| BuildError::Io(e.to_string()))?
} else {
Lockfile::new()
};
let fetcher = Fetcher::new().ok_or(BuildError::CacheError)?;
Ok(Self {
manifest,
lockfile,
fetcher,
project_root,
})
}
pub fn root(&self) -> &Path {
&self.project_root
}
pub fn name(&self) -> &str {
&self.manifest.package.name
}
pub fn fetch_dependencies(&mut self) -> Result<(), BuildError> {
if self.manifest.dependencies.is_empty() {
eprintln!("No dependencies to fetch.");
return Ok(());
}
for (name, dep) in &self.manifest.dependencies {
eprintln!("Fetching dependency: {}", name);
let path = self
.fetcher
.fetch(name, dep)
.map_err(|e| BuildError::FetchError(e.to_string()))?;
eprintln!(" Fetched to: {}", path.display());
let source = match dep {
crate::deps::Dependency::Git { git, version } => {
let mut source = format!("git+{}", git);
if let Some(tag) = &version.tag {
source.push_str(&format!("?tag={}", tag));
} else if let Some(branch) = &version.branch {
source.push_str(&format!("?branch={}", branch));
} else if let Some(rev) = &version.rev {
source.push_str(&format!("?rev={}", rev));
}
source
}
crate::deps::Dependency::Path { path } => format!("path+{}", path),
};
self.lockfile.add_package(LockedPackage {
name: name.clone(),
version: "0.0.0".to_string(), source,
resolved: None, dependencies: vec![],
});
}
let lockfile_path = self.project_root.join("fastc.lock");
self.lockfile
.save(&lockfile_path)
.map_err(|e| BuildError::Io(e.to_string()))?;
eprintln!("Updated fastc.lock");
Ok(())
}
pub fn compile(&self, output_dir: &Path, _release: bool) -> Result<PathBuf, BuildError> {
let src_dir = self.project_root.join("src");
let main_file = src_dir.join("main.fc");
let lib_file = src_dir.join("lib.fc");
let source_file = if main_file.exists() {
main_file
} else if lib_file.exists() {
lib_file
} else {
return Err(BuildError::Io(
"no src/main.fc or src/lib.fc found".to_string(),
));
};
eprintln!("Compiling: {}", source_file.display());
let source =
std::fs::read_to_string(&source_file).map_err(|e| BuildError::Io(e.to_string()))?;
let filename = source_file.display().to_string();
let (c_code, header) = crate::compile_with_options(&source, &filename, true)?;
std::fs::create_dir_all(output_dir).map_err(|e| BuildError::Io(e.to_string()))?;
let base_name = source_file.file_stem().unwrap().to_string_lossy();
let c_file = output_dir.join(format!("{}.c", base_name));
let h_file = output_dir.join(format!("{}.h", base_name));
std::fs::write(&c_file, &c_code).map_err(|e| BuildError::Io(e.to_string()))?;
eprintln!(" Wrote: {}", c_file.display());
if let Some(h) = header {
std::fs::write(&h_file, &h).map_err(|e| BuildError::Io(e.to_string()))?;
eprintln!(" Wrote: {}", h_file.display());
}
eprintln!("FastC compilation complete.");
Ok(c_file)
}
pub fn cc_compile(
&self,
c_file: &Path,
compiler: &str,
cflags: &[&str],
release: bool,
) -> Result<PathBuf, BuildError> {
let output_dir = c_file.parent().unwrap_or(Path::new("."));
let base_name = c_file.file_stem().unwrap().to_string_lossy();
#[cfg(windows)]
let exe_name = format!("{}.exe", base_name);
#[cfg(not(windows))]
let exe_name = base_name.to_string();
let executable = output_dir.join(&exe_name);
eprintln!("Compiling C code with {}...", compiler);
let mut args: Vec<&str> = vec![c_file.to_str().unwrap(), "-o", executable.to_str().unwrap()];
if let Some(runtime_path) = Self::find_runtime_include() {
args.push("-I");
let leaked: &'static str = Box::leak(runtime_path.into_boxed_str());
args.push(leaked);
}
if release {
args.push("-O2");
args.push("-DNDEBUG");
} else {
args.push("-g");
args.push("-O0");
}
args.extend(cflags);
args.push("-lm");
eprintln!(" {} {}", compiler, args.join(" "));
let output = Command::new(compiler)
.args(&args)
.output()
.map_err(|e| BuildError::CcError(format!("failed to run {}: {}", compiler, e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
return Err(BuildError::CcError(format!(
"{} failed:\n{}{}",
compiler, stdout, stderr
)));
}
eprintln!(" Wrote: {}", executable.display());
eprintln!("C compilation complete.");
Ok(executable)
}
pub fn run(&self, executable: &Path, args: &[String]) -> Result<(), BuildError> {
eprintln!("Running: {} {}", executable.display(), args.join(" "));
eprintln!("---");
let status = Command::new(executable)
.args(args)
.status()
.map_err(|e| BuildError::Io(format!("failed to run {}: {}", executable.display(), e)))?;
eprintln!("---");
if !status.success() {
let code = status.code().unwrap_or(-1);
if code != 0 {
eprintln!("Program exited with code: {}", code);
}
}
Ok(())
}
fn find_runtime_include() -> Option<String> {
if let Ok(path) = std::env::var("FASTC_RUNTIME") {
if Path::new(&path).exists() {
return Some(path);
}
}
if let Ok(exe_path) = std::env::current_exe() {
if let Some(parent) = exe_path.parent() {
let candidates = [
parent.join("../../../runtime"), parent.join("../../runtime"), parent.join("../runtime"), parent.join("runtime"), parent.join("../share/fastc/runtime"), ];
for candidate in &candidates {
if candidate.join("fastc_runtime.h").exists() {
return candidate.canonicalize().ok()?.to_str().map(String::from);
}
}
}
}
let common_paths = [
"/usr/local/share/fastc/runtime",
"/usr/share/fastc/runtime",
];
for path in &common_paths {
let p = Path::new(path);
if p.join("fastc_runtime.h").exists() {
return Some(path.to_string());
}
}
None
}
pub fn include_paths(&self) -> Vec<PathBuf> {
let mut paths = Vec::new();
for (name, dep) in &self.manifest.dependencies {
if let crate::deps::Dependency::Git { git, version } = dep {
let version_str = if let Some(tag) = &version.tag {
format!("tag-{}", tag)
} else if let Some(branch) = &version.branch {
format!("branch-{}", branch)
} else if let Some(rev) = &version.rev {
format!("rev-{}", rev)
} else {
"default".to_string()
};
let dep_path = self.fetcher.cache().dep_path(name, git, &version_str);
if dep_path.exists() {
let src_path = dep_path.join("src");
if src_path.exists() {
paths.push(src_path);
} else {
paths.push(dep_path);
}
}
}
}
paths
}
}