use std::path::{Path, PathBuf};
use std::process::ExitCode;
use clap::Args;
use rossi_build::project::discover_projects;
use rossi_build::repack::repackage_zip_bytes_multi;
use rossi_build::{BuildResult, Project, Severity, build};
use rossi::{NamedComponent, to_zip};
use super::eventb_io::{self, InputKind};
#[derive(Args)]
pub struct BuildArgs {
pub input: PathBuf,
#[arg(short, long)]
pub output: Option<PathBuf>,
}
pub fn run_build_command(args: BuildArgs) -> ExitCode {
match run_build(&args.input, args.output.as_deref()) {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("rossi build: {e}");
ExitCode::from(1)
}
}
}
fn run_build(input: &Path, output: Option<&Path>) -> Result<(), Box<dyn std::error::Error>> {
let outcome = build_one(input)?;
let default_out;
let out_path = match output {
Some(p) => p,
None => {
let stem = input
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("project");
default_out = input
.parent()
.unwrap_or_else(|| Path::new("."))
.join(format!("{stem}.regen.zip"));
&default_out
}
};
write_output(input, out_path, &outcome)?;
report_diagnostics(&outcome);
let errors = outcome
.results
.iter()
.flat_map(|(_, r)| &r.diagnostics)
.filter(|d| d.severity == Severity::Error)
.count();
let files: usize = outcome.results.iter().map(|(_, r)| r.files.len()).sum();
eprintln!(
"rossi build: wrote {} -> {} ({} file(s) across {} project(s), {} error diagnostic(s))",
input.display(),
out_path.display(),
files,
outcome.results.len(),
errors
);
Ok(())
}
struct BuildOutcome {
results: Vec<(String, BuildResult)>,
archive_bytes: Option<Vec<u8>>,
}
fn build_one(input: &Path) -> Result<BuildOutcome, Box<dyn std::error::Error>> {
if input.is_dir() {
if !eventb_io::collect_rodin_xml_files(&[input.to_path_buf()])?.is_empty() {
let project = Project::from_directory(input)?;
let result = build(&project);
return Ok(BuildOutcome {
results: vec![(String::new(), result)],
archive_bytes: None,
});
}
let text_files = eventb_io::collect_eventb_files(&[input.to_path_buf()])?;
if text_files.is_empty() {
return Err(format!("no Event-B files found in {}", input.display()).into());
}
return build_from_text_files(&dir_project_name(input), &text_files);
}
match eventb_io::classify_file(input)? {
InputKind::Text => build_from_text_files(&file_project_name(input), &[input.to_path_buf()]),
InputKind::RodinXml => build_from_components(
&file_project_name(input),
vec![eventb_io::parse_rodin_xml_file(input)?],
),
InputKind::RodinZip => build_from_zip(input),
}
}
fn build_from_text_files(
name: &str,
files: &[PathBuf],
) -> Result<BuildOutcome, Box<dyn std::error::Error>> {
let mut components = Vec::new();
for path in files {
let source = std::fs::read_to_string(path)?;
components.extend(eventb_io::parse_text_components(
&path.display().to_string(),
&source,
)?);
}
build_from_components(name, components)
}
fn build_from_components(
name: &str,
components: Vec<NamedComponent>,
) -> Result<BuildOutcome, Box<dyn std::error::Error>> {
if components.is_empty() {
return Err("no Event-B components to build".into());
}
let bytes = to_zip(&components)?;
build_zip_bytes(name, bytes)
}
fn build_from_zip(input: &Path) -> Result<BuildOutcome, Box<dyn std::error::Error>> {
let bytes = std::fs::read(input)?;
build_zip_bytes(&file_project_name(input), bytes)
}
fn build_zip_bytes(
fallback_name: &str,
bytes: Vec<u8>,
) -> Result<BuildOutcome, Box<dyn std::error::Error>> {
let projects = discover_projects(&bytes, fallback_name)?;
if projects.is_empty() {
return Err("no Event-B projects found in archive".into());
}
let results = projects
.into_iter()
.map(|dp| {
let prefix = dp.prefix.clone();
(prefix, build(&dp.into_project()))
})
.collect();
Ok(BuildOutcome {
results,
archive_bytes: Some(bytes),
})
}
fn file_project_name(input: &Path) -> String {
input
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("project")
.to_string()
}
fn dir_project_name(input: &Path) -> String {
input
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("project")
.to_string()
}
fn write_output(
input: &Path,
out_path: &Path,
outcome: &BuildOutcome,
) -> Result<(), Box<dyn std::error::Error>> {
let is_zip_out = out_path
.extension()
.and_then(|s| s.to_str())
.is_some_and(|e| e.eq_ignore_ascii_case("zip"));
if is_zip_out {
write_zip(input, out_path, outcome)
} else {
write_dir(out_path, outcome)
}
}
fn write_zip(
input: &Path,
out_path: &Path,
outcome: &BuildOutcome,
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(parent) = out_path.parent()
&& !parent.as_os_str().is_empty()
{
std::fs::create_dir_all(parent)?;
}
let bytes = match &outcome.archive_bytes {
Some(b) => repackage_zip_bytes_multi(
b,
outcome
.results
.iter()
.map(|(prefix, result)| (prefix.as_str(), result)),
)?,
None => {
let empty = BuildResult {
files: vec![],
diagnostics: vec![],
};
let result = outcome.results.first().map_or(&empty, |(_, r)| r);
synthesize_flat_zip(input, result)?
}
};
std::fs::write(out_path, bytes)?;
Ok(())
}
fn write_dir(out_dir: &Path, outcome: &BuildOutcome) -> Result<(), Box<dyn std::error::Error>> {
std::fs::create_dir_all(out_dir)?;
let multi = outcome.results.len() > 1;
for (prefix, result) in &outcome.results {
let base = if multi {
let dir = out_dir.join(prefix.trim_end_matches('/'));
std::fs::create_dir_all(&dir)?;
dir
} else {
out_dir.to_path_buf()
};
for f in &result.files {
std::fs::write(base.join(&f.filename), &f.contents)?;
}
}
Ok(())
}
fn synthesize_flat_zip(
input: &Path,
result: &BuildResult,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
use zip::write::{SimpleFileOptions, ZipWriter};
let prefix = input
.file_name()
.and_then(|s| s.to_str())
.map(|s| format!("{s}/"))
.unwrap_or_default();
let mut cursor = std::io::Cursor::new(Vec::<u8>::new());
let mut w = ZipWriter::new(&mut cursor);
let opts = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
for f in &result.files {
w.start_file(format!("{prefix}{}", f.filename), opts)?;
use std::io::Write;
w.write_all(f.contents.as_bytes())?;
}
w.finish()?;
Ok(cursor.into_inner())
}
fn report_diagnostics(outcome: &BuildOutcome) {
let multi = outcome.results.len() > 1;
for (prefix, result) in &outcome.results {
if multi && !result.diagnostics.is_empty() {
let label = if prefix.is_empty() {
"(root)"
} else {
prefix.trim_end_matches('/')
};
eprintln!("--- {label} ---");
}
for d in &result.diagnostics {
eprintln!("{d}");
}
}
}