use std::path::{Path, PathBuf};
use std::process::Command;
use aion_package::{Package, PackageOptions, WorkflowVersion, package_project};
use crate::error::ToolchainError;
use crate::project;
use crate::workspace::Workspace;
pub struct CompileRequest<'a> {
pub template_root: &'a Path,
pub gleam_path: &'a Path,
pub source: &'a str,
}
#[derive(Clone, Debug)]
pub struct CompiledWorkflow {
pub package: Package,
pub version: WorkflowVersion,
pub workflow_type: String,
pub output_path: PathBuf,
}
pub fn compile_source(request: &CompileRequest<'_>) -> Result<CompiledWorkflow, ToolchainError> {
compile_source_with_entry(request, None)
}
pub fn compile_source_for_entry(
request: &CompileRequest<'_>,
entry_module: &str,
) -> Result<CompiledWorkflow, ToolchainError> {
compile_source_with_entry(request, Some(entry_module))
}
fn compile_source_with_entry(
request: &CompileRequest<'_>,
requested_entry: Option<&str>,
) -> Result<CompiledWorkflow, ToolchainError> {
project::validate_project_root(request.template_root)?;
let template_entry = project::single_entry_module(request.template_root)?;
let workspace = Workspace::stage(request.template_root)?;
let workspace_root = workspace.root();
let canonical_requested = requested_entry
.map(project::canonical_entry_module)
.transpose()?;
let explicit_entry = canonical_requested.is_some();
let entry_module = match &canonical_requested {
Some(entry) => entry.as_str(),
None => template_entry.as_str(),
};
if explicit_entry {
project::retarget_single_entry_module(workspace_root, entry_module)?;
let template_source = project::entry_module_source_path(workspace_root, &template_entry)?;
project::remove_entry_source(&template_source)?;
project::remove_staged_root_build(workspace_root)?;
project::retarget_root_package(workspace_root, entry_module)?;
}
let source_path = project::entry_module_source_path(workspace_root, entry_module)?;
project::write_entry_source(&source_path, request.source)?;
if explicit_entry {
build_project(workspace_root, request.gleam_path)?;
project::remove_generated_root_main_beam(workspace_root)?;
package_built_project(workspace_root)
} else {
compile_built_project(workspace_root, request.gleam_path)
}
}
fn compile_built_project(
project_root: &Path,
gleam_path: &Path,
) -> Result<CompiledWorkflow, ToolchainError> {
build_project(project_root, gleam_path)?;
package_built_project(project_root)
}
pub fn build_project(project_root: &Path, gleam_path: &Path) -> Result<(), ToolchainError> {
let output = Command::new(gleam_path)
.arg("build")
.current_dir(project_root)
.output()
.map_err(|source| ToolchainError::GleamSpawn {
gleam_path: gleam_path.to_path_buf(),
source,
})?;
if output.status.success() {
return Ok(());
}
Err(ToolchainError::TypeCheck {
diagnostics: combine_diagnostics(&output.stderr, &output.stdout),
})
}
fn combine_diagnostics(stderr: &[u8], stdout: &[u8]) -> String {
let stderr = String::from_utf8_lossy(stderr);
let stdout = String::from_utf8_lossy(stdout);
let stderr_trimmed = stderr.trim_end();
let stdout_trimmed = stdout.trim_end();
match (stderr_trimmed.is_empty(), stdout_trimmed.is_empty()) {
(false, false) => format!("{stderr_trimmed}\n\n{stdout_trimmed}"),
(false, true) => stderr_trimmed.to_owned(),
(true, false) => stdout_trimmed.to_owned(),
(true, true) => {
"gleam build failed with no diagnostic output on stderr or stdout".to_owned()
}
}
}
fn package_built_project(project_root: &Path) -> Result<CompiledWorkflow, ToolchainError> {
let report = package_project(project_root, &PackageOptions::default())?;
let mut built = report.packages;
let packaged = match built.len() {
1 => built.remove(0),
count => {
return Err(ToolchainError::InvalidProject {
message: format!(
"authoring project packaged {count} workflows; source submission requires exactly one"
),
});
}
};
Ok(CompiledWorkflow {
workflow_type: packaged.workflow_type,
output_path: packaged.output_path,
version: packaged.version,
package: packaged.package,
})
}
#[cfg(test)]
mod tests {
use super::combine_diagnostics;
#[test]
fn diagnostics_prefer_stderr_and_append_stdout() {
assert_eq!(combine_diagnostics(b"type error\n", b""), "type error");
assert_eq!(combine_diagnostics(b"", b"compiling\n"), "compiling");
assert_eq!(
combine_diagnostics(b"type error\n", b"compiling demo\n"),
"type error\n\ncompiling demo"
);
assert_eq!(
combine_diagnostics(b"", b""),
"gleam build failed with no diagnostic output on stderr or stdout"
);
}
}