use crate::config::{BuildSection, CStandard, Language, LanguageStandard, ManifestConfig, PathsConfig, ProjectSection, ProjectType};
use crate::error::{BuildError, Result};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use crate::resolver::{self, SourceLocation};
use crate::toolchain::BuildOutput;
const GITIGNORE_TEMPLATE: &str = include_str!("../templates/.gitignore");
const MAIN_C_TEMPLATE: &str = include_str!("../templates/main.c");
const LIB_C_TEMPLATE: &str = include_str!("../templates/lib.c");
const LIB_H_TEMPLATE: &str = include_str!("../templates/lib.h");
const MAIN_CPP_TEMPLATE: &str = include_str!("../templates/main.cpp");
const LIB_CPP_TEMPLATE: &str = include_str!("../templates/lib.cpp");
const LIB_HPP_TEMPLATE: &str = include_str!("../templates/lib.hpp");
pub struct Project {
pub root: PathBuf,
pub config: ManifestConfig,
pub src_dir: PathBuf,
pub build_dir: PathBuf,
pub install_dir: PathBuf,
pub resolved_deps: Vec<(String, BuildOutput)>,
}
impl Project {
pub fn load(project_dir: &Path) -> Result<Self> {
let config = ManifestConfig::load(project_dir)?;
let src_dir_name = config.paths.src_dir.as_deref().unwrap_or("src");
let src_dir = project_dir.join(src_dir_name);
Ok(Self {
root: project_dir.to_path_buf(),
src_dir,
build_dir: project_dir.join("target"),
install_dir: project_dir.join("target/deps"),
config,
resolved_deps: Vec::new(),
})
}
pub fn init(
name: &str,
project_type: ProjectType,
language_standard: Option<LanguageStandard>,
) -> Result<()> {
if name.is_empty()
|| name.contains('/')
|| name.contains('\\')
|| name == "."
|| name == ".."
{
return Err(BuildError::InvalidProjectName(name.to_string()));
}
let root = PathBuf::from(name);
if root.exists() {
return Err(BuildError::ProjectAlreadyExists(root));
}
std::fs::create_dir_all(root.join("src"))?;
std::fs::create_dir_all(root.join("include"))?;
let (language, c_standard, cpp_standard, ext, header_ext) = match language_standard {
Some(std) => (
std.language(),
std.c_standard(),
std.cpp_standard(),
if std.language() == Language::C { "c" } else { "cpp" },
if std.language() == Language::C { "h" } else { "hpp" },
),
None => (Language::C, Some(CStandard::default()), None, "c", "h"),
};
match project_type {
ProjectType::Binary => {
let template = match language {
Language::C => MAIN_C_TEMPLATE,
Language::Cpp => MAIN_CPP_TEMPLATE,
};
std::fs::write(root.join(format!("src/main.{ext}")), template)?;
}
ProjectType::StaticLibrary | ProjectType::SharedLibrary => {
let (lib_template, header_template) = match language {
Language::C => (LIB_C_TEMPLATE, LIB_H_TEMPLATE),
Language::Cpp => (LIB_CPP_TEMPLATE, LIB_HPP_TEMPLATE),
};
std::fs::write(root.join(format!("src/lib.{ext}")), lib_template)?;
std::fs::write(root.join(format!("include/lib.{header_ext}")), header_template)?;
}
}
let config = ManifestConfig {
project: Some(ProjectSection {
name: name.to_string(),
version: "0.1.0".to_string(),
authors: Vec::new(),
authors_email: Vec::new(),
description: None,
license: None,
project_type,
language,
c_standard,
cpp_standard,
output_name: None,
}),
build: Some(BuildSection {
compiler: Default::default(),
cflags: Vec::new(),
libs: Vec::new(),
linker_flags: Vec::new(),
}),
dependencies: BTreeMap::new(),
workspace: None,
profile: Default::default(),
extra_bins: Vec::new(),
paths: PathsConfig::default(),
};
std::fs::write(root.join("Smidr.toml"), config.to_toml_string()?)?;
std::fs::write(root.join(".gitignore"), GITIGNORE_TEMPLATE)?;
println!("Created project: {}", name);
Ok(())
}
fn collect_files(&self, dirs: &[&Path], extensions: &[&str]) -> Result<Vec<PathBuf>> {
let mut files = Vec::new();
for dir in dirs {
if !dir.exists() {
continue;
}
Self::collect_files_recursive(dir, extensions, &mut files)?;
}
Ok(files)
}
fn collect_files_recursive(dir: &Path, extensions: &[&str], out: &mut Vec<PathBuf>) -> Result<()> {
for entry in std::fs::read_dir(dir).map_err(BuildError::Io)? {
let entry = entry.map_err(BuildError::Io)?;
let path = entry.path();
if path.is_dir() {
Self::collect_files_recursive(&path, extensions, out)?;
} else if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
if extensions.contains(&ext) {
out.push(path);
}
}
}
Ok(())
}
pub fn source_files(&self) -> Result<Vec<PathBuf>> {
let custom_dirs: Vec<PathBuf> = self.config.paths.custom.values()
.map(|p| self.root.join(p))
.collect();
let mut dirs: Vec<&Path> = vec![self.src_dir.as_path()];
dirs.extend(custom_dirs.iter().map(|p| p.as_path()));
let sources = self.collect_files(&dirs, &["c", "cpp", "cc", "cxx"])?;
if sources.is_empty() {
return Err(BuildError::NoSourceFiles);
}
Ok(sources)
}
pub fn header_files(&self) -> Result<Vec<PathBuf>> {
let include_dir_name = self.config.paths.include.as_deref().unwrap_or("include");
let include_dir = self.root.join(include_dir_name);
let headers = self.collect_files(&[&include_dir], &["h", "hpp", "hh"])?;
if let Some(project_section) = &self.config.project {
if headers.is_empty() && project_section.project_type == ProjectType::StaticLibrary {
return Err(BuildError::NoHeaderFiles);
}
}
Ok(headers)
}
pub fn formattable_files(&self) -> Result<Vec<PathBuf>> {
let include_dir = self.root.join("include");
self.collect_files(
&[&self.src_dir, &include_dir],
&["c", "h", "cpp", "hpp", "cc", "hxx", "cxx", "hh"],
)
}
pub fn dep_prefix(&self, dep_name: &str) -> PathBuf {
self.install_dir.join(dep_name)
}
pub fn resolve_dependencies(&mut self, is_release: bool) -> Result<()> {
let dependencies = self.config.dependencies.clone();
for (name, spec) in &dependencies {
match resolver::resolve(name, spec, &self.root)? {
SourceLocation::System { .. } => {
let lib_info = resolver::resolve_system_lib(name)?;
self.resolved_deps.push((
name.to_string(),
BuildOutput {
include_dirs: lib_info.cflags.iter()
.filter_map(|f| f.strip_prefix("-I").map(PathBuf::from))
.collect(),
lib_dirs: Vec::new(),
libs: lib_info.libs.iter()
.filter_map(|f| f.strip_prefix("-l").map(String::from))
.collect(),
},
));
}
SourceLocation::Path(dep_root) => {
self.build_and_register_dep(name, spec, &dep_root, is_release)?;
}
SourceLocation::Git { url, tag, branch } => {
let dest = self.build_dir.join("deps-src").join(name);
let dep_root = resolver::resolve_git(name, &url, &tag, &branch, &dest)?;
self.build_and_register_dep(name, spec, &dep_root, is_release)?;
}
}
}
Ok(())
}
fn build_and_register_dep(
&mut self,
name: &str,
spec: &crate::config::DependencySpec,
dep_root: &Path,
is_release: bool,
) -> Result<()> {
if dep_root.join("Smidr.toml").exists() {
let dep_project = crate::project::Project::load(dep_root)?;
crate::builder::build_project(&dep_project, is_release, false, false)?;
let dep_include = dep_root.join("include");
let profile_dir = if is_release { "target/release" } else { "target/debug" };
let lib_filename = format!("lib{}.a", name);
let dep_original_lib_path = dep_root.join(profile_dir).join("bin").join(&lib_filename);
let deps_cache_dir = self.build_dir.join(profile_dir).join("deps");
std::fs::create_dir_all(&deps_cache_dir)?;
let dep_cached_lib_path = deps_cache_dir.join(&lib_filename);
let needs_update = match (
std::fs::metadata(&dep_cached_lib_path),
std::fs::metadata(&dep_original_lib_path),
) {
(Ok(cached), Ok(orig)) => cached.modified().unwrap() < orig.modified().unwrap(),
_ => true,
};
if needs_update {
std::fs::copy(&dep_original_lib_path, &dep_cached_lib_path)?;
}
self.resolved_deps.push((
name.to_string(),
BuildOutput {
include_dirs: vec![dep_include],
lib_dirs: vec![deps_cache_dir],
libs: vec![name.to_string()],
},
));
} else {
let build_system = match spec {
crate::config::DependencySpec::Detailed { build_system, .. } => build_system,
crate::config::DependencySpec::Version(_) => &crate::config::BuildSystemKind::Auto,
};
let builder = crate::toolchain::resolve_builder(name, build_system, dep_root, spec)?;
let output = builder.build(dep_root, &self.dep_prefix(name))?;
self.resolved_deps.push((name.to_string(), output));
}
Ok(())
}
pub fn add_dependency(&mut self, name: &str) -> Result<()> {
resolver::resolve_system_lib(name)?;
self.config.dependencies.insert(
name.to_string(),
crate::config::DependencySpec::Version("*".to_string()),
);
std::fs::write(self.root.join("Smidr.toml"), self.config.to_toml_string()?)?;
println!("Added dependency: {}", name);
Ok(())
}
pub fn remove_dependency(&mut self, name: &str) -> Result<()> {
self.config.dependencies.remove(name);
std::fs::write(self.root.join("Smidr.toml"), self.config.to_toml_string()?)?;
println!("Removed dependency: {}", name);
Ok(())
}
}