Skip to content

Commit

Permalink
Merge pull request #3 from pclewis/cudd-3.0.0
Browse files Browse the repository at this point in the history
Integration with cudd-3.0.0
  • Loading branch information
daemontus authored Sep 14, 2021
2 parents fdbc35b + ec60521 commit b0ced69
Show file tree
Hide file tree
Showing 9 changed files with 2,311 additions and 1,419 deletions.
15 changes: 14 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cudd-sys"
version = "0.1.1"
version = "1.0.0"
authors = ["Philip Lewis <[email protected]>"]
build = "build.rs"
description = "Bindings for CU Decision Diagram library (CUDD)"
Expand All @@ -9,3 +9,16 @@ license = "CC0-1.0"

[dependencies]
libc = "0.2"

[build-dependencies]
autotools = "0.2.3"

[features]
default = ["build_cudd"]
# When disabled, the build script will not attempt to build CUDD and will just silently continue.
# This is necessary for building documentation on docs.rs without access to the internet. For other commands
# (aside from `cargo doc`), this will fail to produce a binary during linking.
build_cudd = []

[package.metadata.docs.rs]
no-default-features = true
34 changes: 28 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,33 @@

# Rust Bindings for the CUDD library

Allows usage of the CUDD decision diagram library from Rust (tested on Linux and macOS). Uses version `2.5.1` of CUDD available from the unofficial [Github mirror](https://github.com/ivmai/cudd).
This crate provides unsafe Rust bindings for the University of Colorado decision diagram
package (CUDD), including the DDDMP serialisation library. It uses version `3.0.0` of CUDD
available from the unofficial [Github mirror](https://github.com/ivmai/cudd) and compiles on
Linux and MacOS (you should be also able to build CUDD on Windows using cygwin, but the project
is not set-up to do it automatically).

To learn more about CUDD, check out the [manual](https://add-lib.scce.info/assets/documents/cudd-manual.pdf) or [API documentation](https://add-lib.scce.info/assets/doxygen-cudd-documentation/cudd_8h.html).
> On Linux and macOS, you should ideally have `autoconf`, `automake` and `libtool` installed to build CUDD. And of course, some C/C++ compiler (`clang`, `gcc`, etc.).
At the moment, the bindings are functional, but there are some TODOs that need to be addressed:
- Support for latest CUDD (3.0.0).
- CUDD uses C macros for some basic functionality. Port these to Rust.
- Everything is provided as raw C bindings. It would be nice to have *some* type-safe wrappers, at least for basic stuff.
In the root module, you will find declarations of the C structs and types used
throughout CUDD. The main API of the CUDD package is then exported in `::cudd`. However,
CUDD also includes other "public" functionality (multiway-branching trees, extended
double precision numbers, serialisation, ...) which can be found in the remaining modules.

In some cases, there are macro and constant definitions which cannot be directly exported
to Rust. These have been re-implemented and should have their own documentation.
For the functions which are re-exported without change, please refer to the original
[CUDD doxygen](https://add-lib.scce.info/assets/doxygen-cudd-documentation/) and
[manual](https://add-lib.scce.info/assets/documents/cudd-manual.pdf). The documentation
of the DDDMP library is available
[here](https://www.cs.rice.edu/~lm30/RSynth/CUDD/dddmp/doc/dddmpExt.html).

**Completeness:** The main CUDD API should be fully reproduced here (except for one small
issue with `f128` numbers). The remaining modules may still be incomplete: if you need
a function that isn't exported yet, let us know in the issues.

**Correctness:** Unfortunately, CUDD cannot be processed using `bindgen`, so the API was
reproduced using a semi-automated method with a manual validation step (bunch of regexes
that a human makes sure didn't break anything ;)). As such, it is possible that there
are some minor problems that need to be sorted out. Please file an issue if you see any
unexpected behaviour or segfaults.
195 changes: 69 additions & 126 deletions build.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
// build.rs
extern crate autotools;

use autotools::Config;
use std::env;
use std::fs;
use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use std::process::Command;
use MD5Status::{Mismatch, Unknown};

const PACKAGE_URL: &str = "https://github.com/ivmai/cudd/archive/refs/tags/cudd-2.5.1.tar.gz";
const PACKAGE_MD5: &str = "42283a52ff815c5ca8231b6927318fbf";
const PACKAGE_URL: &str = "https://github.com/ivmai/cudd/archive/refs/tags/cudd-3.0.0.tar.gz";
const PACKAGE_MD5: &str = "edca9c69528256ca8ae37be9cedef73f";

#[derive(Debug)]
enum FetchError {
Expand Down Expand Up @@ -38,7 +39,7 @@ fn run_command(cmd: &mut Command) -> Result<(String, String), FetchError> {
String::from_utf8(output.stderr).unwrap(),
))
} else {
println!("Command {:?} exited with status {}", cmd, output.status);
eprintln!("Command {:?} exited with status {}", cmd, output.status);
Err(FetchError::CommandError(output.status))
};
}
Expand All @@ -47,146 +48,88 @@ fn run_command(cmd: &mut Command) -> Result<(String, String), FetchError> {
fn fetch_package(out_dir: &str, url: &str, md5: &str) -> Result<(PathBuf, MD5Status), FetchError> {
let out_path = Path::new(&out_dir);
let target_path = out_path.join(Path::new(url).file_name().unwrap());

let md5_result = {
let target_path_str = target_path.to_str().unwrap();
let meta = fs::metadata(&target_path);

match meta {
Ok(m) => {
if m.is_file() {
println!("{} already exists, skipping download", target_path_str)
} else {
return Err(FetchError::PathExists);
}
}
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
println!("Downloading {} to {}", url, target_path_str);
run_command(&mut Command::new("curl").args(&[
"-L",
url,
"-o",
target_path_str,
]))?;
} else {
return Err(FetchError::IOError(e));
}
}
let target_path_str = target_path.clone().into_os_string().into_string().unwrap();

match target_path.metadata() {
Err(error) if error.kind() == ErrorKind::NotFound => {
// Path does not exist! Start download...
println!("Downloading {} to {}", url, target_path_str);
let mut command = Command::new("curl");
command.args(&["-L", url, "-o", target_path_str.as_str()]);
run_command(&mut command)?;
}
Ok(data) if data.is_file() => {
println!("{} exists. Skipping download.", target_path_str);
}
Ok(_) => return Err(FetchError::PathExists),
Err(error) => return Err(FetchError::from(error)),
}

run_command(&mut Command::new("md5sum").arg(target_path_str))
.or_else(|_| run_command(&mut Command::new("md5").arg(target_path_str)))
// Now run md5 sum check:
let mut command_1 = Command::new("md5sum");
command_1.arg(target_path.clone());
let mut command_2 = Command::new("md5");
command_2.arg(target_path.clone());
let md5_result = run_command(&mut command_1).or_else(|_| run_command(&mut command_2));

let md5_status = match md5_result {
Err(_) => MD5Status::Unknown,
Ok((output, _)) if output.contains(md5) => MD5Status::Match,
_ => MD5Status::Mismatch,
};

Ok((
target_path,
match md5_result {
Err(_) => MD5Status::Unknown,
Ok((out, _)) => {
// md5sum outputs "<md5> <file>", md5 on OSX outputs "MD5 (<file>) = <md5>"
if out.contains(md5) {
MD5Status::Match
} else {
MD5Status::Mismatch
}
}
},
))
Ok((target_path, md5_status))
}

fn replace_lines(path: &Path, replacements: Vec<(&str, &str)>) -> Result<u32, std::io::Error> {
let mut lines_replaced = 0;
let new_path = path.with_extension(".new");

{
let f_in = File::open(&path)?;
let f_out = File::create(&new_path)?;
let reader = BufReader::new(&f_in);
let mut writer = BufWriter::new(&f_out);

'read: for line in reader.lines() {
let line = line.unwrap();
for &(original, replacement) in &replacements {
if line.starts_with(&original) {
writeln!(writer, "{}", replacement)?;
lines_replaced += 1;
continue 'read;
}
}
writeln!(writer, "{}", line)?;
}
fn main() -> Result<(), String> {
let build_cudd = env::var_os("CARGO_FEATURE_BUILD_CUDD").is_some();
if !build_cudd {
// If silent build is active, don't do anything.
return Ok(());
}

fs::remove_file(&path)?;
fs::rename(&new_path, &path)?;

Ok(lines_replaced)
}

fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let out_dir = env::var("OUT_DIR")
.map_err(|_| "Environmental variable `OUT_DIR` not defined.".to_string())?;

let (tar_path, md5_status) = fetch_package(&out_dir, PACKAGE_URL, PACKAGE_MD5).unwrap();
let (tar_path, md5_status) = fetch_package(&out_dir, PACKAGE_URL, PACKAGE_MD5)
.map_err(|e| format!("Error downloading CUDD package: {:?}.", e))?;
let tar_path_str = tar_path.to_str().unwrap().to_string();

match md5_status {
MD5Status::Match => (),
MD5Status::Unknown => println!("No md5 available, skipping package validation"),
MD5Status::Mismatch => panic!("MD5 mismatch on package"),
Unknown => eprintln!("WARNING: MD5 not computed. Package validation skipped."),
Mismatch => return Err("CUDD package MD5 hash mismatch.".to_string()),
_ => (),
}

let untarred_path = tar_path.with_extension("").with_extension(""); // kill .tar and .gz
if !untarred_path.exists() {
// Get cudd.tar.gz path without extensions.
let cudd_path = tar_path.with_extension("").with_extension("");
let cudd_path_str = cudd_path.clone().into_os_string().into_string().unwrap();

if !cudd_path.exists() {
// Create the destination directory.
std::fs::create_dir_all(untarred_path.clone()).unwrap();
std::fs::create_dir_all(cudd_path.clone())
.map_err(|e| format!("Cannot create CUDD directory: {:?}", e))?;
}

// untar package, ignoring the name of the top level folder, dumping into untarred_path instead.
let untarred_path_str = untarred_path
.clone()
.into_os_string()
.into_string()
.unwrap();
run_command(Command::new("tar").args(&[
// un-tar package, ignoring the name of the top level folder, dumping into cudd_path instead.
let mut tar_command = Command::new("tar");
tar_command.args(&[
"xf",
tar_path.to_str().unwrap(),
&tar_path_str,
"--strip-components=1",
"-C",
&untarred_path_str,
]))
.unwrap();

// patch Makefile
let lines_replaced = replace_lines(
&untarred_path.join("Makefile"),
vec![
// need PIC so our rust lib can be dynamically linked
("ICFLAGS\t= -g -O3", "ICFLAGS\t= -g -O3 -fPIC"),
// remove "nanotrav" from DIRS, it doesn't compile on OSX
(
"DIRS\t= $(BDIRS)", // (matches prefix)
"DIRS\t= $(BDIRS)",
),
],
)
.unwrap();
if lines_replaced != 2 {
panic!("Replaced {} lines in Makefile, expected 1", lines_replaced);
}

// build libraries (execute make)
run_command(Command::new("make").current_dir(&untarred_path)).unwrap();
&cudd_path_str,
]);
run_command(&mut tar_command).map_err(|e| format!("Error decompressing CUDD: {:?}", e))?;

// Move all libs to output directory so we don't have to specify each directory
run_command(
Command::new("sh")
.current_dir(&out_dir)
.args(&["-c", "cp cudd-*/*/*.a ."]),
)
.unwrap();
// Enable dddmp when building.
let build_output = Config::new(cudd_path).enable("dddmp", None).build();

println!("cargo:rustc-flags=-L {}", out_dir);
println!(
"cargo:rustc-flags=-l static=cudd -l static=mtr -l static=util -l static=epd -l static=st"
"cargo:rustc-link-search=native={}",
build_output.join("lib").display()
);
println!("cargo:rustc-link-lib=static=cudd");

Ok(())
}
Loading

0 comments on commit b0ced69

Please sign in to comment.