use crate::error::Error;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::process::Command;
const MIN_JAVA_VERSION: usize = 8;
pub(crate) const TLA_JAR: &str = "tla2tools-v1.8.0.jar";
pub(crate) const COMMUNITY_MODULES_JAR: &str = "CommunityModules-202103092123.jar";
pub(crate) const APALACHE_JAR: &str = "apalache-pkg-0.17.5-full.jar";
pub(crate) const JARS_CHECKSUM: &str =
"fa5c17bfe6adf631e2d0250de5a8f905e9e41a0ead837608f8c35d131077a9c2";
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
pub(crate) enum Jar {
Tla,
CommunityModules,
Apalache,
}
impl Jar {
pub(crate) fn path<P: AsRef<Path>>(&self, modelator_dir: P) -> PathBuf {
modelator_dir.as_ref().join(self.file_name())
}
const fn file_name(&self) -> &str {
match self {
Self::Tla => TLA_JAR,
Self::CommunityModules => COMMUNITY_MODULES_JAR,
Self::Apalache => APALACHE_JAR,
}
}
fn link(&self) -> String {
format!(
"https://github.com/informalsystems/modelator/raw/main/jars/{}",
self.file_name()
)
}
fn download<P: AsRef<Path>>(&self, modelator_dir: P) -> Result<(), Error> {
let modelator_dir = modelator_dir.as_ref();
let path = self.path(&modelator_dir);
let response = ureq::get(&self.link()).call()?;
let mut reader = response.into_reader();
let file = std::fs::File::create(path)?;
let mut file_writer = std::io::BufWriter::new(file);
std::io::copy(&mut reader, &mut file_writer)?;
Ok(())
}
const fn all() -> [Self; 3] {
[Self::Tla, Self::CommunityModules, Self::Apalache]
}
}
impl<'a> TryFrom<&'a str> for Jar {
type Error = &'a str;
fn try_from(file_name: &'a str) -> Result<Self, Self::Error> {
match file_name {
TLA_JAR => Ok(Self::Tla),
COMMUNITY_MODULES_JAR => Ok(Self::CommunityModules),
APALACHE_JAR => Ok(Self::Apalache),
_ => Err(file_name),
}
}
}
pub(crate) fn download_jars_if_necessary<P: AsRef<Path>>(modelator_dir: P) -> Result<(), Error> {
let existing_jars = existing_jars(&modelator_dir)?;
let missing_jars: HashSet<_> = Jar::all()
.into_iter()
.filter(|jar| !existing_jars.contains(jar))
.collect();
if !missing_jars.is_empty() {
println!(
"[modelator] Downloading model-checkers at \"{}\"...",
modelator_dir.as_ref().to_string_lossy()
);
for jar in missing_jars {
jar.download(&modelator_dir)?;
}
println!("[modelator] Done!");
check_java_version()?;
if !checksums_correct(&modelator_dir)? {
eprintln!("[modelator] Checksum of downloaded jars does not match the expected. Will try again!");
std::fs::remove_dir_all(&modelator_dir)?;
std::fs::create_dir(&modelator_dir)?;
return download_jars_if_necessary(modelator_dir);
}
}
Ok(())
}
fn existing_jars<P: AsRef<Path>>(modelator_dir: P) -> Result<HashSet<Jar>, Error> {
let existing_jars: HashSet<_> = list_jars(&modelator_dir)?
.into_iter()
.flat_map(|file_name| match file_name.as_str().try_into() {
Ok(jar) => Some(Ok(jar)),
Err(file_name) => {
match std::fs::remove_file(modelator_dir.as_ref().to_path_buf().join(file_name)) {
Err(e) => Some(Err(Error::IO(format!("IO error: {:?}", e)))),
_ => None,
}
}
})
.collect::<Result<_, Error>>()?;
assert!(
existing_jars.len() <= 3,
"[modelator] at most 3 jar files should have been downloaded"
);
Ok(existing_jars)
}
fn list_jars<P: AsRef<Path>>(modelator_dir: P) -> Result<HashSet<String>, Error> {
let mut jars = HashSet::new();
for file_name in crate::util::read_dir(modelator_dir)? {
if file_name.ends_with(".jar") {
jars.insert(file_name);
}
}
Ok(jars)
}
fn checksums_correct<P: AsRef<Path>>(modelator_dir: P) -> Result<bool, Error> {
let files_to_hash = list_jars(&modelator_dir)?
.into_iter()
.map(|filename| modelator_dir.as_ref().to_path_buf().join(filename))
.map(|path| crate::util::absolute_path(&path))
.collect();
let digest = crate::util::digest::digest_files(files_to_hash)?;
let hash = crate::util::digest::encode(digest);
tracing::debug!("jars checksum: {} | expected: {}", hash, JARS_CHECKSUM);
Ok(hash == JARS_CHECKSUM)
}
fn check_java_version() -> Result<(), Error> {
let mut cmd = Command::new("java");
cmd.arg("-XshowSettings:all").arg("-version");
tracing::debug!("{}", crate::util::cmd_show(&cmd));
match cmd.output() {
Ok(output) => {
let stderr = crate::util::cmd_output_to_string(&output.stderr);
let mut versions: Vec<_> = stderr
.lines()
.filter(|line| line.trim().starts_with("java.specification.version ="))
.collect();
tracing::debug!("java version {:?}", versions);
assert_eq!(
versions.len(),
1,
"[modelator] expected at most one 'java.specification.version'"
);
let version_parts: Vec<_> = versions.pop().unwrap().split('=').collect();
assert_eq!(
version_parts.len(),
2,
"[modelator] unexpected 'java.specification.version' format"
);
let version_number: usize = version_parts[1]
.trim()
.split('.')
.last()
.unwrap()
.parse()
.expect("[modelator] unexpected 'java.specification.version' format");
if version_number < MIN_JAVA_VERSION {
Err(Error::MinimumJavaVersion(version_number, MIN_JAVA_VERSION))
} else {
Ok(())
}
}
Err(err) => {
tracing::debug!("error checking Java version: {}", err);
Err(Error::MissingJava)
}
}
}