use super::{
compile::{self, CompileEntry},
deps, fingerprint, link, lock, proj_parse,
proj_parse::{
BuildSystem, Dependency, Flags, Linkage, Output, Profile, Project,
ProjectType, Test,
},
toolchain,
ui::{StepStatus, Ui},
};
use miette::{IntoDiagnostic, Result};
use std::{
fs,
path::{Path, PathBuf},
};
pub struct BuildCtx<'a> {
pub project: &'a Project,
pub dir: PathBuf,
pub root: PathBuf,
pub profile: Option<&'a str>,
pub record_state: bool,
pub force: bool,
}
impl<'a> BuildCtx<'a> {
fn resolve(&self) -> Option<(&'a str, &'a Profile)> {
self.profile.and_then(|name| self.project.profile(name))
}
pub fn profile_name(&self) -> &str {
self.resolve().map_or("default", |(name, _)| name)
}
}
fn built_artifact(
project: &Project,
dir: &Path,
root: &Path,
profile_name: &str,
) -> PathBuf {
let scoped = link::output_scoped(project, dir, root);
if project.is_library() {
link::library_path(project, root, profile_name, scoped)
} else {
link::binary_path(project, root, profile_name, scoped)
}
}
pub fn build_ctx(ctx: &BuildCtx, ui: &Ui) -> Result<()> {
if let Some(name) = ctx.profile
&& ctx.resolve().is_none()
{
ui.println(
Some(&StepStatus::Info),
format!(
"{}: no `{name}` profile; building default",
ctx.project.name
),
)?;
}
let profile_name = ctx.profile_name();
let project = ctx.project.with_profile(ctx.resolve().map(|(_, p)| p));
let lock = lock::LockFile::load(ctx.dir.join("conjure.lock"))?;
let dep_commits: Vec<String> =
lock.entries().values().map(|e| e.commit.clone()).collect();
let mut file_cache = fingerprint::FileCache::load(&ctx.dir);
let fp = fingerprint::fingerprint(
&project,
&ctx.dir,
profile_name,
&dep_commits.iter().map(String::as_str).collect::<Vec<_>>(),
&mut file_cache,
)?;
file_cache.save(&ctx.dir);
let state_file = ctx.dir.join(".conjure/build/state.kdl");
let up_to_date = ctx.record_state
&& !ctx.force
&& fs::read_to_string(&state_file).is_ok_and(|s| s == fp)
&& built_artifact(&project, &ctx.dir, &ctx.root, profile_name).exists();
if up_to_date {
ui.println(Some(&StepStatus::Info), "Nothing to do")?;
return Ok(());
}
let dep_libs =
deps::build_deps(ui, &project, &ctx.dir, &ctx.root, ctx.profile)?;
let (pkg_cflags, pkg_libs) =
deps::resolve_pkg_config(&project, &ctx.dir, &ctx.root)?;
let (objects, entries): (Vec<PathBuf>, Vec<CompileEntry>) =
compile::compile_entries(
&ctx.dir,
&ctx.root,
&project,
profile_name,
pkg_cflags,
)?
.into_iter()
.unzip();
let compile = project
.compile
.as_ref()
.ok_or_else(|| miette::miette!("No compile section in conjure.kdl"))?;
let ld_flags = compile
.ld_flags
.as_ref()
.map(Flags::list)
.unwrap_or_default();
let threads = compile.threads.unwrap_or_else(|| {
std::thread::available_parallelism().map_or(1, |n| n.get())
});
let build_env = toolchain::resolve(compile, &project.language).env();
compile::compile(ui, &ctx.dir, entries, threads, build_env)?;
let inputs = link::LinkInputs {
objects: &objects,
libs: &dep_libs,
extra_libs: &pkg_libs,
ld_flags: &ld_flags,
compile,
};
link::produce(ui, &project, &ctx.dir, &ctx.root, profile_name, &inputs)?;
if ctx.record_state {
let _ = fs::create_dir_all(state_file.parent().unwrap());
let _ = fs::write(state_file, fp);
}
Ok(())
}
fn test_profile(p: Profile) -> Profile {
Profile {
ty: None,
link: None,
src: None,
tests: None,
..p.clone()
}
}
pub fn test_project(
parent: &Project,
name: &str,
test: &Test,
profile: Option<&str>,
) -> Result<Project> {
let src = test.src.clone().ok_or_else(|| {
miette::miette!(
help = "Test `{name}` must have a `src` specified",
"Test `{name}` has no `src`"
)
})?;
let mut compile = parent.compile.clone().unwrap_or_default();
compile.src = Some(src);
let mut dependencies = parent.dependencies.clone().unwrap_or_default();
dependencies.insert(
parent.name.clone(),
Dependency {
local: Some(".".into()),
build: Some(BuildSystem::Conjure(None)),
..Default::default()
},
);
let profiles = parent.profiles.as_ref().map(|profiles| {
let mut profiles = profiles.clone();
if let Some(active) = profile
&& let Some(p) = profiles.get(active).cloned()
{
profiles.insert(active.to_string(), test_profile(p));
}
profiles
});
let bin = parent
.output
.as_ref()
.and_then(|o| o.bin.clone())
.unwrap_or_else(|| "bin".into());
let output_bin = PathBuf::from(bin).join(name).display().to_string();
Ok(Project {
name: name.to_string(),
language: parent.language.clone(),
ty: ProjectType::Binary,
link: Linkage::Dynamic,
compile: Some(compile),
profiles,
dependencies: Some(dependencies),
output: Some(Output {
bin: Some(output_bin),
lib: None,
symlink_binaries: Some(false),
}),
..Default::default()
})
}
pub fn build(
project: &Project,
profile: Option<&str>,
force: bool,
siblings: bool,
) -> Result<()> {
let cwd = std::env::current_dir().into_diagnostic()?;
let ui = Ui::new();
build_ctx(
&BuildCtx {
project,
dir: cwd.clone(),
root: cwd.clone(),
profile,
force,
record_state: true,
},
&ui,
)?;
if !siblings {
return Ok(());
}
if let Some(map) = &project.siblings {
let mut sibs: Vec<_> = map.iter().collect();
sibs.sort_by(|a, b| a.1.path.cmp(&b.1.path));
for (_, sib) in sibs {
let sib_dir = cwd.join(&sib.path);
let sib_project =
proj_parse::Project::from_file(sib_dir.join("conjure.kdl"))?;
build_ctx(
&BuildCtx {
project: &sib_project,
dir: sib_dir,
root: cwd.clone(),
profile, force,
record_state: true,
},
&ui,
)?;
}
}
Ok(())
}
pub fn test(
project: &Project,
profile: Option<&str>,
names: &[String],
) -> Result<()> {
let resolved = profile.and_then(|name| project.profile(name));
let effective = project.with_profile(resolved.map(|(_, p)| p));
let tests = effective
.tests
.as_ref()
.filter(|t| !t.is_empty())
.ok_or_else(|| miette::miette!("No `tests` in conjure.kdl"))?;
miette::ensure!(
effective.is_library(),
help = "Tests link the project's library, so the selected profile must be `type library`",
"`{}` is not a library under this profile",
project.name
);
for want in names {
miette::ensure!(tests.contains_key(want), "test `{want}` not found");
}
let cwd = std::env::current_dir().into_diagnostic()?;
let ui = Ui::new();
let mut selected: Vec<(&String, &Test)> = tests
.iter()
.filter(|(name, _)| names.is_empty() || names.iter().any(|n| n == *name))
.collect();
selected.sort_by(|a, b| a.0.cmp(b.0));
for (name, target) in selected {
let child = test_project(project, name, target, profile)?;
build_ctx(
&BuildCtx {
project: &child,
dir: cwd.clone(),
root: cwd.clone(),
profile,
force: false,
record_state: false,
},
&ui,
)?;
}
Ok(())
}