diff --git a/bin/dist/asc/linux/asc b/bin/dist/asc/linux/asc new file mode 100644 index 00000000..bb894a4d Binary files /dev/null and b/bin/dist/asc/linux/asc differ diff --git a/bin/dist/asc/windows/asc.exe b/bin/dist/asc/windows/asc.exe new file mode 100644 index 00000000..a05088e4 Binary files /dev/null and b/bin/dist/asc/windows/asc.exe differ diff --git a/bin/src/commands/build.rs b/bin/src/commands/build.rs index 43afa5f6..be3b7172 100644 --- a/bin/src/commands/build.rs +++ b/bin/src/commands/build.rs @@ -4,7 +4,9 @@ use crate::{ context::{self, Context}, error::Error, executor::Executor, - modules::{pbo::Collapse, Binarize, Files, Hooks, Rapifier, SQFCompiler}, + modules::{ + asc::ArmaScriptCompiler, pbo::Collapse, Binarize, Files, Hooks, Rapifier, SQFCompiler, + }, report::Report, }; @@ -94,6 +96,7 @@ pub fn executor(ctx: Context, matches: &ArgMatches) -> Executor { if matches.get_one::("no-bin") != Some(&true) { executor.add_module(Box::::default()); } + executor.add_module(Box::::default()); executor.add_module(Box::::default()); executor.init(); diff --git a/bin/src/commands/dev.rs b/bin/src/commands/dev.rs index a8e68c02..449812c8 100644 --- a/bin/src/commands/dev.rs +++ b/bin/src/commands/dev.rs @@ -5,7 +5,10 @@ use crate::{ context::Context, error::Error, executor::Executor, - modules::{pbo::Collapse, Binarize, FilePatching, Files, Hooks, Rapifier, SQFCompiler}, + modules::{ + asc::ArmaScriptCompiler, pbo::Collapse, Binarize, FilePatching, Files, Hooks, Rapifier, + SQFCompiler, + }, report::Report, }; @@ -118,6 +121,7 @@ pub fn context(matches: &ArgMatches, launch_optionals: &[String]) -> Result::default()); executor.add_module(Box::::default()); executor.add_module(Box::::default()); + executor.add_module(Box::::default()); executor.add_module(Box::::default()); executor.add_module(Box::::default()); if matches.get_one::("binarize") == Some(&true) { diff --git a/bin/src/error.rs b/bin/src/error.rs index 2d3ac5db..c7ad6ca1 100644 --- a/bin/src/error.rs +++ b/bin/src/error.rs @@ -5,6 +5,10 @@ pub enum Error { #[error("`.hemtt/project.toml` not found")] ConfigNotFound, + #[error("ASC: {0}")] + #[cfg(not(target_os = "macos"))] + ArmaScriptCompiler(String), + #[error("Unable to create link: {0}")] #[allow(dead_code)] // Unused on Linux and Mac Link(String), diff --git a/bin/src/modules/asc.rs b/bin/src/modules/asc.rs new file mode 100644 index 00000000..fd65acf5 --- /dev/null +++ b/bin/src/modules/asc.rs @@ -0,0 +1,231 @@ +use std::{ + fs::{create_dir_all, File}, + io::{Read, Write}, + process::Command, + sync::{ + atomic::{AtomicU16, Ordering}, + Arc, RwLock, + }, +}; + +use hemtt_preprocessor::Processor; +use rayon::prelude::{IntoParallelRefIterator, ParallelIterator}; +use rust_embed::RustEmbed; +use serde::Serialize; +use time::Instant; + +use crate::{context::Context, error::Error, report::Report}; + +use super::Module; + +#[cfg(windows)] +#[derive(RustEmbed)] +#[folder = "dist/asc/windows"] +struct Distributables; + +#[cfg(not(windows))] +#[derive(RustEmbed)] +#[folder = "dist/asc/linux"] +struct Distributables; + +#[derive(Default)] +pub struct ArmaScriptCompiler; + +#[cfg(windows)] +const SOURCE: [&str; 1] = ["asc.exe"]; + +#[cfg(not(windows))] +const SOURCE: [&str; 1] = ["asc"]; + +impl Module for ArmaScriptCompiler { + fn name(&self) -> &'static str { + "ArmaScriptCompiler" + } + + fn init(&mut self, ctx: &Context) -> Result { + let asc = ctx.tmp().join("asc"); + trace!("using asc folder at {:?}", asc.display()); + create_dir_all(asc.join("output"))?; + Ok(Report::new()) + } + + #[allow(clippy::too_many_lines)] + fn pre_build(&self, ctx: &Context) -> Result { + let mut out_file = + File::create(".hemttout/asc.log").expect("Unable to create `.hemttout/asc.log`"); + let mut config = ASCConfig::new(); + let tmp = ctx.tmp().join("asc"); + for file in SOURCE { + let out = tmp.join(file); + trace!("unpacking {:?} to {:?}", file, out.display()); + let mut f = File::create(&out)?; + f.write_all(&Distributables::get(file).unwrap().data)?; + #[cfg(target_os = "linux")] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(out, PermissionsExt::from_mode(0o744))?; + } + } + let sqf_ext = Some(String::from("sqf")); + let files = Arc::new(RwLock::new(Vec::new())); + let start = Instant::now(); + let mut root_dirs = Vec::new(); + for addon in ctx.addons() { + if !root_dirs.contains(&addon.prefix().main_prefix()) { + root_dirs.push(addon.prefix().main_prefix()); + } + let tmp_addon = tmp.join(addon.prefix().as_pathbuf()); + create_dir_all(&tmp_addon)?; + let mut entries = Vec::new(); + for entry in ctx.workspace().join(addon.folder())?.walk_dir()? { + if entry.is_file()? { + if entry.extension() != sqf_ext { + continue; + } + entries.push(entry); + } + } + entries + .par_iter() + .map(|entry| { + let processed = Processor::run(entry)?; + let source = tmp_addon.join( + entry + .as_str() + .trim_start_matches(&format!("/{}/", addon.folder())), + ); + let parent = source.parent().unwrap(); + if !parent.exists() { + std::mem::drop(create_dir_all(parent)); + } + let mut f = File::create(source)?; + f.write_all(processed.as_str().as_bytes())?; + files.write().unwrap().push(( + format!( + "{}{}", + addon + .prefix() + .to_string() + .replace('\\', "/") + .trim_end_matches(&addon.folder().replacen( + "optionals/", + "addons/", + 1 + )) + .trim_end_matches(&addon.folder()), + entry.as_str().to_string().trim_start_matches('/'), + ), + entry + .as_str() + .to_string() + .trim_start_matches('/') + .to_string(), + )); + Ok(()) + }) + .collect::>()?; + } + debug!( + "ASC Preprocess took {:?}", + start.elapsed().whole_milliseconds() + ); + for root in root_dirs { + config.add_input_dir(root.to_string()); + } + config.set_output_dir(tmp.join("output").display().to_string()); + let include = tmp.join("source").join("include"); + if include.exists() { + config.add_include_dir(include.display().to_string()); + } + config.set_worker_threads(num_cpus::get()); + let mut f = File::create(tmp.join("sqfc.json"))?; + f.write_all(serde_json::to_string_pretty(&config)?.as_bytes())?; + std::env::set_current_dir(&tmp)?; + let start = Instant::now(); + let command = Command::new(tmp.join(SOURCE[0])).output()?; + out_file.write_all(&command.stdout)?; + out_file.write_all(&command.stderr)?; + if String::from_utf8(command.stdout.clone()) + .unwrap() + .contains("Parse Error") + { + warn!("ASC 'Parse Error' - check .hemttout/asc.log"); + } + if command.status.success() { + debug!("ASC took {:?}", start.elapsed().whole_milliseconds()); + } else { + return Err(Error::ArmaScriptCompiler( + String::from_utf8(command.stdout).unwrap(), + )); + } + std::env::set_current_dir(ctx.project_folder())?; + let tmp_output = tmp.join("output"); + let counter = AtomicU16::new(0); + for (src, dst) in &*files.read().unwrap() { + let from = tmp_output.join(&format!("{src}c")); + let to = ctx.workspace().join(&format!("{dst}c"))?; + if !from.exists() { + // sqf that have parse errors OR just empty//no-code + debug!("asc didn't process {}", src); + continue; + } + let mut f = File::open(from)?; + let mut data = Vec::new(); + f.read_to_end(&mut data)?; + to.create_file()?.write_all(&data)?; + counter.fetch_add(1, Ordering::Relaxed); + } + info!("Compiled {} sqf files", counter.load(Ordering::Relaxed)); + Ok(Report::new()) + } +} + +#[derive(Default, Serialize)] +pub struct ASCConfig { + #[serde(rename = "inputDirs")] + input_dirs: Vec, + #[serde(rename = "outputDir")] + output_dir: String, + #[serde(rename = "includePaths")] + include_dirs: Vec, + #[serde(rename = "excludeList")] + exclude_list: Vec, + #[serde(rename = "workerThreads")] + worker_threads: usize, +} + +impl ASCConfig { + #[must_use] + pub const fn new() -> Self { + Self { + input_dirs: vec![], + output_dir: String::new(), + include_dirs: vec![], + exclude_list: vec![], + worker_threads: 2, + } + } + + pub fn add_input_dir(&mut self, dir: String) { + if self.input_dirs.contains(&dir) { + return; + } + self.input_dirs.push(dir); + } + + pub fn set_output_dir(&mut self, dir: String) { + self.output_dir = dir; + } + + pub fn add_include_dir(&mut self, dir: String) { + self.include_dirs.push(dir); + } + + pub fn add_exclude(&mut self, dir: &str) { + self.exclude_list.push(dir.replace('/', "\\")); + } + + pub fn set_worker_threads(&mut self, threads: usize) { + self.worker_threads = threads; + } +} diff --git a/bin/src/modules/mod.rs b/bin/src/modules/mod.rs index 203e8d93..238874f9 100644 --- a/bin/src/modules/mod.rs +++ b/bin/src/modules/mod.rs @@ -1,6 +1,7 @@ use crate::{context::Context, error::Error, report::Report}; pub mod archive; +pub mod asc; pub mod hook; pub mod pbo; diff --git a/bin/src/modules/sqf.rs b/bin/src/modules/sqf.rs index 37040361..a88b70df 100644 --- a/bin/src/modules/sqf.rs +++ b/bin/src/modules/sqf.rs @@ -47,14 +47,14 @@ impl Module for SQFCompiler { } match hemtt_sqf::parser::run(&database, &processed) { Ok(sqf) => { - let mut out = entry.with_extension("sqfc")?.create_file()?; + // let mut out = entry.with_extension("sqfc")?.create_file()?; let (warnings, errors) = analyze(&sqf, Some(ctx.config()), &processed, Some(addon), &database); for warning in warnings { report.warn(warning); } if errors.is_empty() { - sqf.compile_to_writer(&processed, &mut out)?; + // sqf.compile_to_writer(&processed, &mut out)?; counter.fetch_add(1, Ordering::Relaxed); } for error in errors {