devalang_wasm/services/build/
pipeline.rs

1#![cfg(feature = "cli")]
2
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5use std::time::{Duration, Instant};
6
7use anyhow::Result;
8
9use crate::engine::audio::settings::{AudioBitDepth, AudioChannels, AudioFormat, ResampleQuality};
10use crate::language::syntax::ast::Statement;
11use crate::language::syntax::parser::driver::SimpleParser;
12use crate::tools::logger::Logger;
13
14use super::outputs::ast::AstBuilder;
15use super::outputs::audio::builder::AudioBuilder;
16use super::outputs::logs::LogWriter;
17
18#[derive(Debug, Clone)]
19pub struct BuildRequest {
20    pub entry_path: PathBuf,
21    pub output_root: PathBuf,
22    pub audio_formats: Vec<AudioFormat>,
23    pub bit_depth: AudioBitDepth,
24    pub channels: AudioChannels,
25    pub resample_quality: ResampleQuality,
26    pub sample_rate: u32,
27    pub bpm: f32,
28}
29
30#[derive(Debug, Clone)]
31pub struct BuildArtifacts {
32    pub primary_format: AudioFormat,
33    pub exported_formats: Vec<(AudioFormat, PathBuf)>,
34    pub bit_depth: AudioBitDepth,
35    pub channels: AudioChannels,
36    pub resample_quality: ResampleQuality,
37    pub sample_rate: u32,
38    pub module_name: String,
39    pub statements: Vec<Statement>,
40    pub ast_path: PathBuf,
41    pub primary_audio_path: PathBuf,
42    pub rms: f32,
43    pub audio_render_time: Duration,
44    pub audio_length: Duration,
45    pub total_duration: Duration,
46}
47
48#[derive(Clone)]
49pub struct ProjectBuilder {
50    logger: Arc<Logger>,
51    ast_builder: AstBuilder,
52    audio_builder: AudioBuilder,
53    log_writer: LogWriter,
54}
55
56impl ProjectBuilder {
57    pub fn new(logger: Arc<Logger>) -> Self {
58        let log_writer = LogWriter::new();
59        let audio_logger = logger.clone();
60        Self {
61            logger,
62            ast_builder: AstBuilder::new(),
63            audio_builder: AudioBuilder::new(log_writer, audio_logger),
64            log_writer,
65        }
66    }
67
68    pub fn build(&self, request: &BuildRequest) -> Result<BuildArtifacts> {
69        let build_start = Instant::now();
70        self.logger.action(format!(
71            "Building module from {}",
72            request.entry_path.display()
73        ));
74
75        let statements = self.parse(&request.entry_path)?;
76        let module_name = module_name_from_path(&request.entry_path);
77
78        let ast_path = self
79            .ast_builder
80            .write(&statements, &request.output_root, &module_name)?;
81
82        // Render audio output in all requested formats
83        use super::outputs::audio::builder::MultiFormatRenderSummary;
84        let MultiFormatRenderSummary {
85            primary_path,
86            primary_format,
87            exported_formats,
88            bit_depth,
89            rms,
90            render_time: audio_render_time,
91            audio_length,
92        } = self.audio_builder.render_all_formats(
93            &statements,
94            &request.entry_path,
95            &request.output_root,
96            &module_name,
97            &request.audio_formats,
98            request.bit_depth,
99            request.channels,
100            request.sample_rate,
101            request.resample_quality,
102            request.bpm,
103        )?;
104
105        // Clear logs before writing new entries
106        self.log_writer.clear(&request.output_root)?;
107
108        // Append build summary to logs
109        let formats_str = exported_formats
110            .iter()
111            .map(|(fmt, _)| format!("{:?}", fmt))
112            .collect::<Vec<_>>()
113            .join(", ");
114
115        self.log_writer.append(
116            &request.output_root,
117            &format!(
118                "Module '{}' built with {} statement(s); exported formats: [{}] ({} bits, {} ch, {:?})",
119                module_name,
120                statements.len(),
121                formats_str,
122                bit_depth.bits(),
123                request.channels.count(),
124                request.resample_quality
125            ),
126        )?;
127
128        let total_duration = build_start.elapsed();
129        self.logger.watch(format!(
130            "Build complete in {:.1} ms (audio regen {:.1} ms)",
131            total_duration.as_secs_f64() * 1000.0,
132            audio_render_time.as_secs_f64() * 1000.0
133        ));
134
135        Ok(BuildArtifacts {
136            primary_format,
137            exported_formats,
138            bit_depth,
139            channels: request.channels,
140            resample_quality: request.resample_quality,
141            sample_rate: request.sample_rate,
142            module_name,
143            statements,
144            ast_path,
145            primary_audio_path: primary_path,
146            rms,
147            audio_render_time,
148            audio_length,
149            total_duration,
150        })
151    }
152
153    fn parse(&self, entry: impl AsRef<Path>) -> Result<Vec<Statement>> {
154        SimpleParser::parse_file(entry)
155    }
156}
157
158fn module_name_from_path(path: &Path) -> String {
159    // Preserve directory structure in module name relative to "scripts" or "demos" folder
160    // This will be used to recreate the directory structure in the output folder
161
162    let file_stem = path
163        .file_stem()
164        .map(|stem| stem.to_string_lossy().to_string())
165        .unwrap_or_else(|| "module".to_string());
166
167    if let Some(parent) = path.parent() {
168        let components: Vec<_> = parent.components().collect();
169
170        // Find if we have a "scripts" or "demos" directory and capture from there
171        let mut start_idx = None;
172        for (idx, component) in components.iter().enumerate() {
173            let comp_str = component.as_os_str().to_string_lossy();
174            if comp_str == "scripts" || comp_str == "demos" {
175                start_idx = Some(idx);
176                break;
177            }
178        }
179
180        if let Some(idx) = start_idx {
181            // Collect path components from the found "scripts" or "demos" onwards
182            let mut path_buf = PathBuf::new();
183            for component in &components[idx..] {
184                path_buf.push(component);
185            }
186            path_buf.push(&file_stem);
187
188            // Convert to forward slashes for consistency
189            path_buf.to_string_lossy().replace('\\', "/")
190        } else {
191            file_stem
192        }
193    } else {
194        file_stem
195    }
196}