use clap::Args;
use rossi::{
NamedComponent, NamedProject, to_multi_project_zip, write_multi_project_directory,
write_project_directory, write_project_zip_file,
};
use rossi_build::project::{
duplicate_component_name, project_from_text_components, projects_from_text_components,
};
use rossi_build::{BuildResult, build, is_normal_path_component};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use super::build_common::{
build_discovered, eb019_result, gate_after_write, gate_before_write, project_label,
repack_results, report_diagnostics,
};
use super::eventb_io::{self, CmdResult, InputFamily};
use super::proofs::ProofSource;
#[derive(Args)]
pub struct ExportArgs {
#[arg(required = true, value_name = "INPUT")]
inputs: Vec<PathBuf>,
#[arg(short, long, required = true, value_name = "OUTPUT")]
output: PathBuf,
#[arg(long)]
build: bool,
#[arg(long, value_name = "PATH", num_args = 0..=1, require_equals = true)]
proofs: Option<Option<PathBuf>>,
#[arg(short, long)]
verbose: bool,
}
struct ProjectPlan {
project: NamedProject,
local_dirs: Vec<PathBuf>,
}
fn flat_plan(
cli: &ExportArgs,
components: Vec<NamedComponent>,
local_dirs: Vec<PathBuf>,
) -> ProjectPlan {
ProjectPlan {
project: NamedProject {
name: project_name_from_output(&cli.output).to_string(),
components,
},
local_dirs,
}
}
pub fn run(cli: ExportArgs) -> ExitCode {
match run_inner(&cli) {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("rossi export: {e}");
ExitCode::from(1)
}
}
}
fn run_inner(cli: &ExportArgs) -> CmdResult<()> {
let building = cli.build || cli.proofs.is_some();
if eventb_io::stdin_is_sole_input(&cli.inputs)? {
if matches!(cli.proofs, Some(None)) {
return Err(
"--proofs without a path needs file or directory inputs; use --proofs=PATH".into(),
);
}
let source = eventb_io::read_stdin_to_string()?;
let components = eventb_io::parse_text_components("<stdin>", &source)?;
if building {
return export_built(cli, vec![flat_plan(cli, components, Vec::new())], false);
}
return write_flat_project(cli, &components);
}
for input in &cli.inputs {
eventb_io::ensure_input(input, InputFamily::Text)?;
}
if let [only] = cli.inputs.as_slice()
&& only.is_dir()
&& let Some(projects) = discover_text_projects(only, cli.verbose)?
{
if building {
let plans = projects
.into_iter()
.map(|(dir, project)| ProjectPlan {
project,
local_dirs: vec![dir],
})
.collect();
return export_built(cli, plans, true);
}
let projects: Vec<NamedProject> = projects.into_iter().map(|(_, p)| p).collect();
return write_multi_projects(cli, &projects);
}
let eventb_files = eventb_io::collect_eventb_files(&cli.inputs)?;
if eventb_files.is_empty() {
return Err("No .eventb or .txt files found in inputs".into());
}
let components = parse_eventb_files(&eventb_files, cli.verbose)?;
if building {
let plan = flat_plan(cli, components, flat_local_dirs(&cli.inputs));
return export_built(cli, vec![plan], false);
}
write_flat_project(cli, &components)
}
fn flat_local_dirs(inputs: &[PathBuf]) -> Vec<PathBuf> {
let mut dirs: Vec<PathBuf> = Vec::new();
for input in inputs {
let dir = if input.is_dir() {
input.clone()
} else {
eventb_io::parent_or_cwd(input)
};
if !dirs.contains(&dir) {
dirs.push(dir);
}
}
dirs
}
fn export_built(cli: &ExportArgs, plans: Vec<ProjectPlan>, multi: bool) -> CmdResult<()> {
let prefix_of = |plan: &ProjectPlan| {
if multi {
format!("{}/", plan.project.name)
} else {
String::new()
}
};
let (duplicated, plans): (Vec<ProjectPlan>, Vec<ProjectPlan>) = plans
.into_iter()
.partition(|p| duplicate_component_name(&p.project.components).is_some());
if !duplicated.is_empty() {
let results: Vec<(String, BuildResult)> = duplicated
.into_iter()
.map(|p| {
let prefix = prefix_of(&p);
(prefix, eb019_result(&p.project.name, p.project.components))
})
.collect();
report_diagnostics(&results);
let labels: Vec<&str> = results.iter().map(|(p, _)| project_label(p)).collect();
return Err(format!(
"duplicate component names in project(s) {}; nothing was written",
labels.join(", ")
)
.into());
}
let source = match &cli.proofs {
None => None,
Some(path) => Some(ProofSource::open(path.as_deref())?),
};
let mut proof_entries: Vec<(String, Vec<u8>)> = Vec::new();
if let Some(source) = &source {
for plan in &plans {
let prefix = prefix_of(plan);
let sub_project = multi.then_some(plan.project.name.as_str());
let files = source.for_project(sub_project, &plan.local_dirs)?;
if cli.verbose {
eprintln!(
"Attaching {} proof file(s) for {}",
files.len(),
project_label(&prefix)
);
}
for (basename, bytes) in files {
proof_entries.push((format!("{prefix}{basename}"), bytes));
}
}
}
let proof_count = proof_entries.len();
let projects: Vec<NamedProject> = plans.into_iter().map(|p| p.project).collect();
let (mut src_bytes, results) = if multi {
let (bytes, discovered) = projects_from_text_components(&projects)?;
(bytes, build_discovered(discovered))
} else {
let (bytes, project) =
project_from_text_components(&projects[0].name, &projects[0].components)?;
(bytes, vec![(String::new(), build(&project))])
};
if !proof_entries.is_empty() {
src_bytes = append_zip_entries(src_bytes, proof_entries)?;
}
let failed = gate_before_write(&results)?;
let bytes = repack_results(&src_bytes, &results)?;
if is_zip_output(&cli.output) {
eventb_io::ensure_parent_dir(&cli.output)?;
fs::write(&cli.output, &bytes)?;
} else {
extract_archive_to_dir(&bytes, &cli.output)?;
}
report_diagnostics(&results);
if cli.verbose {
let components: usize = projects.iter().map(|p| p.components.len()).sum();
let generated: usize = results.iter().map(|(_, r)| r.files.len()).sum();
eprintln!(
"Wrote {} component(s), {} generated file(s), and {} proof file(s) across {} project(s) to {}",
components,
generated,
proof_count,
results.len(),
cli.output.display()
);
}
gate_after_write(&results, &failed, "the output")
}
fn append_zip_entries(bytes: Vec<u8>, entries: Vec<(String, Vec<u8>)>) -> CmdResult<Vec<u8>> {
use std::io::Write;
let mut cursor = std::io::Cursor::new(bytes);
{
let mut writer = zip::ZipWriter::new_append(&mut cursor)?;
for (name, data) in entries {
let method = if name.ends_with(".bpr") {
zip::CompressionMethod::Deflated
} else {
zip::CompressionMethod::Stored
};
let options = zip::write::SimpleFileOptions::default().compression_method(method);
writer.start_file(name.as_str(), options)?;
writer.write_all(&data)?;
}
writer.finish()?;
}
Ok(cursor.into_inner())
}
fn extract_archive_to_dir(bytes: &[u8], out_dir: &Path) -> CmdResult<()> {
use std::io::Read;
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(bytes))?;
for i in 0..archive.len() {
let mut entry = archive.by_index(i)?;
if entry.is_dir() {
continue;
}
let name = entry.name().to_string();
if !name.split('/').all(is_normal_path_component) {
return Err(format!("unsafe archive entry name {name:?}").into());
}
let dest = out_dir.join(&name);
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent)?;
}
let mut contents = Vec::with_capacity(entry.size() as usize);
entry.read_to_end(&mut contents)?;
fs::write(dest, contents)?;
}
Ok(())
}
fn discover_text_projects(
dir: &Path,
verbose: bool,
) -> CmdResult<Option<Vec<(PathBuf, NamedProject)>>> {
let mut subdirs = Vec::new();
for entry in fs::read_dir(dir)? {
let entry = entry?;
let file_type = entry.file_type()?;
if file_type.is_file() {
if entry
.path()
.extension()
.and_then(|e| e.to_str())
.is_some_and(eventb_io::is_eventb_ext)
{
return Ok(None);
}
} else if file_type.is_dir() {
subdirs.push(entry.path());
}
}
subdirs.sort();
let mut projects = Vec::new();
for subdir in subdirs {
let files = eventb_io::collect_eventb_files(std::slice::from_ref(&subdir))?;
if files.is_empty() {
continue;
}
let name = subdir
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| format!("invalid project directory name: {}", subdir.display()))?
.to_string();
let components = parse_eventb_files(&files, verbose)?;
projects.push((subdir, NamedProject { name, components }));
}
Ok((!projects.is_empty()).then_some(projects))
}
fn parse_eventb_files(files: &[PathBuf], verbose: bool) -> CmdResult<Vec<NamedComponent>> {
let mut components = Vec::new();
for path in files {
if verbose {
eprintln!("Parsing: {}", path.display());
}
let source = fs::read_to_string(path)?;
components.extend(eventb_io::parse_text_components(
&path.display().to_string(),
&source,
)?);
}
Ok(components)
}
fn write_flat_project(cli: &ExportArgs, components: &[NamedComponent]) -> CmdResult<()> {
let project_name = project_name_from_output(&cli.output);
if is_zip_output(&cli.output) {
eventb_io::ensure_parent_dir(&cli.output)?;
write_project_zip_file(&cli.output, components, project_name)?;
} else {
write_project_directory(&cli.output, components, project_name)?;
}
if cli.verbose {
eprintln!(
"Wrote {} component(s) to {}",
components.len(),
cli.output.display()
);
}
Ok(())
}
fn write_multi_projects(cli: &ExportArgs, projects: &[NamedProject]) -> CmdResult<()> {
if is_zip_output(&cli.output) {
eventb_io::ensure_parent_dir(&cli.output)?;
let bytes = to_multi_project_zip(projects)?;
fs::write(&cli.output, bytes)?;
} else {
write_multi_project_directory(&cli.output, projects)?;
}
if cli.verbose {
let total: usize = projects.iter().map(|p| p.components.len()).sum();
eprintln!(
"Wrote {} component(s) across {} project(s) to {}",
total,
projects.len(),
cli.output.display()
);
}
Ok(())
}
fn is_zip_output(output: &Path) -> bool {
output
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(eventb_io::is_zip_ext)
}
fn project_name_from_output(output: &Path) -> &str {
output
.file_stem()
.and_then(|stem| stem.to_str())
.unwrap_or_default()
}