1mod macos;
5mod windows;
6
7use std::env;
8use std::fs;
9use std::io;
10use std::path::{Path, PathBuf};
11use std::process::{self, ExitCode};
12use std::sync::atomic::{AtomicUsize, Ordering};
13
14use nifty_check::hir::Program;
15use nifty_codegen::{RUNTIME_LIBRARY, compile_app, link};
16
17use crate::error::CliError;
18use crate::frontend::Sources;
19use crate::{CommandResult, EXIT_HAS_ERRORS, report};
20
21const OUTPUT_DIR: &str = "build";
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25enum Target {
26 MacOs,
27 Windows,
28 Linux,
29}
30
31impl Target {
32 const ALL: [Self; 3] = [Self::MacOs, Self::Windows, Self::Linux];
33
34 const HOST: Self = if cfg!(target_os = "macos") {
36 Self::MacOs
37 } else if cfg!(target_os = "windows") {
38 Self::Windows
39 } else {
40 Self::Linux
41 };
42
43 fn flag(self) -> &'static str {
44 match self {
45 Self::MacOs => "--macos",
46 Self::Windows => "--windows",
47 Self::Linux => "--linux",
48 }
49 }
50
51 fn name(self) -> &'static str {
52 match self {
53 Self::MacOs => "macOS",
54 Self::Windows => "Windows",
55 Self::Linux => "Linux",
56 }
57 }
58
59 fn from_flag(flag: &str) -> Option<Self> {
60 Self::ALL.into_iter().find(|target| target.flag() == flag)
61 }
62}
63
64#[derive(Debug, PartialEq)]
65struct Request<'a> {
66 project: &'a Path,
67 target: Target,
68}
69
70pub(crate) fn run(args: &[&str]) -> CommandResult {
71 let request = parse(args)?;
72 if request.target != Target::HOST {
74 return Err(CliError::new(format!(
75 "nf builds apps for the system it runs on, so build {0} apps on {0}",
76 request.target.name()
77 )));
78 }
79 if request.target == Target::Linux {
80 return Err(CliError::new(
81 "Nifty has no Linux backend yet, so it can't build Linux apps",
82 ));
83 }
84 let sources = Sources::read(request.project)?;
85 let program = match sources.compile() {
86 Ok(program) => program,
87 Err(diagnostics) => {
88 report::print(&sources, &diagnostics)?;
89 return Ok(ExitCode::from(EXIT_HAS_ERRORS));
90 }
91 };
92 let name = app_name(request.project)?;
93 let output = output_dir(&sources.entry().path);
94 let app = build(&sources, &program, &name, &output, &runtime_library()?)?;
95 println!("built {}", app.display());
96 Ok(ExitCode::SUCCESS)
97}
98
99fn build(
102 sources: &Sources,
103 program: &Program,
104 name: &str,
105 output: &Path,
106 runtime: &Path,
107) -> Result<PathBuf, CliError> {
108 static BUILDS: AtomicUsize = AtomicUsize::new(0);
110 let number = BUILDS.fetch_add(1, Ordering::Relaxed);
111 let scratch = env::temp_dir().join(format!("nf-build-{}-{number}", process::id()));
112 let app =
113 link_in(&scratch, sources, program, name, runtime).and_then(
114 |executable| match Target::HOST {
115 Target::MacOs => macos::write_app(output, name, &executable),
116 Target::Windows | Target::Linux => windows::write_app(output, name, &executable),
117 },
118 );
119 let cleaned = fs::remove_dir_all(&scratch)
120 .map_err(|error| CliError::new(format!("can't remove {}: {error}", scratch.display())));
121 let app = app?;
122 cleaned?;
123 Ok(app)
124}
125
126fn link_in(
128 scratch: &Path,
129 sources: &Sources,
130 program: &Program,
131 name: &str,
132 runtime: &Path,
133) -> Result<PathBuf, CliError> {
134 let failed =
135 |error: io::Error| CliError::new(format!("can't write {}: {error}", scratch.display()));
136 fs::create_dir_all(scratch).map_err(failed)?;
137 let compiled = compile_app(name, program, &|span| sources.locate(span))
138 .map_err(|error| CliError::new(format!("can't compile {name}: {error}")))?;
139 let object = scratch.join("app.o");
140 fs::write(&object, compiled).map_err(failed)?;
141 let executable = scratch.join(format!("app{}", env::consts::EXE_SUFFIX));
142 link(&object, runtime, &executable).map_err(|error| CliError::new(error.to_string()))?;
143 Ok(executable)
144}
145
146fn parse<'a>(args: &[&'a str]) -> Result<Request<'a>, CliError> {
147 let mut project = None;
148 let mut target = None;
149 for arg in args {
150 if arg.starts_with('-') {
151 let flags = Target::ALL.map(Target::flag).join(", ");
152 let chosen = Target::from_flag(arg)
153 .ok_or_else(|| CliError::new(format!("unknown option `{arg}`; use {flags}")))?;
154 if target.replace(chosen).is_some() {
155 return Err(CliError::new(format!(
156 "build for one system at a time: {flags}"
157 )));
158 }
159 } else if project.replace(Path::new(*arg)).is_some() {
160 return Err(CliError::new("build one project at a time"));
161 }
162 }
163 Ok(Request {
164 project: project.unwrap_or(Path::new(".")),
165 target: target.unwrap_or(Target::HOST),
166 })
167}
168
169fn app_name(project: &Path) -> Result<String, CliError> {
171 let path = fs::canonicalize(project)
172 .map_err(|error| CliError::new(format!("can't find {}: {error}", project.display())))?;
173 let name = if path.is_dir() {
174 path.file_name()
175 } else {
176 path.file_stem()
177 };
178 name.and_then(|name| name.to_str())
179 .map(str::to_owned)
180 .ok_or_else(|| {
181 CliError::new(format!(
182 "can't name an app after {}; give the project folder a plain name",
183 path.display()
184 ))
185 })
186}
187
188fn output_dir(entry: &Path) -> PathBuf {
189 entry.parent().unwrap_or(Path::new("")).join(OUTPUT_DIR)
190}
191
192fn runtime_library() -> Result<PathBuf, CliError> {
196 let nf = env::current_exe()
197 .and_then(fs::canonicalize)
198 .map_err(|error| CliError::new(format!("can't find where nf is installed: {error}")))?;
199 let library = nf.with_file_name(RUNTIME_LIBRARY);
200 if library.is_file() {
201 Ok(library)
202 } else {
203 Err(CliError::new(format!(
204 "{} is missing; reinstall Nifty to get it back",
205 library.display()
206 )))
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use nifty_codegen::testing::runtime_library;
214
215 use crate::test_support::scratch_dir;
216
217 #[test]
218 fn builds_the_current_folder_for_this_system_by_default() {
219 assert_eq!(
220 parse(&[]).expect("no arguments is fine"),
221 Request {
222 project: Path::new("."),
223 target: Target::HOST
224 }
225 );
226 }
227
228 #[test]
229 fn takes_a_project_and_a_target_in_any_order() {
230 let expected = Request {
231 project: Path::new("todos"),
232 target: Target::Windows,
233 };
234 assert_eq!(parse(&["todos", "--windows"]).expect("valid"), expected);
235 assert_eq!(parse(&["--windows", "todos"]).expect("valid"), expected);
236 }
237
238 #[test]
239 fn rejects_unknown_repeated_and_conflicting_arguments() {
240 for args in [
241 &["--win"][..],
242 &["--macos", "--linux"],
243 &["--macos", "--macos"],
244 &["a", "b"],
245 ] {
246 assert!(parse(args).is_err(), "{args:?} should be rejected");
247 }
248 }
249
250 #[test]
251 fn apps_are_named_after_their_folder_or_file() {
252 let dir = scratch_dir("build-name");
253 let folder = dir.join("todos");
254 fs::create_dir(&folder).expect("create project folder");
255 let file = dir.join("demo.nf");
256 fs::write(&file, "").expect("write demo.nf");
257
258 assert_eq!(app_name(&folder).expect("a folder"), "todos");
259 assert_eq!(app_name(&file).expect("a file"), "demo");
260 }
261
262 #[test]
263 fn apps_are_built_next_to_the_entry_file() {
264 assert_eq!(
265 output_dir(Path::new("todos/main.nf")),
266 Path::new("todos/build")
267 );
268 assert_eq!(output_dir(Path::new("main.nf")), Path::new("build"));
269 }
270
271 fn project(dir: &Path, source: &str) -> (PathBuf, Sources, Program) {
273 let project = dir.join("project");
274 fs::create_dir_all(&project).expect("create the project folder");
275 fs::write(project.join("main.nf"), source).expect("write main.nf");
276 let sources = Sources::read(&project).expect("read the project");
277 let program = sources.compile().expect("the project checks");
278 (project, sources, program)
279 }
280
281 fn executable(app: &Path, name: &str) -> PathBuf {
283 match Target::HOST {
284 Target::MacOs => app.join("Contents/MacOS").join(name),
285 Target::Windows | Target::Linux => app.join(format!("{name}.exe")),
286 }
287 }
288
289 #[test]
290 fn builds_the_counter_example() {
291 let dir = scratch_dir("build-counter");
292 let source = include_str!("../../../fixtures/counter.nf");
293 let (_, sources, program) = project(&dir, source);
294
295 let app = build(
296 &sources,
297 &program,
298 "Counter",
299 &dir.join("build"),
300 &runtime_library(),
301 )
302 .unwrap_or_else(|error| panic!("{error}"));
303
304 assert!(executable(&app, "Counter").is_file());
305 }
306
307 #[test]
308 fn built_apps_run_compiled_and_point_errors_at_the_source() {
309 let dir = scratch_dir("build-failing");
310 let source = "fn zero() -> int {\n return 0\n}\n\nstate count = 1 / zero()\n\nwindow(\"Never\") {}\n";
311 let (_, sources, program) = project(&dir, source);
312
313 let app = build(
314 &sources,
315 &program,
316 "broken",
317 &dir.join("build"),
318 &runtime_library(),
319 )
320 .unwrap_or_else(|error| panic!("{error}"));
321
322 let output = std::process::Command::new(executable(&app, "broken"))
323 .output()
324 .expect("run the app");
325 assert_eq!(output.status.code(), Some(1));
326 assert_eq!(
327 String::from_utf8_lossy(&output.stderr).trim_end(),
328 "main.nf:5:15: can't divide by zero"
329 );
330 }
331}