1use std::{
2 collections::BTreeSet,
3 error::Error,
4 fmt, fs, io,
5 path::{Path, PathBuf},
6};
7
8use nwnrs_types::resman::prelude::{ResType, get_res_ext};
9
10use crate::{
11 CompileArtifacts, CompilerSession, CompilerSessionError, CompilerSessionOptions,
12 NW_SCRIPT_SOURCE_RES_TYPE, ScriptResolver, SourceError, session::PreparedScript,
13};
14
15#[derive(#[automatically_derived]
impl ::core::fmt::Debug for CompilerHostError {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field1_finish(f,
"CompilerHostError", "message", &&self.message)
}
}Debug, #[automatically_derived]
impl ::core::clone::Clone for CompilerHostError {
#[inline]
fn clone(&self) -> CompilerHostError {
CompilerHostError {
message: ::core::clone::Clone::clone(&self.message),
}
}
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for CompilerHostError {
#[inline]
fn eq(&self, other: &CompilerHostError) -> bool {
self.message == other.message
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for CompilerHostError {
#[inline]
#[doc(hidden)]
#[coverage(off)]
fn assert_fields_are_eq(&self) {
let _: ::core::cmp::AssertParamIsEq<String>;
}
}Eq)]
18pub struct CompilerHostError {
19 pub message: String,
21}
22
23impl CompilerHostError {
24 #[must_use]
26 pub fn new(message: impl Into<String>) -> Self {
27 Self {
28 message: message.into(),
29 }
30 }
31}
32
33impl fmt::Display for CompilerHostError {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 f.write_str(&self.message)
36 }
37}
38
39impl Error for CompilerHostError {}
40
41impl From<io::Error> for CompilerHostError {
42 fn from(value: io::Error) -> Self {
43 Self::new(value.to_string())
44 }
45}
46
47pub trait CompilerHost {
50 fn resolve_script_bytes(
56 &self,
57 script_name: &str,
58 res_type: ResType,
59 ) -> Result<Option<Vec<u8>>, SourceError>;
60
61 fn write_file(
68 &mut self,
69 file_name: &str,
70 res_type: ResType,
71 data: &[u8],
72 binary: bool,
73 ) -> Result<(), CompilerHostError>;
74
75 fn write_graphviz(&mut self, _file_name: &str, _dot: &str) -> Result<(), CompilerHostError> {
82 Ok(())
83 }
84}
85
86#[derive(#[automatically_derived]
impl ::core::fmt::Debug for CompilerDriverOptions {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
let names: &'static _ =
&["session", "source_res_type", "binary_res_type",
"debug_res_type", "output_alias", "emit_graphviz",
"graphviz_alias", "skip_missing_entrypoint"];
let values: &[&dyn ::core::fmt::Debug] =
&[&self.session, &self.source_res_type, &self.binary_res_type,
&self.debug_res_type, &self.output_alias,
&self.emit_graphviz, &self.graphviz_alias,
&&self.skip_missing_entrypoint];
::core::fmt::Formatter::debug_struct_fields_finish(f,
"CompilerDriverOptions", names, values)
}
}Debug, #[automatically_derived]
impl ::core::clone::Clone for CompilerDriverOptions {
#[inline]
fn clone(&self) -> CompilerDriverOptions {
CompilerDriverOptions {
session: ::core::clone::Clone::clone(&self.session),
source_res_type: ::core::clone::Clone::clone(&self.source_res_type),
binary_res_type: ::core::clone::Clone::clone(&self.binary_res_type),
debug_res_type: ::core::clone::Clone::clone(&self.debug_res_type),
output_alias: ::core::clone::Clone::clone(&self.output_alias),
emit_graphviz: ::core::clone::Clone::clone(&self.emit_graphviz),
graphviz_alias: ::core::clone::Clone::clone(&self.graphviz_alias),
skip_missing_entrypoint: ::core::clone::Clone::clone(&self.skip_missing_entrypoint),
}
}
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for CompilerDriverOptions {
#[inline]
fn eq(&self, other: &CompilerDriverOptions) -> bool {
self.emit_graphviz == other.emit_graphviz &&
self.skip_missing_entrypoint ==
other.skip_missing_entrypoint &&
self.session == other.session &&
self.source_res_type == other.source_res_type &&
self.binary_res_type == other.binary_res_type &&
self.debug_res_type == other.debug_res_type &&
self.output_alias == other.output_alias &&
self.graphviz_alias == other.graphviz_alias
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for CompilerDriverOptions {
#[inline]
#[doc(hidden)]
#[coverage(off)]
fn assert_fields_are_eq(&self) {
let _: ::core::cmp::AssertParamIsEq<CompilerSessionOptions>;
let _: ::core::cmp::AssertParamIsEq<ResType>;
let _: ::core::cmp::AssertParamIsEq<String>;
let _: ::core::cmp::AssertParamIsEq<bool>;
let _: ::core::cmp::AssertParamIsEq<Option<String>>;
}
}Eq)]
88pub struct CompilerDriverOptions {
89 pub session: CompilerSessionOptions,
91 pub source_res_type: ResType,
93 pub binary_res_type: ResType,
95 pub debug_res_type: ResType,
97 pub output_alias: String,
99 pub emit_graphviz: bool,
101 pub graphviz_alias: Option<String>,
103 pub skip_missing_entrypoint: bool,
106}
107
108impl Default for CompilerDriverOptions {
109 fn default() -> Self {
110 Self {
111 session: CompilerSessionOptions::default(),
112 source_res_type: NW_SCRIPT_SOURCE_RES_TYPE,
113 binary_res_type: ResType(2010),
114 debug_res_type: ResType(2064),
115 output_alias: "scriptout".to_string(),
116 emit_graphviz: false,
117 graphviz_alias: None,
118 skip_missing_entrypoint: false,
119 }
120 }
121}
122
123#[derive(#[automatically_derived]
impl ::core::fmt::Debug for CompileFileOutcome {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
match self {
CompileFileOutcome::Compiled(__self_0) =>
::core::fmt::Formatter::debug_tuple_field1_finish(f,
"Compiled", &__self_0),
CompileFileOutcome::SkippedNoEntrypoint =>
::core::fmt::Formatter::write_str(f, "SkippedNoEntrypoint"),
}
}
}Debug, #[automatically_derived]
impl ::core::clone::Clone for CompileFileOutcome {
#[inline]
fn clone(&self) -> CompileFileOutcome {
match self {
CompileFileOutcome::Compiled(__self_0) =>
CompileFileOutcome::Compiled(::core::clone::Clone::clone(__self_0)),
CompileFileOutcome::SkippedNoEntrypoint =>
CompileFileOutcome::SkippedNoEntrypoint,
}
}
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for CompileFileOutcome {
#[inline]
fn eq(&self, other: &CompileFileOutcome) -> bool {
let __self_discr = ::core::intrinsics::discriminant_value(self);
let __arg1_discr = ::core::intrinsics::discriminant_value(other);
__self_discr == __arg1_discr &&
match (self, other) {
(CompileFileOutcome::Compiled(__self_0),
CompileFileOutcome::Compiled(__arg1_0)) =>
__self_0 == __arg1_0,
_ => true,
}
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for CompileFileOutcome {
#[inline]
#[doc(hidden)]
#[coverage(off)]
fn assert_fields_are_eq(&self) {
let _: ::core::cmp::AssertParamIsEq<CompileArtifacts>;
}
}Eq)]
125pub enum CompileFileOutcome {
126 Compiled(CompileArtifacts),
129 SkippedNoEntrypoint,
131}
132
133#[derive(#[automatically_derived]
impl ::core::fmt::Debug for CompilerDriverError {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
match self {
CompilerDriverError::Session(__self_0) =>
::core::fmt::Formatter::debug_tuple_field1_finish(f,
"Session", &__self_0),
CompilerDriverError::Host(__self_0) =>
::core::fmt::Formatter::debug_tuple_field1_finish(f, "Host",
&__self_0),
}
}
}Debug)]
135pub enum CompilerDriverError {
136 Session(CompilerSessionError),
138 Host(CompilerHostError),
140}
141
142impl fmt::Display for CompilerDriverError {
143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144 match self {
145 Self::Session(error) => error.fmt(f),
146 Self::Host(error) => error.fmt(f),
147 }
148 }
149}
150
151impl Error for CompilerDriverError {}
152
153impl From<CompilerSessionError> for CompilerDriverError {
154 fn from(value: CompilerSessionError) -> Self {
155 Self::Session(value)
156 }
157}
158
159impl From<CompilerHostError> for CompilerDriverError {
160 fn from(value: CompilerHostError) -> Self {
161 Self::Host(value)
162 }
163}
164
165struct HostResolver<'a, H> {
166 host: &'a H,
167}
168
169impl<H: CompilerHost> ScriptResolver for HostResolver<'_, H> {
170 fn resolve_script_bytes(
171 &self,
172 script_name: &str,
173 res_type: ResType,
174 ) -> Result<Option<Vec<u8>>, SourceError> {
175 self.host.resolve_script_bytes(script_name, res_type)
176 }
177}
178
179pub fn compile_file_with_host<H: CompilerHost>(
186 host: &mut H,
187 script_name: &str,
188 options: &CompilerDriverOptions,
189) -> Result<CompileFileOutcome, CompilerDriverError> {
190 let (prepared, artifacts, graphviz) = {
191 let resolver = HostResolver {
192 host: &*host
193 };
194 let mut session = CompilerSession::with_options(&resolver, options.session.clone());
195 let prepared = session.prepare_script_name(script_name)?;
196 if options.skip_missing_entrypoint && !prepared_has_entrypoint(&prepared) {
197 return Ok(CompileFileOutcome::SkippedNoEntrypoint);
198 }
199 let graphviz = if options.emit_graphviz {
200 Some(crate::render_script_graphviz(
201 &prepared.script,
202 Some(&prepared.bundle.source_map),
203 ))
204 } else {
205 None
206 };
207 let artifacts = session
208 .compile_prepared(&prepared)
209 .map_err(CompilerSessionError::from)
210 .map_err(CompilerDriverError::from)?;
211 (prepared, artifacts, graphviz)
212 };
213
214 host.write_file(
215 &options.output_alias,
216 options.binary_res_type,
217 &artifacts.ncs,
218 true,
219 )?;
220 if let Some(ndb) = artifacts.ndb.as_ref() {
221 host.write_file(&options.output_alias, options.debug_res_type, ndb, true)?;
222 }
223 if let Some(dot) = graphviz.as_deref() {
224 let graphviz_alias = options
225 .graphviz_alias
226 .as_deref()
227 .unwrap_or(&options.output_alias);
228 host.write_graphviz(graphviz_alias, dot)?;
229 }
230 let _ = prepared;
231 Ok(CompileFileOutcome::Compiled(artifacts))
232}
233
234fn prepared_has_entrypoint(prepared: &PreparedScript) -> bool {
235 prepared.script.items.iter().any(|item| match item {
236 crate::TopLevelItem::Function(function) => {
237 function.body.is_some()
238 && (function.name == "main" || function.name == "StartingConditional")
239 }
240 _ => false,
241 })
242}
243
244#[derive(#[automatically_derived]
impl ::core::fmt::Debug for FileSystemScriptResolver {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field1_finish(f,
"FileSystemScriptResolver", "roots", &&self.roots)
}
}Debug, #[automatically_derived]
impl ::core::clone::Clone for FileSystemScriptResolver {
#[inline]
fn clone(&self) -> FileSystemScriptResolver {
FileSystemScriptResolver {
roots: ::core::clone::Clone::clone(&self.roots),
}
}
}Clone, #[automatically_derived]
impl ::core::default::Default for FileSystemScriptResolver {
#[inline]
fn default() -> FileSystemScriptResolver {
FileSystemScriptResolver {
roots: ::core::default::Default::default(),
}
}
}Default, #[automatically_derived]
impl ::core::cmp::PartialEq for FileSystemScriptResolver {
#[inline]
fn eq(&self, other: &FileSystemScriptResolver) -> bool {
self.roots == other.roots
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for FileSystemScriptResolver {
#[inline]
#[doc(hidden)]
#[coverage(off)]
fn assert_fields_are_eq(&self) {
let _: ::core::cmp::AssertParamIsEq<Vec<PathBuf>>;
}
}Eq)]
247pub struct FileSystemScriptResolver {
248 roots: Vec<PathBuf>,
249}
250
251impl FileSystemScriptResolver {
252 #[must_use]
254 pub fn new() -> Self {
255 Self::default()
256 }
257
258 #[must_use]
260 pub fn with_root(root: impl Into<PathBuf>) -> Self {
261 let mut resolver = Self::new();
262 resolver.add_root(root);
263 resolver
264 }
265
266 pub fn add_root(&mut self, root: impl Into<PathBuf>) {
268 self.roots.push(root.into());
269 }
270
271 fn candidate_paths(&self, script_name: &str) -> Vec<PathBuf> {
272 let path = Path::new(script_name);
273 let mut names = ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
[PathBuf::from(script_name)]))vec![PathBuf::from(script_name)];
274 if path.extension().is_none() {
275 names.push(PathBuf::from(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{1}.{0}",
get_res_ext(NW_SCRIPT_SOURCE_RES_TYPE), script_name))
})format!(
276 "{script_name}.{}",
277 get_res_ext(NW_SCRIPT_SOURCE_RES_TYPE)
278 )));
279 }
280
281 let mut candidates = Vec::new();
282 for name in names {
283 if path.is_absolute() || name.is_absolute() {
284 candidates.push(name.clone());
285 } else {
286 candidates.push(name.clone());
287 for root in &self.roots {
288 candidates.push(root.join(&name));
289 }
290 }
291 }
292 candidates
293 }
294}
295
296impl ScriptResolver for FileSystemScriptResolver {
297 fn resolve_script_bytes(
298 &self,
299 script_name: &str,
300 res_type: ResType,
301 ) -> Result<Option<Vec<u8>>, SourceError> {
302 if res_type != NW_SCRIPT_SOURCE_RES_TYPE {
303 return Ok(None);
304 }
305 for candidate in self.candidate_paths(script_name) {
306 if candidate.is_file() {
307 return fs::read(&candidate)
308 .map(Some)
309 .map_err(|error| SourceError::resolver(error.to_string()));
310 }
311 }
312 Ok(None)
313 }
314}
315
316#[derive(#[automatically_derived]
impl ::core::fmt::Debug for DirectoryCompilerHost {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field5_finish(f,
"DirectoryCompilerHost", "resolver", &self.resolver,
"output_directory", &self.output_directory, "graphviz_directory",
&self.graphviz_directory, "simulate", &self.simulate,
"written_paths", &&self.written_paths)
}
}Debug, #[automatically_derived]
impl ::core::clone::Clone for DirectoryCompilerHost {
#[inline]
fn clone(&self) -> DirectoryCompilerHost {
DirectoryCompilerHost {
resolver: ::core::clone::Clone::clone(&self.resolver),
output_directory: ::core::clone::Clone::clone(&self.output_directory),
graphviz_directory: ::core::clone::Clone::clone(&self.graphviz_directory),
simulate: ::core::clone::Clone::clone(&self.simulate),
written_paths: ::core::clone::Clone::clone(&self.written_paths),
}
}
}Clone)]
319pub struct DirectoryCompilerHost {
320 resolver: FileSystemScriptResolver,
321 output_directory: PathBuf,
322 graphviz_directory: Option<PathBuf>,
323 simulate: bool,
324 written_paths: Vec<PathBuf>,
325}
326
327impl DirectoryCompilerHost {
328 #[must_use]
330 pub fn new(resolver: FileSystemScriptResolver, output_directory: impl Into<PathBuf>) -> Self {
331 Self {
332 resolver,
333 output_directory: output_directory.into(),
334 graphviz_directory: None,
335 simulate: false,
336 written_paths: Vec::new(),
337 }
338 }
339
340 pub fn set_graphviz_directory(&mut self, directory: impl Into<PathBuf>) {
342 self.graphviz_directory = Some(directory.into());
343 }
344
345 pub fn set_simulate(&mut self, simulate: bool) {
348 self.simulate = simulate;
349 }
350
351 #[must_use]
353 pub fn written_paths(&self) -> &[PathBuf] {
354 &self.written_paths
355 }
356}
357
358impl CompilerHost for DirectoryCompilerHost {
359 fn resolve_script_bytes(
360 &self,
361 script_name: &str,
362 res_type: ResType,
363 ) -> Result<Option<Vec<u8>>, SourceError> {
364 self.resolver.resolve_script_bytes(script_name, res_type)
365 }
366
367 fn write_file(
368 &mut self,
369 file_name: &str,
370 res_type: ResType,
371 data: &[u8],
372 _binary: bool,
373 ) -> Result<(), CompilerHostError> {
374 let path = self
375 .output_directory
376 .join(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{1}.{0}", get_res_ext(res_type),
file_name))
})format!("{file_name}.{}", get_res_ext(res_type)));
377 self.written_paths.push(path.clone());
378 if self.simulate {
379 return Ok(());
380 }
381 if let Some(parent) = path.parent() {
382 fs::create_dir_all(parent)?;
383 }
384 fs::write(&path, data)?;
385 Ok(())
386 }
387
388 fn write_graphviz(&mut self, file_name: &str, dot: &str) -> Result<(), CompilerHostError> {
389 let base = self
390 .graphviz_directory
391 .as_ref()
392 .unwrap_or(&self.output_directory);
393 let path = base.join(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}.dot", file_name))
})format!("{file_name}.dot"));
394 self.written_paths.push(path.clone());
395 if self.simulate {
396 return Ok(());
397 }
398 if let Some(parent) = path.parent() {
399 fs::create_dir_all(parent)?;
400 }
401 fs::write(&path, dot.as_bytes())?;
402 Ok(())
403 }
404}
405
406#[derive(#[automatically_derived]
impl ::core::fmt::Debug for BatchCompileOptions {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
let names: &'static _ =
&["driver", "search_roots", "recurse", "follow_symlinks",
"continue_on_error", "simulate", "output_directory",
"graphviz_directory"];
let values: &[&dyn ::core::fmt::Debug] =
&[&self.driver, &self.search_roots, &self.recurse,
&self.follow_symlinks, &self.continue_on_error,
&self.simulate, &self.output_directory,
&&self.graphviz_directory];
::core::fmt::Formatter::debug_struct_fields_finish(f,
"BatchCompileOptions", names, values)
}
}Debug, #[automatically_derived]
impl ::core::clone::Clone for BatchCompileOptions {
#[inline]
fn clone(&self) -> BatchCompileOptions {
BatchCompileOptions {
driver: ::core::clone::Clone::clone(&self.driver),
search_roots: ::core::clone::Clone::clone(&self.search_roots),
recurse: ::core::clone::Clone::clone(&self.recurse),
follow_symlinks: ::core::clone::Clone::clone(&self.follow_symlinks),
continue_on_error: ::core::clone::Clone::clone(&self.continue_on_error),
simulate: ::core::clone::Clone::clone(&self.simulate),
output_directory: ::core::clone::Clone::clone(&self.output_directory),
graphviz_directory: ::core::clone::Clone::clone(&self.graphviz_directory),
}
}
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for BatchCompileOptions {
#[inline]
fn eq(&self, other: &BatchCompileOptions) -> bool {
self.recurse == other.recurse &&
self.follow_symlinks == other.follow_symlinks &&
self.continue_on_error == other.continue_on_error &&
self.simulate == other.simulate &&
self.driver == other.driver &&
self.search_roots == other.search_roots &&
self.output_directory == other.output_directory &&
self.graphviz_directory == other.graphviz_directory
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for BatchCompileOptions {
#[inline]
#[doc(hidden)]
#[coverage(off)]
fn assert_fields_are_eq(&self) {
let _: ::core::cmp::AssertParamIsEq<CompilerDriverOptions>;
let _: ::core::cmp::AssertParamIsEq<Vec<PathBuf>>;
let _: ::core::cmp::AssertParamIsEq<bool>;
let _: ::core::cmp::AssertParamIsEq<Option<PathBuf>>;
let _: ::core::cmp::AssertParamIsEq<Option<PathBuf>>;
}
}Eq)]
408pub struct BatchCompileOptions {
409 pub driver: CompilerDriverOptions,
411 pub search_roots: Vec<PathBuf>,
413 pub recurse: bool,
415 pub follow_symlinks: bool,
417 pub continue_on_error: bool,
419 pub simulate: bool,
421 pub output_directory: Option<PathBuf>,
423 pub graphviz_directory: Option<PathBuf>,
425}
426
427impl Default for BatchCompileOptions {
428 fn default() -> Self {
429 Self {
430 driver: CompilerDriverOptions {
431 skip_missing_entrypoint: true,
432 ..CompilerDriverOptions::default()
433 },
434 search_roots: Vec::new(),
435 recurse: false,
436 follow_symlinks: false,
437 continue_on_error: false,
438 simulate: false,
439 output_directory: None,
440 graphviz_directory: None,
441 }
442 }
443}
444
445#[derive(#[automatically_derived]
impl ::core::fmt::Debug for BatchCompileEntry {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field4_finish(f,
"BatchCompileEntry", "input", &self.input, "status", &self.status,
"outputs", &self.outputs, "error", &&self.error)
}
}Debug, #[automatically_derived]
impl ::core::clone::Clone for BatchCompileEntry {
#[inline]
fn clone(&self) -> BatchCompileEntry {
BatchCompileEntry {
input: ::core::clone::Clone::clone(&self.input),
status: ::core::clone::Clone::clone(&self.status),
outputs: ::core::clone::Clone::clone(&self.outputs),
error: ::core::clone::Clone::clone(&self.error),
}
}
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for BatchCompileEntry {
#[inline]
fn eq(&self, other: &BatchCompileEntry) -> bool {
self.input == other.input && self.status == other.status &&
self.outputs == other.outputs && self.error == other.error
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for BatchCompileEntry {
#[inline]
#[doc(hidden)]
#[coverage(off)]
fn assert_fields_are_eq(&self) {
let _: ::core::cmp::AssertParamIsEq<PathBuf>;
let _: ::core::cmp::AssertParamIsEq<BatchCompileStatus>;
let _: ::core::cmp::AssertParamIsEq<Vec<PathBuf>>;
let _: ::core::cmp::AssertParamIsEq<Option<String>>;
}
}Eq)]
447pub struct BatchCompileEntry {
448 pub input: PathBuf,
450 pub status: BatchCompileStatus,
452 pub outputs: Vec<PathBuf>,
454 pub error: Option<String>,
456}
457
458#[derive(#[automatically_derived]
impl ::core::fmt::Debug for BatchCompileStatus {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::write_str(f,
match self {
BatchCompileStatus::Success => "Success",
BatchCompileStatus::Skipped => "Skipped",
BatchCompileStatus::Error => "Error",
})
}
}Debug, #[automatically_derived]
impl ::core::clone::Clone for BatchCompileStatus {
#[inline]
fn clone(&self) -> BatchCompileStatus { *self }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for BatchCompileStatus { }Copy, #[automatically_derived]
impl ::core::cmp::PartialEq for BatchCompileStatus {
#[inline]
fn eq(&self, other: &BatchCompileStatus) -> bool {
let __self_discr = ::core::intrinsics::discriminant_value(self);
let __arg1_discr = ::core::intrinsics::discriminant_value(other);
__self_discr == __arg1_discr
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for BatchCompileStatus {
#[inline]
#[doc(hidden)]
#[coverage(off)]
fn assert_fields_are_eq(&self) {}
}Eq)]
460pub enum BatchCompileStatus {
461 Success,
463 Skipped,
465 Error,
467}
468
469#[derive(#[automatically_derived]
impl ::core::fmt::Debug for BatchCompileReport {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field4_finish(f,
"BatchCompileReport", "entries", &self.entries, "successes",
&self.successes, "skips", &self.skips, "errors", &&self.errors)
}
}Debug, #[automatically_derived]
impl ::core::clone::Clone for BatchCompileReport {
#[inline]
fn clone(&self) -> BatchCompileReport {
BatchCompileReport {
entries: ::core::clone::Clone::clone(&self.entries),
successes: ::core::clone::Clone::clone(&self.successes),
skips: ::core::clone::Clone::clone(&self.skips),
errors: ::core::clone::Clone::clone(&self.errors),
}
}
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for BatchCompileReport {
#[inline]
fn eq(&self, other: &BatchCompileReport) -> bool {
self.entries == other.entries && self.successes == other.successes &&
self.skips == other.skips && self.errors == other.errors
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for BatchCompileReport {
#[inline]
#[doc(hidden)]
#[coverage(off)]
fn assert_fields_are_eq(&self) {
let _: ::core::cmp::AssertParamIsEq<Vec<BatchCompileEntry>>;
let _: ::core::cmp::AssertParamIsEq<usize>;
}
}Eq, #[automatically_derived]
impl ::core::default::Default for BatchCompileReport {
#[inline]
fn default() -> BatchCompileReport {
BatchCompileReport {
entries: ::core::default::Default::default(),
successes: ::core::default::Default::default(),
skips: ::core::default::Default::default(),
errors: ::core::default::Default::default(),
}
}
}Default)]
471pub struct BatchCompileReport {
472 pub entries: Vec<BatchCompileEntry>,
474 pub successes: usize,
476 pub skips: usize,
478 pub errors: usize,
480}
481
482#[derive(#[automatically_derived]
impl ::core::fmt::Debug for BatchCompileError {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
match self {
BatchCompileError::Io(__self_0) =>
::core::fmt::Formatter::debug_tuple_field1_finish(f, "Io",
&__self_0),
BatchCompileError::Driver(__self_0) =>
::core::fmt::Formatter::debug_tuple_field1_finish(f, "Driver",
&__self_0),
}
}
}Debug)]
484pub enum BatchCompileError {
485 Io(io::Error),
487 Driver(CompilerDriverError),
489}
490
491impl fmt::Display for BatchCompileError {
492 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
493 match self {
494 Self::Io(error) => error.fmt(f),
495 Self::Driver(error) => error.fmt(f),
496 }
497 }
498}
499
500impl Error for BatchCompileError {}
501
502impl From<io::Error> for BatchCompileError {
503 fn from(value: io::Error) -> Self {
504 Self::Io(value)
505 }
506}
507
508impl From<CompilerDriverError> for BatchCompileError {
509 fn from(value: CompilerDriverError) -> Self {
510 Self::Driver(value)
511 }
512}
513
514pub fn compile_paths(
521 paths: &[PathBuf],
522 options: &BatchCompileOptions,
523) -> Result<BatchCompileReport, BatchCompileError> {
524 let queue = collect_compile_inputs(paths, options)?;
525 let mut report = BatchCompileReport::default();
526
527 for input in queue {
528 let parent = input
529 .parent()
530 .map(Path::to_path_buf)
531 .unwrap_or_else(|| PathBuf::from("."));
532 let output_directory = options
533 .output_directory
534 .clone()
535 .unwrap_or_else(|| parent.clone());
536 let mut resolver = FileSystemScriptResolver::with_root(&parent);
537 for root in &options.search_roots {
538 resolver.add_root(root);
539 }
540 let mut host = DirectoryCompilerHost::new(resolver, output_directory);
541 if let Some(graphviz_directory) = &options.graphviz_directory {
542 host.set_graphviz_directory(graphviz_directory.clone());
543 }
544 host.set_simulate(options.simulate);
545
546 let mut driver = options.driver.clone();
547 driver.output_alias = input
548 .file_stem()
549 .and_then(|stem| stem.to_str())
550 .unwrap_or("scriptout")
551 .to_string();
552 if driver.graphviz_alias.is_none() {
553 driver.graphviz_alias = Some(driver.output_alias.clone());
554 }
555
556 match compile_file_with_host(&mut host, &input.to_string_lossy(), &driver) {
557 Ok(CompileFileOutcome::Compiled(_)) => {
558 report.successes += 1;
559 report.entries.push(BatchCompileEntry {
560 input,
561 status: BatchCompileStatus::Success,
562 outputs: host.written_paths().to_vec(),
563 error: None,
564 });
565 }
566 Ok(CompileFileOutcome::SkippedNoEntrypoint) => {
567 report.skips += 1;
568 report.entries.push(BatchCompileEntry {
569 input,
570 status: BatchCompileStatus::Skipped,
571 outputs: host.written_paths().to_vec(),
572 error: None,
573 });
574 }
575 Err(error) => {
576 let message = error.to_string();
577 report.errors += 1;
578 report.entries.push(BatchCompileEntry {
579 input,
580 status: BatchCompileStatus::Error,
581 outputs: host.written_paths().to_vec(),
582 error: Some(message),
583 });
584 if !options.continue_on_error {
585 return Err(BatchCompileError::Driver(error));
586 }
587 }
588 }
589 }
590
591 Ok(report)
592}
593
594fn collect_compile_inputs(
595 paths: &[PathBuf],
596 options: &BatchCompileOptions,
597) -> Result<Vec<PathBuf>, io::Error> {
598 let mut queue = BTreeSet::new();
599 for path in paths {
600 collect_one(path, options, &mut queue)?;
601 }
602 Ok(queue.into_iter().collect())
603}
604
605fn collect_one(
606 path: &Path,
607 options: &BatchCompileOptions,
608 queue: &mut BTreeSet<PathBuf>,
609) -> Result<(), io::Error> {
610 if path.is_file() {
611 if can_compile_file(path) {
612 queue.insert(path.to_path_buf());
613 }
614 return Ok(());
615 }
616 if path.is_dir() {
617 for entry in fs::read_dir(path)? {
618 let entry = entry?;
619 let file_type = entry.file_type()?;
620 let entry_path = entry.path();
621 if file_type.is_symlink() && !options.follow_symlinks {
622 continue;
623 }
624 if file_type.is_dir() {
625 if options.recurse {
626 collect_one(&entry_path, options, queue)?;
627 }
628 } else if file_type.is_file() && can_compile_file(&entry_path) {
629 queue.insert(entry_path);
630 }
631 }
632 }
633 Ok(())
634}
635
636fn can_compile_file(path: &Path) -> bool {
637 path.extension().and_then(|ext| ext.to_str()) == Some("nss")
638 && path.file_name().and_then(|name| name.to_str()) != Some("nwscript.nss")
639}
640
641#[cfg(test)]
642mod tests {
643 use std::{
644 collections::HashMap,
645 fs,
646 path::PathBuf,
647 time::{SystemTime, UNIX_EPOCH},
648 };
649
650 use nwnrs_types::resman::prelude::ResType;
651
652 use super::{
653 BatchCompileOptions, BatchCompileStatus, CompileFileOutcome, CompilerDriverOptions,
654 CompilerHost, CompilerHostError, FileSystemScriptResolver, compile_file_with_host,
655 compile_paths,
656 };
657 use crate::{NW_SCRIPT_SOURCE_RES_TYPE, ScriptResolver};
658
659 #[derive(Default)]
660 struct MemoryHost {
661 sources: HashMap<(ResType, String), Vec<u8>>,
662 files: Vec<(String, ResType, Vec<u8>)>,
663 graphviz: Vec<(String, String)>,
664 }
665
666 impl MemoryHost {
667 fn insert_source(&mut self, name: &str, contents: &str) {
668 self.sources.insert(
669 (NW_SCRIPT_SOURCE_RES_TYPE, name.to_ascii_lowercase()),
670 contents.as_bytes().to_vec(),
671 );
672 }
673 }
674
675 impl CompilerHost for MemoryHost {
676 fn resolve_script_bytes(
677 &self,
678 script_name: &str,
679 res_type: ResType,
680 ) -> Result<Option<Vec<u8>>, crate::SourceError> {
681 Ok(self
682 .sources
683 .get(&(res_type, script_name.to_ascii_lowercase()))
684 .cloned())
685 }
686
687 fn write_file(
688 &mut self,
689 file_name: &str,
690 res_type: ResType,
691 data: &[u8],
692 _binary: bool,
693 ) -> Result<(), CompilerHostError> {
694 self.files
695 .push((file_name.to_string(), res_type, data.to_vec()));
696 Ok(())
697 }
698
699 fn write_graphviz(&mut self, file_name: &str, dot: &str) -> Result<(), CompilerHostError> {
700 self.graphviz.push((file_name.to_string(), dot.to_string()));
701 Ok(())
702 }
703 }
704
705 fn unique_temp_dir(prefix: &str) -> PathBuf {
706 let nanos = SystemTime::now()
707 .duration_since(UNIX_EPOCH)
708 .unwrap_or_default()
709 .as_nanos();
710 std::env::temp_dir().join(format!("nwnrs-types-{prefix}-{nanos}"))
711 }
712
713 #[test]
714 fn compiles_through_callback_host_and_emits_graphviz() -> Result<(), Box<dyn std::error::Error>>
715 {
716 let mut host = MemoryHost::default();
717 host.insert_source("nwscript", "void PrintInteger(int n);");
718 host.insert_source("main", "void main() { PrintInteger(42); }");
719
720 let options = CompilerDriverOptions {
721 emit_graphviz: true,
722 output_alias: "main".to_string(),
723 ..CompilerDriverOptions::default()
724 };
725 let outcome = compile_file_with_host(&mut host, "main", &options)?;
726 assert!(matches!(outcome, CompileFileOutcome::Compiled(_)));
727 assert_eq!(host.files.len(), 2);
728 assert_eq!(host.graphviz.len(), 1);
729 assert_eq!(
730 host.graphviz
731 .first()
732 .map(|(_name, dot)| dot.contains("Function main")),
733 Some(true)
734 );
735 Ok(())
736 }
737
738 #[test]
739 fn batch_compiler_reports_success_skip_and_error() -> Result<(), Box<dyn std::error::Error>> {
740 let root = unique_temp_dir("batch");
741 fs::create_dir_all(&root)?;
742 fs::write(root.join("nwscript.nss"), "void PrintInteger(int n);")?;
743 fs::write(root.join("main.nss"), "void main() { PrintInteger(42); }")?;
744 fs::write(
745 root.join("helper.nss"),
746 "int AddOne(int n) { return n + 1; }",
747 )?;
748 fs::write(root.join("broken.nss"), "void main( {")?;
749
750 let mut options = BatchCompileOptions {
751 recurse: true,
752 continue_on_error: true,
753 simulate: true,
754 driver: CompilerDriverOptions {
755 emit_graphviz: true,
756 skip_missing_entrypoint: true,
757 ..CompilerDriverOptions::default()
758 },
759 ..BatchCompileOptions::default()
760 };
761 options.search_roots.push(root.clone());
762
763 let report = compile_paths(std::slice::from_ref(&root), &options)?;
764 assert_eq!(report.successes, 1);
765 assert_eq!(report.skips, 1);
766 assert_eq!(report.errors, 1);
767 assert!(
768 report
769 .entries
770 .iter()
771 .any(|entry| entry.status == BatchCompileStatus::Success)
772 );
773 assert!(
774 report
775 .entries
776 .iter()
777 .any(|entry| entry.status == BatchCompileStatus::Skipped)
778 );
779 assert!(
780 report
781 .entries
782 .iter()
783 .any(|entry| entry.status == BatchCompileStatus::Error)
784 );
785 fs::remove_dir_all(&root)?;
786 Ok(())
787 }
788
789 #[test]
790 fn filesystem_resolver_checks_roots_and_default_extension()
791 -> Result<(), Box<dyn std::error::Error>> {
792 let root = unique_temp_dir("resolver");
793 fs::create_dir_all(&root)?;
794 fs::write(root.join("test.nss"), "void main() {}")?;
795 let resolver = FileSystemScriptResolver::with_root(&root);
796 let resolved = resolver.resolve_script_bytes("test", NW_SCRIPT_SOURCE_RES_TYPE)?;
797 assert!(resolved.is_some());
798 fs::remove_dir_all(&root)?;
799 Ok(())
800 }
801}