use std::path::PathBuf;
use std::process::Command;
use std::sync::LazyLock;
use crate::path::resolve_project_relative_path;
#[cfg(test)]
#[path = "cairo0_compiler_test.rs"]
pub mod test;
#[derive(Debug, Eq, PartialEq)]
pub struct CairoLangVersion<'a>(pub &'a str);
pub const EXPECTED_CAIRO0_STARKNET_COMPILE_VERSION: CairoLangVersion<'static> =
CairoLangVersion("0.14.0.1");
pub const EXPECTED_CAIRO0_VERSION: CairoLangVersion<'static> = CairoLangVersion("0.14.1a0");
pub(crate) static PIP_REQUIREMENTS_FILE: LazyLock<PathBuf> =
LazyLock::new(|| resolve_project_relative_path("scripts/requirements.txt").unwrap());
pub(crate) static STARKNET_DEPRECATED_COMPILE_REQUIREMENTS_FILE: LazyLock<PathBuf> =
LazyLock::new(|| {
resolve_project_relative_path(
"crates/blockifier_test_utils/resources/blockifier-test-utils-requirements.txt",
)
.unwrap()
});
pub(crate) fn enter_venv_instructions(script_type: &Cairo0Script) -> String {
format!(
r#"
python3 -m venv sequencer_venv
. sequencer_venv/bin/activate
pip install -r {:?}"#,
script_type.requirements_file_path()
)
}
#[derive(Clone, Copy, Debug)]
pub enum Cairo0Script {
Compile,
Format,
StarknetCompileDeprecated,
}
impl Cairo0Script {
pub fn script_name(&self) -> &'static str {
match self {
Self::Compile => "cairo-compile",
Self::Format => "cairo-format",
Self::StarknetCompileDeprecated => "starknet-compile-deprecated",
}
}
pub fn required_version(&self) -> CairoLangVersion<'static> {
match self {
Self::Compile | Self::Format => EXPECTED_CAIRO0_VERSION,
Self::StarknetCompileDeprecated => EXPECTED_CAIRO0_STARKNET_COMPILE_VERSION,
}
}
pub fn requirements_file_path(&self) -> &PathBuf {
match self {
Self::Compile | Self::Format => &PIP_REQUIREMENTS_FILE,
Self::StarknetCompileDeprecated => &STARKNET_DEPRECATED_COMPILE_REQUIREMENTS_FILE,
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum Cairo0ScriptVersionError {
#[error(
"{script:?} version is not correct: required {required}, got {existing}. Are you in the \
venv? If not, run the following commands:\n{}",
enter_venv_instructions(script)
)]
IncorrectVersion { script: Cairo0Script, existing: String, required: String },
#[error(
"{error}. Are you in the venv? If not, run the following commands:\n{}",
enter_venv_instructions(script)
)]
CompilerNotFound { script: Cairo0Script, error: String },
}
#[derive(thiserror::Error, Debug)]
pub enum Cairo0CompilerError {
#[error(transparent)]
Cairo0CompilerVersion(#[from] Cairo0ScriptVersionError),
#[error("Cairo root path not found at {0:?}.")]
CairoRootNotFound(PathBuf),
#[error("Failed to compile the program. Error: {0}.")]
CompileError(String),
#[error("Invalid path unicode: {0:?}.")]
InvalidPath(PathBuf),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("No file found at path {0:?}.")]
SourceFileNotFound(PathBuf),
}
pub fn cairo0_script_correct_version(
script_type: &Cairo0Script,
) -> Result<(), Cairo0ScriptVersionError> {
let expected_version = script_type.required_version();
let script = script_type.script_name();
let version = match Command::new(script).arg("--version").output() {
Ok(output) => String::from_utf8_lossy(&output.stdout).to_string(),
Err(error) => {
return Err(Cairo0ScriptVersionError::CompilerNotFound {
script: *script_type,
error: format!("Failed to get {script} version: {error}."),
});
}
};
if CairoLangVersion(version.trim().replace("==", " ").split(" ").nth(1).ok_or(
Cairo0ScriptVersionError::CompilerNotFound {
script: *script_type,
error: format!("No {script} version found."),
},
)?) != expected_version
{
return Err(Cairo0ScriptVersionError::IncorrectVersion {
script: *script_type,
existing: version,
required: expected_version.0.to_string(),
});
}
Ok(())
}
pub fn compile_cairo0_program(
path_to_main: PathBuf,
cairo_root_path: PathBuf,
) -> Result<Vec<u8>, Cairo0CompilerError> {
let script_type = Cairo0Script::Compile;
cairo0_script_correct_version(&script_type)?;
if !path_to_main.exists() {
return Err(Cairo0CompilerError::SourceFileNotFound(path_to_main));
}
if !cairo_root_path.exists() {
return Err(Cairo0CompilerError::CairoRootNotFound(cairo_root_path));
}
let mut compile_command = Command::new(script_type.script_name());
compile_command.args([
path_to_main.to_str().ok_or(Cairo0CompilerError::InvalidPath(path_to_main.clone()))?,
"--debug_info_with_source",
"--cairo_path",
cairo_root_path
.to_str()
.ok_or(Cairo0CompilerError::InvalidPath(cairo_root_path.clone()))?,
]);
let compile_output = compile_command.output()?;
if !compile_output.status.success() {
return Err(Cairo0CompilerError::CompileError(
String::from_utf8_lossy(&compile_output.stderr).trim().to_string(),
));
}
Ok(compile_output.stdout)
}