logicaffeine_cli/project/
build_c.rs1use std::fs;
17use std::path::{Path, PathBuf};
18use std::process::Command;
19
20use super::build::{BuildConfig, BuildError, BuildResult};
21use super::manifest::Manifest;
22
23pub const C_DEBUG_FLAGS: &[&str] = &["-O0", "-g"];
25
26pub const C_RELEASE_FLAGS: &[&str] = &["-O3", "-march=native", "-flto", "-ffp-contract=off"];
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum CLowering {
34 Ast,
36 Bytecode,
39}
40
41impl CLowering {
42 pub fn emit_name(self) -> &'static str {
44 match self {
45 CLowering::Ast => "c",
46 CLowering::Bytecode => "c-bytecode",
47 }
48 }
49}
50
51pub fn c_compiler() -> String {
53 std::env::var("CC")
54 .ok()
55 .filter(|cc| !cc.trim().is_empty())
56 .unwrap_or_else(|| "cc".to_string())
57}
58
59fn release_strips() -> bool {
62 !matches!(
63 std::env::var("CARGO_PROFILE_RELEASE_STRIP")
64 .ok()
65 .as_deref()
66 .map(str::trim),
67 Some("false") | Some("none")
68 )
69}
70
71pub fn c_compiler_args(release: bool, source: &Path, output: &Path, links: &[String]) -> Vec<String> {
75 let mut args: Vec<String> = (if release {
76 C_RELEASE_FLAGS
77 } else {
78 C_DEBUG_FLAGS
79 })
80 .iter()
81 .map(|f| f.to_string())
82 .collect();
83 if release && release_strips() {
84 args.push("-s".to_string());
85 }
86 args.push("-o".to_string());
87 args.push(output.display().to_string());
88 args.push(source.display().to_string());
89 args.extend(links.iter().cloned());
90 args.push("-lm".to_string());
91 args
92}
93
94pub(crate) fn build_c_with_entry(
95 config: &BuildConfig,
96 manifest: &Manifest,
97 entry_path: &Path,
98 lowering: CLowering,
99) -> Result<BuildResult, BuildError> {
100 if config.lib_mode {
101 return Err(BuildError::Unsupported(format!(
102 "`--emit {}` builds an executable; `--lib` is a Rust build (drop `--emit` to build the library)",
103 lowering.emit_name()
104 )));
105 }
106 if config.target.is_some() {
107 return Err(BuildError::Unsupported(format!(
108 "`--emit {}` builds for this machine; `--target` cross-compiles the Rust build (drop `--emit` to cross-compile)",
109 lowering.emit_name()
110 )));
111 }
112
113 let started = std::time::Instant::now();
114 crate::ui::phase(
115 "Compiling",
116 format!(
117 "{} v{} (LOGOS → C)",
118 manifest.package.name, manifest.package.version
119 ),
120 );
121
122 let build_dir =
123 config
124 .project_dir
125 .join("target")
126 .join(if config.release { "release" } else { "debug" });
127 let c_dir = build_dir.join("build-c");
128 if c_dir.exists() {
129 fs::remove_dir_all(&c_dir).map_err(|e| BuildError::Io(e.to_string()))?;
130 }
131 fs::create_dir_all(&c_dir).map_err(|e| BuildError::Io(e.to_string()))?;
132
133 let dep_roots = manifest
134 .resolve_dependencies(&config.project_dir)
135 .map_err(|e| BuildError::Io(e.to_string()))?
136 .into_iter()
137 .map(|d| logicaffeine_compile::loader::DepRoot {
138 name: d.name,
139 root: d.root,
140 })
141 .collect::<Vec<_>>();
142 let closure = crate::closure::assemble_closure_with_deps(entry_path, dep_roots)
143 .map_err(|e| BuildError::Io(format!("could not assemble project: {e}")))?;
144 let emitted = match lowering {
145 CLowering::Ast => logicaffeine_compile::compile::compile_to_c(&closure.source),
146 CLowering::Bytecode => {
147 logicaffeine_compile::compile::compile_to_c_via_bytecode(&closure.source)
148 }
149 };
150 let c_code = emitted.map_err(|pe| {
151 let at = crate::diagnostics::Placed::locate(
152 pe.span,
153 &closure.source,
154 &closure.source_map,
155 entry_path,
156 );
157 BuildError::Diagnostic(crate::diagnostics::render_parse_error(&pe, &at))
158 })?;
159
160 let source = c_dir.join("main.c");
161 fs::write(&source, &c_code).map_err(|e| BuildError::Io(e.to_string()))?;
162 let built = c_dir.join(&manifest.package.name);
163
164 let cc = c_compiler();
165 if logicaffeine_compile::codegen_c::value_runtime::links(&c_code) {
166 crate::ui::phase("Building", "the exact extension's value runtime (Rust)".to_string());
167 }
168 let links = logicaffeine_compile::codegen_c::value_runtime::link_args(&c_code).map_err(|why| {
169 BuildError::CToolchain(format!("the exact extension's value runtime did not build: {why}"))
170 })?;
171 let args = c_compiler_args(config.release, &source, &built, &links);
172 crate::ui::phase("Building", format!("generated C ({cc})"));
173 if crate::ui::verbosity() > 0 {
174 crate::ui::phase("Running", format!("`{cc} {}`", args.join(" ")));
175 }
176 let status = Command::new(&cc).args(&args).status().map_err(|e| {
177 if e.kind() == std::io::ErrorKind::NotFound {
178 BuildError::CToolchain(format!(
179 "the C compiler `{cc}` was not found — `largo build --emit {}` needs a C compiler (set CC to choose one)",
180 lowering.emit_name()
181 ))
182 } else {
183 BuildError::Io(format!("could not run the C compiler `{cc}`: {e}"))
184 }
185 })?;
186 if !status.success() {
187 return Err(BuildError::CCompiler(format!(
188 "the C compiler `{cc}` failed ({status}) on the generated C at {}; its diagnostics are above",
189 source.display()
190 )));
191 }
192
193 crate::ui::phase(
194 "Finished",
195 format!(
196 "{} profile in {:.1}s",
197 if config.release { "release" } else { "dev" },
198 started.elapsed().as_secs_f64()
199 ),
200 );
201 let binary_path: PathBuf = build_dir.join(&manifest.package.name);
202 fs::copy(&built, &binary_path).map_err(|e| {
203 BuildError::Io(format!(
204 "the build succeeded but its artifact could not be published to {}: {e}\n\
205 The freshly built file is at {}. Anything at the stable path is from an EARLIER \
206 build — do not ship it.",
207 binary_path.display(),
208 built.display()
209 ))
210 })?;
211 crate::ui::phase("Artifact", binary_path.display().to_string());
212
213 Ok(BuildResult {
214 target_dir: build_dir,
215 binary_path,
216 })
217}