use clap::Args;
use rossi::{NamedComponent, NamedProject, PrettyPrinter};
use rossi_build::project::discover_projects;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use super::eventb_io::{self, CmdResult, InputFamily};
#[derive(Args)]
pub struct ImportArgs {
#[arg(required = true, value_name = "INPUT")]
inputs: Vec<PathBuf>,
#[arg(short, long, required = true, value_name = "OUTPUT")]
output: PathBuf,
#[arg(long)]
ascii: bool,
#[arg(long, value_name = "STR")]
indent: Option<String>,
#[arg(long, num_args = 0..=1, default_missing_value = "", require_equals = true, value_name = "ORDER")]
merge: Option<String>,
#[arg(short, long)]
verbose: bool,
}
pub fn run(cli: ImportArgs) -> ExitCode {
match run_inner(&cli) {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("rossi import: {e}");
ExitCode::from(1)
}
}
}
fn run_inner(cli: &ImportArgs) -> CmdResult<()> {
for input in &cli.inputs {
eventb_io::ensure_input(input, InputFamily::Rodin)?;
}
let projects = collect_projects(cli)?;
let total: usize = projects.iter().map(|p| p.components.len()).sum();
if total == 0 {
return Err("No Event-B components found in input files".into());
}
let project_count = projects.len();
let printer = PrettyPrinter {
use_unicode: !cli.ascii,
indent: cli.indent.clone().unwrap_or_else(|| " ".to_string()),
private_use_glyphs: false,
};
let multi = project_count > 1;
match (cli.merge.as_deref(), multi) {
(Some(order), false) => write_merged_flat(cli, &printer, projects, order)?,
(Some(order), true) => write_merged_per_project(cli, &printer, projects, order)?,
(None, false) => write_files_flat(cli, &printer, projects)?,
(None, true) => write_files_per_project(cli, &printer, projects)?,
}
if cli.verbose {
eprintln!(
"Wrote {total} component(s) across {project_count} project(s) to {}",
cli.output.display()
);
}
Ok(())
}
fn collect_projects(cli: &ImportArgs) -> CmdResult<Vec<NamedProject>> {
let mut projects: Vec<NamedProject> = Vec::new();
let mut loose: Vec<NamedComponent> = Vec::new();
for input in &cli.inputs {
if cli.verbose {
eprintln!("Reading Rodin input: {}", input.display());
}
if input.is_dir() {
let components = parse_rodin_directory(input)?;
if cli.verbose {
eprintln!(" Found {} component(s)", components.len());
}
if components.is_empty() {
continue;
}
projects.push(NamedProject {
name: eventb_io::safe_path_segment(&path_name(input)),
components,
});
} else {
match input.extension().and_then(|e| e.to_str()) {
Some(ext) if ext.eq_ignore_ascii_case("zip") => {
let bytes = fs::read(input)?;
let fallback = file_stem(input);
for dp in discover_projects(&bytes, &fallback)? {
if dp.components.is_empty() {
continue;
}
let components = dp
.components
.into_iter()
.map(|pc| NamedComponent {
filename: pc.filename,
component: pc.component,
})
.collect::<Vec<_>>();
if cli.verbose {
eprintln!(
" Found {} component(s) in project {}",
components.len(),
dp.name
);
}
projects.push(NamedProject {
name: eventb_io::safe_path_segment(dp.prefix.trim_end_matches('/')),
components,
});
}
}
Some(ext) if eventb_io::is_rodin_xml_ext(ext) => {
loose.push(eventb_io::parse_rodin_xml_file(input)?);
}
_ => return Err(format!("Unsupported Rodin input: {}", input.display()).into()),
}
}
}
if !loose.is_empty() {
projects.push(NamedProject {
name: "components".to_string(),
components: loose,
});
}
Ok(projects)
}
fn write_merged_flat(
cli: &ImportArgs,
printer: &PrettyPrinter,
projects: Vec<NamedProject>,
order: &str,
) -> CmdResult<()> {
let mut components = into_single(projects);
if !order.is_empty() {
warn_unmatched_order(&component_names(&components), order);
reorder_components(&mut components, order);
}
eventb_io::ensure_parent_dir(&cli.output)?;
fs::write(&cli.output, render_merged(printer, &components))?;
Ok(())
}
fn write_merged_per_project(
cli: &ImportArgs,
printer: &PrettyPrinter,
projects: Vec<NamedProject>,
order: &str,
) -> CmdResult<()> {
fs::create_dir_all(&cli.output)?;
if !order.is_empty() {
let all: Vec<String> = projects
.iter()
.flat_map(|p| component_names(&p.components))
.collect();
warn_unmatched_order(&all, order);
}
for mut project in projects {
if !order.is_empty() {
reorder_components(&mut project.components, order);
}
let path = cli.output.join(format!("{}.eventb", project.name));
fs::write(&path, render_merged(printer, &project.components))?;
if cli.verbose {
eprintln!(" Wrote {}", path.display());
}
}
Ok(())
}
fn write_files_flat(
cli: &ImportArgs,
printer: &PrettyPrinter,
projects: Vec<NamedProject>,
) -> CmdResult<()> {
fs::create_dir_all(&cli.output)?;
let components = into_single(projects);
write_component_files(printer, &components, &cli.output, cli.verbose)
}
fn write_files_per_project(
cli: &ImportArgs,
printer: &PrettyPrinter,
projects: Vec<NamedProject>,
) -> CmdResult<()> {
for project in projects {
let dir = cli.output.join(&project.name);
fs::create_dir_all(&dir)?;
write_component_files(printer, &project.components, &dir, cli.verbose)?;
}
Ok(())
}
fn into_single(projects: Vec<NamedProject>) -> Vec<NamedComponent> {
projects
.into_iter()
.next()
.map(|p| p.components)
.unwrap_or_default()
}
fn component_names(components: &[NamedComponent]) -> Vec<String> {
components
.iter()
.map(|c| c.component.name().to_string())
.collect()
}
fn render_merged(printer: &PrettyPrinter, components: &[NamedComponent]) -> String {
let mut out = String::new();
for (i, named) in components.iter().enumerate() {
if i > 0 {
out.push('\n');
}
out.push_str(&printer.print_component(&named.component));
out.push('\n');
}
out
}
fn write_component_files(
printer: &PrettyPrinter,
components: &[NamedComponent],
dir: &Path,
verbose: bool,
) -> CmdResult<()> {
for named in components {
let path = dir.join(format!("{}.eventb", named.component.name()));
let text = printer.print_component(&named.component);
fs::write(&path, format!("{text}\n"))?;
if verbose {
eprintln!(" Wrote {}", path.display());
}
}
Ok(())
}
fn path_name(path: &Path) -> String {
path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("project")
.to_string()
}
fn file_stem(path: &Path) -> String {
path.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("project")
.to_string()
}
fn parse_rodin_directory(dir: &Path) -> CmdResult<Vec<NamedComponent>> {
let mut components = Vec::new();
for file in eventb_io::collect_rodin_xml_files(&[dir.to_path_buf()])? {
components.push(eventb_io::parse_rodin_xml_file(&file)?);
}
Ok(components)
}
fn warn_unmatched_order(all_names: &[String], order: &str) {
for name in order.split(',').map(|s| s.trim()) {
if !all_names.iter().any(|n| n == name) {
eprintln!("Warning: '{name}' does not match any component");
}
}
}
fn reorder_components(components: &mut [NamedComponent], order: &str) {
let names: Vec<&str> = order.split(',').map(|s| s.trim()).collect();
let order_pos = |c: &NamedComponent| -> usize {
let n = c.component.name();
names
.iter()
.position(|&name| name == n)
.unwrap_or(usize::MAX)
};
components.sort_by_key(order_pos);
}