use std::{
collections::{BTreeMap, BTreeSet},
env,
fmt::Write,
fs::{self},
io::{self},
path::{Path, PathBuf},
sync::Arc,
};
use assembly::{
diagnostics::{IntoDiagnostic, Result},
utils::Serializable,
Assembler, DefaultSourceManager, KernelLibrary, Library, LibraryNamespace, Report,
};
use regex::Regex;
use walkdir::WalkDir;
const BUILD_GENERATED_FILES_IN_SRC: bool = option_env!("BUILD_GENERATED_FILES_IN_SRC").is_some();
const ASSETS_DIR: &str = "assets";
const ASM_DIR: &str = "asm";
const ASM_MIDEN_DIR: &str = "miden";
const ASM_NOTE_SCRIPTS_DIR: &str = "note_scripts";
const ASM_ACCOUNT_COMPONENTS_DIR: &str = "account_components";
const SHARED_DIR: &str = "shared";
const ASM_TX_KERNEL_DIR: &str = "kernels/transaction";
const KERNEL_V0_RS_FILE: &str = "src/transaction/procedures/kernel_v0.rs";
const KERNEL_ERRORS_FILE: &str = "src/errors/tx_kernel_errors.rs";
fn main() -> Result<()> {
println!("cargo:rerun-if-changed={ASM_DIR}");
println!("cargo::rerun-if-env-changed=BUILD_GENERATED_FILES_IN_SRC");
let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let build_dir = env::var("OUT_DIR").unwrap();
let src = Path::new(&crate_dir).join(ASM_DIR);
let dst = Path::new(&build_dir).to_path_buf();
copy_directory(src, &dst);
let source_dir = dst.join(ASM_DIR);
let target_dir = Path::new(&build_dir).join(ASSETS_DIR);
let mut assembler =
compile_tx_kernel(&source_dir.join(ASM_TX_KERNEL_DIR), &target_dir.join("kernels"))?;
let miden_lib = compile_miden_lib(&source_dir, &target_dir, assembler.clone())?;
assembler.add_library(miden_lib)?;
compile_note_scripts(
&source_dir.join(ASM_NOTE_SCRIPTS_DIR),
&target_dir.join(ASM_NOTE_SCRIPTS_DIR),
assembler.clone(),
)?;
compile_account_components(&target_dir.join(ASM_ACCOUNT_COMPONENTS_DIR), assembler)?;
generate_kernel_error_constants(&source_dir)?;
Ok(())
}
fn compile_tx_kernel(source_dir: &Path, target_dir: &Path) -> Result<Assembler> {
let shared_path = Path::new(ASM_DIR).join(SHARED_DIR);
let kernel_namespace = LibraryNamespace::new("kernel").expect("namespace should be valid");
let mut assembler = build_assembler(None)?;
assembler.add_modules_from_dir(kernel_namespace.clone(), &shared_path)?;
let kernel_lib = KernelLibrary::from_dir(
source_dir.join("api.masm"),
Some(source_dir.join("lib")),
assembler,
)?;
generate_kernel_proc_hash_file(kernel_lib.clone())?;
let output_file = target_dir.join("tx_kernel").with_extension(Library::LIBRARY_EXTENSION);
kernel_lib.write_to_file(output_file).into_diagnostic()?;
let assembler = build_assembler(Some(kernel_lib))?;
let mut main_assembler = assembler.clone();
main_assembler.add_modules_from_dir(kernel_namespace.clone(), &shared_path)?;
main_assembler.add_modules_from_dir(kernel_namespace, &source_dir.join("lib"))?;
let main_file_path = source_dir.join("main.masm").clone();
let kernel_main = main_assembler.assemble_program(main_file_path)?;
let masb_file_path = target_dir.join("tx_kernel.masb");
kernel_main.write_to_file(masb_file_path).into_diagnostic()?;
#[cfg(any(feature = "testing", test))]
{
let mut kernel_lib_assembler = assembler.clone();
let kernel_namespace =
"kernel".parse::<LibraryNamespace>().expect("invalid base namespace");
kernel_lib_assembler.add_modules_from_dir(kernel_namespace.clone(), &shared_path)?;
let test_lib =
Library::from_dir(source_dir.join("lib"), kernel_namespace, kernel_lib_assembler)
.unwrap();
let masb_file_path =
target_dir.join("kernel_library").with_extension(Library::LIBRARY_EXTENSION);
test_lib.write_to_file(masb_file_path).into_diagnostic()?;
}
Ok(assembler)
}
fn generate_kernel_proc_hash_file(kernel: KernelLibrary) -> Result<()> {
if !BUILD_GENERATED_FILES_IN_SRC {
return Ok(());
}
let (_, module_info, _) = kernel.into_parts();
let to_exclude = BTreeSet::from_iter(["exec_kernel_proc"]);
let offsets_filename = Path::new(ASM_DIR).join(ASM_MIDEN_DIR).join("kernel_proc_offsets.masm");
let offsets = parse_proc_offsets(&offsets_filename)?;
let generated_procs: BTreeMap<usize, String> = module_info
.procedures()
.filter(|(_, proc_info)| !to_exclude.contains::<str>(proc_info.name.as_ref()))
.map(|(_, proc_info)| {
let name = proc_info.name.to_string();
let Some(&offset) = offsets.get(&name) else {
panic!("Offset constant for function `{name}` not found in `{offsets_filename:?}`");
};
(offset, format!(" // {name}\n digest!(\"{}\"),", proc_info.digest))
})
.collect();
let proc_count = generated_procs.len();
let generated_procs: String = generated_procs.into_iter().enumerate().map(|(index, (offset, txt))| {
if index != offset {
panic!("Offset constants in the file `{offsets_filename:?}` are not contiguous (missing offset: {index})");
}
txt
}).collect::<Vec<_>>().join("\n");
fs::write(
KERNEL_V0_RS_FILE,
format!(
r#"/// This file is generated by build.rs, do not modify
use miden_objects::{{digest, Digest}};
// KERNEL V0 PROCEDURES
// ================================================================================================
/// Hashes of all dynamically executed procedures from the kernel 0.
pub const KERNEL0_PROCEDURES: [Digest; {proc_count}] = [
{generated_procs}
];
"#,
),
)
.into_diagnostic()
}
fn parse_proc_offsets(filename: impl AsRef<Path>) -> Result<BTreeMap<String, usize>> {
let regex: Regex = Regex::new(r"^const\.(?P<name>\w+)_OFFSET\s*=\s*(?P<offset>\d+)").unwrap();
let mut result = BTreeMap::new();
for line in fs::read_to_string(filename).into_diagnostic()?.lines() {
if let Some(captures) = regex.captures(line) {
result.insert(
captures["name"].to_string().to_lowercase(),
captures["offset"].parse().into_diagnostic()?,
);
}
}
Ok(result)
}
fn compile_miden_lib(
source_dir: &Path,
target_dir: &Path,
mut assembler: Assembler,
) -> Result<Library> {
let source_dir = source_dir.join(ASM_MIDEN_DIR);
let shared_path = Path::new(ASM_DIR).join(SHARED_DIR);
let miden_namespace = "miden".parse::<LibraryNamespace>().expect("invalid base namespace");
assembler.add_modules_from_dir(miden_namespace.clone(), &shared_path)?;
let miden_lib = Library::from_dir(source_dir, miden_namespace, assembler)?;
let output_file = target_dir.join("miden").with_extension(Library::LIBRARY_EXTENSION);
miden_lib.write_to_file(output_file).into_diagnostic()?;
Ok(miden_lib)
}
fn compile_note_scripts(source_dir: &Path, target_dir: &Path, assembler: Assembler) -> Result<()> {
if let Err(e) = fs::create_dir_all(target_dir) {
println!("Failed to create note_scripts directory: {}", e);
}
for masm_file_path in get_masm_files(source_dir).unwrap() {
let code = assembler.clone().assemble_program(masm_file_path.clone())?;
let bytes = code.to_bytes();
let masb_file_name = masm_file_path.file_name().unwrap().to_str().unwrap();
let mut masb_file_path = target_dir.join(masb_file_name);
masb_file_path.set_extension("masb");
fs::write(masb_file_path, bytes).unwrap();
}
Ok(())
}
const BASIC_WALLET_CODE: &str = "
export.::miden::contracts::wallets::basic::receive_asset
export.::miden::contracts::wallets::basic::create_note
export.::miden::contracts::wallets::basic::move_asset_to_note
";
const RPO_FALCON_AUTH_CODE: &str = "
export.::miden::contracts::auth::basic::auth_tx_rpo_falcon512
";
const BASIC_FUNGIBLE_FAUCET_CODE: &str = "
export.::miden::contracts::faucets::basic_fungible::distribute
export.::miden::contracts::faucets::basic_fungible::burn
";
fn compile_account_components(target_dir: &Path, assembler: Assembler) -> Result<()> {
for (component_name, component_code) in [
("basic_wallet", BASIC_WALLET_CODE),
("rpo_falcon_512", RPO_FALCON_AUTH_CODE),
("basic_fungible_faucet", BASIC_FUNGIBLE_FAUCET_CODE),
] {
let component_library = assembler.clone().assemble_library([component_code])?;
let component_file_path =
target_dir.join(component_name).with_extension(Library::LIBRARY_EXTENSION);
component_library.write_to_file(component_file_path).into_diagnostic()?;
}
Ok(())
}
fn build_assembler(kernel: Option<KernelLibrary>) -> Result<Assembler> {
kernel
.map(|kernel| Assembler::with_kernel(Arc::new(DefaultSourceManager::default()), kernel))
.unwrap_or_default()
.with_debug_mode(cfg!(feature = "with-debug-info"))
.with_library(miden_stdlib::StdLibrary::default())
}
fn copy_directory<T: AsRef<Path>, R: AsRef<Path>>(src: T, dst: R) {
let mut prefix = src.as_ref().canonicalize().unwrap();
prefix.pop();
let target_dir = dst.as_ref().join(ASM_DIR);
if !target_dir.exists() {
fs::create_dir_all(target_dir).unwrap();
}
let dst = dst.as_ref();
let mut todo = vec![src.as_ref().to_path_buf()];
while let Some(goal) = todo.pop() {
for entry in fs::read_dir(goal).unwrap() {
let path = entry.unwrap().path();
if path.is_dir() {
let src_dir = path.canonicalize().unwrap();
let dst_dir = dst.join(src_dir.strip_prefix(&prefix).unwrap());
if !dst_dir.exists() {
fs::create_dir_all(&dst_dir).unwrap();
}
todo.push(src_dir);
} else {
let dst_file = dst.join(path.strip_prefix(&prefix).unwrap());
fs::copy(&path, dst_file).unwrap();
}
}
}
}
fn get_masm_files<P: AsRef<Path>>(dir_path: P) -> io::Result<Vec<PathBuf>> {
let mut files = Vec::new();
let path = dir_path.as_ref();
if path.is_dir() {
match fs::read_dir(path) {
Ok(entries) => {
for entry in entries {
match entry {
Ok(file) => {
let file_path = file.path();
if is_masm_file(&file_path)? {
files.push(file_path);
}
},
Err(e) => println!("Error reading directory entry: {}", e),
}
}
},
Err(e) => println!("Error reading directory: {}", e),
}
} else {
println!("cargo:rerun-The specified path is not a directory.");
}
Ok(files)
}
fn is_masm_file(path: &Path) -> io::Result<bool> {
if let Some(extension) = path.extension() {
let extension = extension
.to_str()
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "invalid UTF-8 filename"))?
.to_lowercase();
Ok(extension == "masm")
} else {
Ok(false)
}
}
fn generate_kernel_error_constants(kernel_source_dir: &Path) -> Result<()> {
if !BUILD_GENERATED_FILES_IN_SRC {
return Ok(());
}
let mut errors = BTreeMap::new();
for entry in WalkDir::new(kernel_source_dir) {
let entry = entry.into_diagnostic()?;
if !is_masm_file(entry.path()).into_diagnostic()? {
continue;
}
let file_contents = std::fs::read_to_string(entry.path()).into_diagnostic()?;
extract_kernel_errors(&mut errors, &file_contents)?;
}
let mut error_codes = BTreeMap::new();
for (error_name, error) in errors.iter() {
if let Some(existing_error_name) = error_codes.get(&error.code) {
return Err(Report::msg(format!("Transaction kernel error code 0x{} is used multiple times; Non-exhaustive list: ERR_{existing_error_name}, ERR_{error_name}", error.code)));
}
error_codes.insert(error.code.clone(), error_name);
}
let error_file_content = generate_kernel_errors(errors)?;
std::fs::write(KERNEL_ERRORS_FILE, error_file_content).into_diagnostic()?;
Ok(())
}
fn extract_kernel_errors(
errors: &mut BTreeMap<ErrorName, ExtractedError>,
file_contents: &str,
) -> Result<()> {
let regex =
Regex::new(r"(# (?<message>.*)\n)?const\.ERR_(?<name>.*)=0x(?<code>[\dABCDEFabcdef]*)")
.unwrap();
for capture in regex.captures_iter(file_contents) {
let error_name = capture
.name("name")
.expect("error name should be captured")
.as_str()
.trim()
.to_owned();
let error_code = capture
.name("code")
.expect("error code should be captured")
.as_str()
.trim()
.to_owned();
let error_message = match capture.name("message") {
Some(message) => message.as_str().trim().to_owned(),
None => {
return Err(Report::msg(format!("error message for constant ERR_{error_name} not found; add a comment above the constant to add an error message")));
},
};
if let Some(ExtractedError { code: existing_error_code, .. }) = errors.get(&error_name) {
if existing_error_code != &error_code {
return Err(Report::msg(format!("Transaction kernel error constant ERR_{error_name} is already defined elsewhere but its error code is different")));
}
}
errors.insert(error_name, ExtractedError { code: error_code, message: error_message });
}
Ok(())
}
fn is_new_error_category<'a>(last_error: &mut Option<&'a str>, current_error: &'a str) -> bool {
let is_new = match last_error {
Some(last_err) => {
let last_category =
last_err.split("_").next().expect("there should be at least one entry");
let new_category =
current_error.split("_").next().expect("there should be at least one entry");
last_category != new_category
},
None => false,
};
last_error.replace(current_error);
is_new
}
fn generate_kernel_errors(errors: BTreeMap<ErrorName, ExtractedError>) -> Result<String> {
let mut output = String::new();
writeln!(
output,
"// This file is generated by build.rs, do not modify manually.
// It is generated by extracting errors from the masm files in the `miden-lib/asm` directory.
//
// To add a new error, define a constant in masm of the pattern `const.ERR_<CATEGORY>_...`.
// Try to fit the error into a pre-existing category if possible (e.g. Account, Prologue,
// Non-Fungible-Asset, ...).
//
// The comment directly above the constant will be interpreted as the error message for that error.
// KERNEL ASSERTION ERROR
// ================================================================================================
"
)
.into_diagnostic()?;
let mut last_error = None;
for (error_name, ExtractedError { code, .. }) in errors.iter() {
if is_new_error_category(&mut last_error, error_name) {
writeln!(output).into_diagnostic()?;
}
writeln!(output, "pub const ERR_{error_name}: u32 = 0x{code};").into_diagnostic()?;
}
writeln!(output).into_diagnostic()?;
writeln!(output, "pub const TX_KERNEL_ERRORS: [(u32, &str); {}] = [", errors.len())
.into_diagnostic()?;
let mut last_error = None;
for (error_name, ExtractedError { message, .. }) in errors.iter() {
if is_new_error_category(&mut last_error, error_name) {
writeln!(output).into_diagnostic()?;
}
writeln!(output, r#" (ERR_{error_name}, "{message}"),"#).into_diagnostic()?;
}
writeln!(output, "];").into_diagnostic()?;
Ok(output)
}
type ErrorName = String;
struct ExtractedError {
code: String,
message: String,
}