#![allow(warnings)]
#![feature(where_clause_attrs)]
use std::{
env,
thread,
time::{Duration, Instant},
collections::{VecDeque, HashMap, HashSet},
io::{stdout, Read, Write, Cursor, BufRead, BufReader},
sync::{mpsc, Arc, OnceLock, Mutex, atomic::{AtomicBool, AtomicUsize, Ordering}},
process::{Command as ProcessCommand, Stdio},
path::{Path, PathBuf, MAIN_SEPARATOR},
fs::{self, File},
};
use console::{style, Emoji};
use indicatif::{HumanDuration, MultiProgress, ProgressBar, ProgressStyle};
use rand::prelude::IndexedRandom;
use rand::Rng;
use clap::{arg, command, value_parser, Arg, ArgAction, Command};
use colored::Colorize;
use tempfile::TempDir;
use anyhow::{Context, Result};
use serde_json::Value;
use zip::write::FileOptions;
use zip::CompressionMethod;
use zip::ZipWriter;
use mech_syntax::*;
use mech_core::*;
use mech_interpreter::*;
const VERSION: &str = env!("CARGO_PKG_VERSION");
static ERROR_MESSAGE: OnceLock<Arc<Mutex<Option<String>>>> = OnceLock::new();
static CANCELLED: OnceLock<Arc<AtomicBool>> = OnceLock::new();
static BUILD_DATA: OnceLock<Arc<Mutex<BuildData>>> = OnceLock::new();
static BUILD_DIR: &str = "./build";
#[derive(Debug, Default)]
pub struct BuildData {
pub sources: Vec<String>,
pub paths: Vec<PathBuf>,
pub packages: Vec<String>,
pub trees: Arc<Mutex<HashMap<String, Program>>>,
pub bytecode: Vec<u8>,
pub build_project_dir: Option<PathBuf>,
pub temp_dir: Option<TempDir>,
pub final_artifact: Option<PathBuf>,
pub output_name: Option<String>,
pub features: HashSet<FeatureFlag>,
}
fn init_cancel_flag() {
CANCELLED.set(Arc::new(AtomicBool::new(false))).ok();
ERROR_MESSAGE.set(Arc::new(Mutex::new(None))).ok();
BUILD_DATA.set(Arc::new(Mutex::new(BuildData::default())));
}
fn set_features(features: HashSet<FeatureFlag>) {
if let Some(data) = BUILD_DATA.get() {
let mut data = data.lock().unwrap();
data.features = features;
} else {
panic!("BuildData not initialized!");
}
}
fn get_features() -> HashSet<FeatureFlag> {
if let Some(data) = BUILD_DATA.get() {
let data = data.lock().unwrap();
data.features.clone()
} else {
panic!("BuildData not initialized!");
}
}
fn set_output_name(path: impl Into<String>) {
if let Some(data) = BUILD_DATA.get() {
let mut data = data.lock().unwrap();
data.output_name = Some(path.into());
} else {
panic!("BuildData not initialized!");
}
}
fn get_output_name() -> Option<String> {
if let Some(data) = BUILD_DATA.get() {
let data = data.lock().unwrap();
data.output_name.clone()
} else {
None
}
}
fn drop_temp_dir() {
if let Some(data) = BUILD_DATA.get() {
let mut data = data.lock().unwrap();
data.temp_dir.take();
} else {
panic!("BuildData not initialized!");
}
}
fn get_final_artifact_path() -> Option<String> {
if let Some(data) = BUILD_DATA.get() {
let data = data.lock().unwrap();
data.final_artifact.as_ref().map(|p| p.canonicalize().unwrap_or_else(|_| p.clone()).display().to_string())
} else {
None
}
}
fn set_final_artifact_path(path: impl Into<PathBuf>) {
if let Some(data) = BUILD_DATA.get() {
let mut data = data.lock().unwrap();
data.final_artifact = Some(path.into());
} else {
panic!("BuildData not initialized!");
}
}
fn save_temp_dir(temp: TempDir) {
if let Some(data) = BUILD_DATA.get() {
let mut data = data.lock().unwrap();
data.temp_dir = Some(temp);
} else {
panic!("BuildData not initialized!");
}
}
fn set_bytecode(bytes: Vec<u8>) {
if let Some(data) = BUILD_DATA.get() {
let mut data = data.lock().unwrap();
data.bytecode = bytes;
} else {
panic!("BuildData not initialized!");
}
}
fn set_build_project_dir(path: impl Into<PathBuf>) {
if let Some(data) = BUILD_DATA.get() {
let mut data = data.lock().unwrap();
data.build_project_dir = Some(path.into());
} else {
panic!("BuildData not initialized!");
}
}
pub fn get_build_project_dir() -> PathBuf {
if let Some(data) = BUILD_DATA.get() {
let data = data.lock().unwrap();
data.build_project_dir.clone().unwrap().clone()
} else {
panic!("BuildData not initialized!");
}
}
pub fn add_tree(path: impl Into<String>, tree: Program) {
if let Some(data) = BUILD_DATA.get() {
let mut data = data.lock().unwrap();
data.trees.lock().unwrap().insert(path.into(), tree);
} else {
panic!("BuildData not initialized!");
}
}
pub fn get_trees() -> Arc<Mutex<HashMap<String, Program>>> {
if let Some(data) = BUILD_DATA.get() {
let mut data = data.lock().unwrap();
data.trees.clone()
} else {
panic!("BuildData not initialized!");
}
}
pub fn get_build_data() -> Option<Arc<Mutex<BuildData>>> {
BUILD_DATA.get().cloned()
}
pub fn add_path(path: impl Into<PathBuf>) {
if let Some(data) = BUILD_DATA.get() {
let mut data = data.lock().unwrap();
data.paths.push(path.into());
} else {
panic!("BuildData not initialized!");
}
}
pub fn add_source(path: impl Into<String>) {
if let Some(data) = BUILD_DATA.get() {
let mut data = data.lock().unwrap();
data.sources.push(path.into());
} else {
panic!("BuildData not initialized!");
}
}
pub fn get_sources() -> Vec<String> {
BUILD_DATA
.get()
.map(|data| data.lock().unwrap().sources.clone())
.unwrap_or_default()
}
fn cancel_all(msg: &str) {
if let Some(flag) = CANCELLED.get() {
flag.store(true, Ordering::Relaxed);
}
if let Some(err) = ERROR_MESSAGE.get() {
*err.lock().unwrap() = Some(msg.to_string());
}
}
fn is_cancelled() -> bool {
CANCELLED.get()
.map(|f| f.load(Ordering::Relaxed))
.unwrap_or(false)
}
fn get_error_message() -> Option<String> {
ERROR_MESSAGE.get()
.and_then(|err| err.lock().unwrap().clone())
}
static EMPTY: &[&str] = &[" "," "];
static SAND: &[&str] = &["⠁","⠂","⠄","⡀","⡈","⡐","⡠","⣀","⣁","⣂","⣄","⣌","⣔","⣤","⣥","⣦","⣮","⣶","⣷","⣿","⡿","⠿","⢟","⠟","⡛","⠛","⠫","⢋","⠋","⠍","⡉","⠉","⠑","⠡","⢁","⣿"];
static FISTBUMP: &[&str] = &[
" 🤜 🤛 ",
" 🤜 🤛 ",
" 🤜 🤛 ",
" 🤜 🤛 ",
" 🤜🤛 ",
" 🤜💥🤛 ",
" 🤜 ✨ 🤛 ",
" 🤜 💭 🤛 ",
" ✌️ ✌️ ",
" ✌️ ✌️ ",
" ✌️ ✌️ ",
" "
];
static DOTSPINNER: &[&str] = &[
"∙∙∙","∙∙∙","●∙∙","●∙∙","•●∙","•●∙","∙•●","∙•●","∙∙•","∙∙•","∙∙∙","∙∙∙","∙∙∙","∙∙∙","∙∙∙"," "
];
static MINDBLOWN: &[&str] = &[
"🙂","🙂","😐","😐","😮","😮","😦","😦","😧","😧","🤯","🤯","💥","✨","💭"," "," ",
];
static SQUARESPINNER: &str = "◰◰◳◳◲◲◱◱▣";
static FAILEDSQUARESPINNER: &str = "▨▨";
static PENDINGSQUARESPINNER: &str = "□□";
static PARALLELOGRAMPROGRESS: &str = "▰▱";
fn format_duration_short(dur: Duration) -> String {
let ms = dur.as_millis();
if ms < 1000 {
format!("{ms}ms")
} else if ms < 60_000 {
format!("{:.1}s", ms as f64 / 1000.0)
} else if ms < 3_600_000 {
let secs = ms / 1000;
let mins = secs / 60;
let rem = secs % 60;
format!("{mins}m{rem}s")
} else {
let secs = ms / 1000;
let hrs = secs / 3600;
let mins = (secs % 3600) / 60;
format!("{hrs}h{mins}m")
}
}
#[derive(Debug)]
enum StepStatus {
NotStarted,
Pending,
Running,
Completed,
Failed,
}
struct BuildProcess {
id: u64,
name: String,
build_status: StepStatus,
indicators: MultiProgress,
stage_handles: Vec<thread::JoinHandle<()>>,
start_time: Option<Instant>,
end_time: Option<Instant>,
stages: VecDeque<BuildStage>,
build_progress: ProgressBar,
status_bar: ProgressBar,
final_build_location: Option<PathBuf>,
}
impl BuildProcess {
pub fn new(id: u64, name: String) -> Self {
let progress_style = ProgressStyle::with_template(
"{prefix:.yellow} {bar:60.yellow/white.dim.bold} {percent}% ({pos}/{len})"
).unwrap()
.progress_chars(PARALLELOGRAMPROGRESS);
let m = MultiProgress::new();
let build_progress = m.add(ProgressBar::new(0));
build_progress.set_style(progress_style);
build_progress.set_prefix("[Build]");
let status_style = ProgressStyle::with_template(
"{spinner:.yellow} [{elapsed_precise}] {msg}"
).unwrap()
.tick_strings(SAND);
let status_bar = m.add(ProgressBar::new_spinner());
status_bar.set_style(status_style);
status_bar.enable_steady_tick(Duration::from_millis(100));
status_bar.set_message("");
BuildProcess {
id, name,
build_progress,
status_bar,
indicators: m,
build_status: StepStatus::NotStarted,
stage_handles: Vec::new(),
start_time: None,
end_time: None,
stages: VecDeque::new(),
final_build_location: None,
}
}
pub fn start(&mut self) {
self.start_time = Some(Instant::now());
self.build_status = StepStatus::Running;
self.status_bar.set_message("Starting build...");
}
pub fn finish(&mut self) {
self.status_bar.finish_and_clear();
let final_state = self.indicators.add(ProgressBar::new_spinner());
self.end_time = Some(Instant::now());
self.build_status = StepStatus::Completed;
self.build_progress.finish_and_clear();
let completed_style = ProgressStyle::with_template(
"{prefix:.yellow} {msg} {spinner}"
).unwrap()
.tick_strings(FISTBUMP);
final_state.set_style(completed_style);
final_state.set_prefix("[Success]");
if let Some(start_time) = self.start_time {
let elapsed = self.end_time.unwrap_or_else(Instant::now).duration_since(start_time);
if elapsed > Duration::from_secs(60) {
for _ in 0..FISTBUMP.len() - 1 {
thread::sleep(Duration::from_millis(100));
final_state.tick();
}
}
}
let path = get_final_artifact_path().unwrap_or_else(|| "Unknown".to_string());
final_state.finish_with_message(format!("Artifact available at: {}", style(path).color256(75)));
}
pub fn fail(&mut self) {
self.status_bar.finish_and_clear();
let final_state = self.indicators.add(ProgressBar::new_spinner());
self.end_time = Some(Instant::now());
self.build_status = StepStatus::Failed;
self.build_progress.finish_and_clear();
let failed_style = ProgressStyle::with_template(
"{prefix:.yellow} {msg} {spinner}"
).unwrap()
.tick_strings(MINDBLOWN);
final_state.set_style(failed_style);
final_state.set_prefix("[Failure]");
for _ in 0..MINDBLOWN.len() - 1 {
thread::sleep(Duration::from_millis(100));
final_state.tick();
}
final_state.finish_with_message(get_error_message().unwrap_or("Unknown error".to_string()));
}
pub fn add_build_stage(&mut self, mut stage: BuildStage, steps: usize) {
match self.build_status {
StepStatus::NotStarted => self.start(),
_ => {}
}
let stage_id = self.stages.len() as u64 + 1;
let header_style = ProgressStyle::with_template(
"{prefix:.yellow.dim} {msg:.dim} {spinner:.dim}"
).unwrap()
.tick_strings(&EMPTY);
let header = self.indicators.insert_before(&self.build_progress, ProgressBar::new_spinner());
header.set_style(header_style);
header.set_prefix(format!("{}❱", stage_id));
header.set_message(stage.name.clone());
stage.id = stage_id;
stage.header = header.clone();
stage.last_step = header.clone();
stage.build_progress = self.build_progress.clone();
stage.indicators = Some(self.indicators.clone());
stage.status = StepStatus::Pending;
self.stages.push_back(stage);
self.build_progress.inc_length(1 + steps as u64);
}
}
struct BuildStage {
pub id: u64,
name: String,
status: StepStatus,
start_time: Option<Instant>,
end_time: Option<Instant>,
header: ProgressBar,
pub last_step: ProgressBar,
style: ProgressStyle,
pub build_progress: ProgressBar,
pub indicators: Option<MultiProgress>,
task_fn: Option<Box<dyn FnOnce(&mut BuildStage) + Send + 'static>>,
}
impl BuildStage {
pub fn new<F>(name: impl Into<String>, f: F) -> Self
where
F: FnOnce(&mut BuildStage) + Send + 'static,
{
let style = ProgressStyle::with_template("{prefix:.yellow} {msg} {spinner:.dim}")
.unwrap()
.tick_strings(&DOTSPINNER);
Self {
id: 0,
name: name.into(),
status: StepStatus::NotStarted,
start_time: None,
end_time: None,
header: ProgressBar::new(0),
last_step: ProgressBar::new(0),
style,
build_progress: ProgressBar::new(0),
indicators: None,
task_fn: Some(Box::new(f)),
}
}
pub fn start(&mut self) {
self.start_time = Some(Instant::now());
self.status = StepStatus::Running;
if is_cancelled() {
self.cancel();
return;
}
self.header.set_style(self.style.clone());
self.header.enable_steady_tick(Duration::from_millis(100));
self.task_fn.take().map(|f| f(self));
if is_cancelled() {
self.fail();
} else {
self.finish();
}
}
pub fn finish(&mut self) {
let end_time = Instant::now();
self.end_time = Some(end_time);
self.status = StepStatus::Completed;
self.build_progress.inc(1);
let elapsed = self.end_time.unwrap().duration_since(self.start_time.unwrap());
let formatted_time = format_duration_short(elapsed);
self.header.finish_with_message(format!("{} [{}] {}", self.name, formatted_time, style("✓").green()));
}
pub fn fail(&mut self) {
self.end_time = Some(Instant::now());
self.status = StepStatus::Failed;
self.header.finish_with_message(format!("{} {}", self.name, style("✗").red()));
}
pub fn cancel(&mut self) {
self.end_time = Some(Instant::now());
self.status = StepStatus::Failed;
let cancel_style = ProgressStyle::with_template(
"{prefix:.yellow.dim} {msg:.dim}"
).unwrap()
.tick_chars(FAILEDSQUARESPINNER);
self.header.set_style(cancel_style);
self.header.finish_with_message(format!("{} {}", self.name, "✗"));
}
}
pub fn main() -> anyhow::Result<()> {
let text_logo = r#"
┌─────────┐ ┌──────┐ ┌─┐ ┌──┐ ┌─┐ ┌─┐
└───┐ ┌───┘ └──────┘ │ │ └┐ │ │ │ │ │
┌─┐ │ │ ┌─┐ ┌──────┐ │ │ └─┘ │ └─┐│ │
│ │ │ │ │ │ │ ┌────┘ │ │ ┌─┐ │ ┌─┘│ │
│ │ └─┘ │ │ │ └────┐ │ └──┘ │ │ │ │ │
└─┘ └─┘ └──────┘ └──────┘ └─┘ └─┘"#.truecolor(246,192,78);
let about = format!("{}", text_logo);
let start_time = Instant::now();
init_cancel_flag();
let mut args: Vec<String> = env::args().collect();
let matches = Command::new("Mech")
.version(VERSION)
.author("Corey Montella corey@mech-lang.org")
.about(about)
.arg(Arg::new("mech_paths")
.help("Source .mec and files")
.required(false)
.action(ArgAction::Append))
.arg(Arg::new("debug")
.short('d')
.long("debug")
.help("Print debug info")
.action(ArgAction::SetTrue))
.arg(Arg::new("release")
.short('r')
.long("release")
.help("Build in release mode")
.action(ArgAction::SetTrue))
.arg(Arg::new("output_name")
.short('o')
.long("out")
.help("Name out output artifact.")
.required(false))
.subcommand(Command::new("clean")
.about("Clean the build artifacts.")
.arg(Arg::new("mech_clean_paths")
.help("Source .mec and .mecb files")
.required(false)
.action(ArgAction::Append)))
.subcommand(Command::new("build")
.about("Build Mech program into a binary.")
.arg(Arg::new("mech_build_file_paths")
.help("Source .mec and .mecb files")
.required(false)
.action(ArgAction::Append))
.arg(Arg::new("build_debug")
.short('d')
.long("debug")
.help("Print debug info")
.action(ArgAction::SetTrue))
.arg(Arg::new("build_release")
.short('r')
.long("release")
.help("Build in release mode")
.action(ArgAction::SetTrue))
.arg(Arg::new("build_output_name")
.short('o')
.long("out")
.help("Name of output artifact.")
.required(false)))
.get_matches();
if let Some(matches) = matches.subcommand_matches("clean") {
let clean_paths: Vec<String> = matches.get_many::<String>("mech_clean_paths").map_or(vec![], |files| files.map(|file| file.to_string()).collect());
if clean_paths.is_empty() {
if Path::new(BUILD_DIR).exists() {
fs::remove_dir_all(BUILD_DIR).with_context(|| format!("Failed to remove build directory: {}", BUILD_DIR))?;
println!("{} Cleaned build directory: {}", style("✓").green(), BUILD_DIR);
} else {
println!("{} Build directory does not exist: {}", style("✗").red(), BUILD_DIR);
}
} else {
for path in clean_paths {
let p = Path::new(&path);
if p.exists() {
if p.is_dir() {
fs::remove_dir_all(p).with_context(|| format!("Failed to remove directory: {}", path))?;
println!("{} Cleaned directory: {}", style("✓").green(), path);
} else if p.is_file() {
fs::remove_file(p).with_context(|| format!("Failed to remove file: {}", path))?;
println!("{} Cleaned file: {}", style("✓").green(), path);
} else {
println!("Path is neither file nor directory: {}", path);
}
} else {
println!("{} Path does not exist: {}", style("✗").red(), path);
}
}
}
return Ok(());
}
let mut debug_flag = matches.get_flag("debug");
let mut release_flag = matches.get_flag("release");
let mut mech_paths: Vec<String> = matches.get_many::<String>("mech_paths").map_or(vec![], |files| files.map(|file| file.to_string()).collect());
let mut output_name = matches.get_one::<String>("output_name").map(|s| s.to_string()).unwrap_or(
mech_paths.get(0)
.and_then(|p| Path::new(p).file_stem())
.and_then(|s| s.to_str())
.unwrap_or("program")
.to_string()
);
if let Some(matches) = matches.subcommand_matches("build") {
mech_paths = matches.get_many::<String>("mech_build_file_paths").map_or(vec![], |files| files.map(|file| file.to_string()).collect());
debug_flag = matches.get_flag("build_debug");
release_flag = matches.get_flag("build_release");
output_name = matches.get_one::<String>("build_output_name").map(|s| s.to_string()).unwrap_or(
mech_paths.get(0)
.and_then(|p| Path::new(p).file_stem())
.and_then(|s| s.to_str())
.unwrap_or("program")
.to_string()
);
}
set_output_name(output_name.clone());
if mech_paths.is_empty() {
args.push(".".to_string());
let current_dir = env::current_dir().unwrap();
add_source(current_dir.to_str().unwrap());
println!("{} No source files provided, using current directory: {}", style("✗").red(), current_dir.display());
} else {
for path in &mech_paths {
add_source(path);
}
}
let start_message = if mech_paths.len() == 1 {
format!("{}", mech_paths[0])
} else {
format!("{} files.", mech_paths.len())
};
println!(r#"{} Building: {}"#, style(format!("[mech v{}]",VERSION).yellow()), start_message);
{
let mut build = BuildProcess::new(42, "Mech Builder".to_string());
let m = build.indicators.clone();
let cancelled = Arc::new(AtomicBool::new(false));
let (path_tx, path_rx) = mpsc::channel();
let (tree_tx, tree_rx) = mpsc::channel();
let path_tx2 = path_tx.clone();
let mut prepare_build = BuildStage::new("Prepare build environment", |mut stage| {
prepare_build(&mut stage, path_tx2);
});
let mut download_packages = BuildStage::new("Download packages", |mut stage| {
download_packages(&mut stage,path_tx);
});
let mut build_packages = BuildStage::new("Build packages", |mut stage| {
parse_packages(&mut stage,path_rx, tree_tx);
});
let mut build_project = BuildStage::new("Build project", |mut stage| {
build_project(&mut stage, tree_rx);
});
let mut compile_shim = BuildStage::new("Compile shim", move |mut stage| {
compile_shim(&mut stage, release_flag);
});
let mut package_artifacts = BuildStage::new("Package artifacts", move |mut stage| {
package_artifacts(&mut stage, release_flag);
});
let status = build.status_bar.clone();
build.add_build_stage(prepare_build, 5);
build.add_build_stage(download_packages, 0);
build.add_build_stage(build_packages, 0);
build.add_build_stage(build_project, 0);
build.add_build_stage(compile_shim, 145);
build.add_build_stage(package_artifacts, 3);
status.set_message("Preparing environment...");
let mut prepare_environment_stage = build.stages.pop_front().unwrap();
prepare_environment_stage.start();
status.set_message("Downloading and building packages...");
let mut download_stage = build.stages.pop_front().unwrap();
let jh1 = thread::spawn(move || {
download_stage.start();
});
let mut build_stage_1 = build.stages.pop_front().unwrap();
let jh2 = thread::spawn(move || {
build_stage_1.start();
});
jh1.join();
jh2.join();
status.set_message("Building project...");
let mut build_stage_2 = build.stages.pop_front().unwrap();
build_stage_2.start();
status.set_message("Compiling shim...");
let mut compile_shim_stage = build.stages.pop_front().unwrap();
compile_shim_stage.start();
status.set_message("Packaging executable...");
let mut packaging_stage = build.stages.pop_front().unwrap();
packaging_stage.start();
if is_cancelled() {
build.fail();
} else {
build.finish();
}
}
let end_time = Instant::now();
let elapsed = end_time.duration_since(start_time);
let formatted_time = format_duration_short(elapsed);
if is_cancelled() {
println!("{} Build failed after {}.", style("✗").red(), formatted_time);
} else {
println!("{} Build succeeded after {}!", style("✓").green(), formatted_time);
}
drop_temp_dir();
Ok(())
}
fn prepare_build(stage: &mut BuildStage, tx: mpsc::Sender<Vec<PathBuf>>) {
let m = stage.indicators.as_ref().unwrap().clone();
let build_progress = stage.build_progress.clone();
let mut steps = VecDeque::new();
let pb = m.insert_after(&stage.last_step, ProgressBar::new_spinner());
stage.last_step = pb.clone();
pb.set_style(build_style());
pb.set_message("Create build directory.");
steps.push_back(pb);
let pb = m.insert_after(&stage.last_step, ProgressBar::new_spinner());
stage.last_step = pb.clone();
pb.set_style(pending_style());
pb.set_message("Gather source files.");
steps.push_back(pb);
let pb = m.insert_after(&stage.last_step, ProgressBar::new_spinner());
stage.last_step = pb.clone();
pb.set_style(pending_style());
pb.set_message("Check for index.mpkg file.");
steps.push_back(pb);
let pb = m.insert_after(&stage.last_step, ProgressBar::new_spinner());
stage.last_step = pb.clone();
pb.set_style(pending_style());
pb.set_message("Verify mech version compatibility.");
steps.push_back(pb);
let pb = m.insert_after(&stage.last_step, ProgressBar::new_spinner());
stage.last_step = pb.clone();
pb.set_style(pending_style());
pb.set_message("Prepare build environment.");
steps.push_back(pb);
let step = steps.pop_front().unwrap();
let build_path = Path::new(BUILD_DIR);
if !build_path.exists() {
if let Err(e) = fs::create_dir_all(build_path) {
step.finish_with_message(format!("Failed to create build directory: {} {}", e, style("✗").red()));
stage.fail();
return;
}
}
step.finish_with_message("Build directory ready");
build_progress.inc(1);
let step = steps.pop_front().unwrap();
step.set_style(build_style());
step.enable_steady_tick(Duration::from_millis(100));
step.set_message("Gathering source files...");
let exts = ["mec", "mpkg", "mecb", "mdoc", "mdb", "dll", "rlib", "m", "md", "🤖"];
let sources = get_sources();
let step_thread = step.clone();
let tx_thread = tx.clone();
let build_progress_thread = build_progress.clone();
let (src_tx, src_rx) = mpsc::channel();
let count = Arc::new(AtomicUsize::new(0));
let count_clone = count.clone();
thread::spawn(move || {
let mut all_files = Vec::new();
for src in sources {
let path = Path::new(&src);
if path.exists() {
let mut on_file = |file: &Path| {
all_files.push(file.to_path_buf());
let n = count_clone.fetch_add(1, Ordering::SeqCst) + 1;
if n % 25 == 0 {
step_thread.set_message(format!("Discovered {} files...", n));
}
};
if let Err(e) = gather_source_files(path, &exts, &mut on_file) {
step_thread.finish_with_message(format!(
"Failed reading {}: {} {}",
src, e, style("✗").red()
));
cancel_all("Build cancelled due to IO error.");
return;
}
} else {
step_thread.finish_with_message(format!(
"Source path does not exist: {} {}",
src,
style("✗").red()
));
cancel_all("Build cancelled due to missing source files.");
return;
}
}
let total = count.load(Ordering::SeqCst);
step_thread.finish_with_message(format!("Discovered {} files.", total));
src_tx.send(all_files).unwrap();
});
let source_files: Vec<PathBuf> = src_rx.recv().unwrap();
let step = steps.pop_front().unwrap();
step.set_style(build_style());
let mut found_index = false;
for src in &source_files {
if let Some(fname) = src.file_name() {
if fname == "index.mpkg" {
found_index = true;
break;
}
}
}
if found_index {
step.finish_with_message(format!("Found index.mpkg file {}", style("✓").green()));
let pb = m.insert_after(&step, ProgressBar::new_spinner());
pb.set_style(build_style());
pb.set_message("Parsing index.mpkg.");
todo!("Parse the .mpkg file");
} else {
step.finish_with_message(format!("No index.mpkg file {}", style("🛈").color256(75)));
}
build_progress.inc(1);
let step = steps.pop_front().unwrap();
step.set_style(build_style());
step.finish_with_message(format!("Targeting Mech v{}.", VERSION));
build_progress.inc(1);
let step = steps.pop_front().unwrap();
step.set_style(build_style());
step.finish_with_message("Configured build environment".to_string());
build_progress.inc(1);
}
fn download_packages(stage: &mut BuildStage, tx: mpsc::Sender<Vec<PathBuf>>) {
let m = stage.indicators.as_ref().unwrap().clone();
let build_progress = stage.build_progress.clone();
}
fn parse_packages(stage: &mut BuildStage, rx: mpsc::Receiver<Vec<PathBuf>>, tx: mpsc::Sender<Program>) {
let m = stage.indicators.as_ref().unwrap().clone();
let build_progress = stage.build_progress.clone();
let mut handles = Vec::new();
for pkgs in rx {
build_progress.inc_length(1);
let pb = m.insert_after(&stage.header, ProgressBar::new_spinner());
pb.set_style(build_style());
pb.set_message("Building project:...");
pb.enable_steady_tick(Duration::from_millis(100));
let build_progress = build_progress.clone();
let tx2 = tx.clone();
let handle = thread::spawn(move || {
for pkg in pkgs {
pb.set_message(format!("{:<20}", pkg.display()));
if is_cancelled() {
pb.set_style(fail_style());
pb.finish();
return;
}
let content = match fs::read_to_string(&pkg) {
Ok(c) => c,
Err(e) => {
pb.set_style(fail_style());
pb.finish_with_message(format!("Failed to read {}: {} {}", pkg.display(), e, style("✗").red()));
cancel_all("Build cancelled due to IO error.");
return;
}
};
let tree = parser::parse(&content);
match tree {
Ok(t) => {
let _ = tx2.send(t.clone());
add_tree(pkg.display().to_string(), t);
pb.set_message(format!("{}", pkg.display()));
}
Err(e) => {
pb.set_style(fail_style());
pb.finish_with_message(format!("Failed to parse {}: {:?} {}", pkg.display(), e, style("✗").red()));
cancel_all("Build cancelled due to parse error.");
return;
}
}
}
pb.finish();
build_progress.inc(1);
});
handles.push(handle);
}
for handle in handles {
let _ = handle.join();
}
drop(tx);
}
fn build_project(stage: &mut BuildStage, rx: mpsc::Receiver<Program>) {
let m = stage.indicators.as_ref().unwrap().clone();
let build_progress = stage.build_progress.clone();
build_progress.inc_length(1);
let pb = m.insert_after(&stage.header, ProgressBar::new_spinner());
pb.set_style(build_style());
pb.set_message("Building project:...");
pb.enable_steady_tick(Duration::from_millis(100));
let mut intrp = Interpreter::new(0);
for tree in rx {
let result = intrp.interpret(&tree);
match result {
Ok(_) => {
pb.set_message(format!("Build succeeded: {:#?}.", tree.pretty_print()));
}
Err(e) => {
pb.set_style(fail_style());
pb.finish_with_message(format!("Build failed: {:?} {}", e, style("✗").red()));
cancel_all("Build cancelled due to interpreter error.");
return;
}
}
}
match intrp.compile() {
Ok(bytecode) => {
let features = intrp.context.unwrap().features;
set_features(features);
pb.set_message(format!("Compiled {} bytes.", bytecode.len()));
set_bytecode(bytecode);
}
Err(e) => {
pb.set_style(fail_style());
pb.finish_with_message(format!("Compilation failed: {:?} {}", e, style("✗").red()));
cancel_all(format!("Build cancelled due to compile error: {:?}", e).as_str());
return;
}
}
pb.finish();
build_progress.inc(1);
}
pub fn short_source_name(path: &str) -> String {
Path::new(path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(path)
.to_string()
}
fn build_style() -> ProgressStyle {
ProgressStyle::with_template(
" {spinner:.yellow} {msg}",
).unwrap()
.tick_chars(SQUARESPINNER)
}
fn fail_style() -> ProgressStyle {
ProgressStyle::with_template(
" {spinner:.red} {msg}",
).unwrap()
.tick_chars(FAILEDSQUARESPINNER)
}
fn pending_style() -> ProgressStyle {
ProgressStyle::with_template(
" {spinner:.dim} {msg:.dim}",
).unwrap()
.tick_chars(PENDINGSQUARESPINNER)
}
fn download_style() -> ProgressStyle {
ProgressStyle::with_template(
" {spinner:.yellow} {msg} {bar:20.yellow/white.dim.bold} {percent}%",
).unwrap()
.progress_chars(PARALLELOGRAMPROGRESS)
.tick_chars(SQUARESPINNER)
}
fn pending_download_style() -> ProgressStyle {
ProgressStyle::with_template(
" {spinner:.yellow} {msg} {bar:20.yellow/white.dim.bold} {percent}%",
).unwrap()
.progress_chars(PARALLELOGRAMPROGRESS)
.tick_chars(PENDINGSQUARESPINNER)
}
fn gather_source_files<F>(path: &Path, exts: &[&str], on_file: &mut F) -> Result<()>
where
F: FnMut(&Path),
{
if path.is_file() {
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
if exts.contains(&ext) {
add_path(path.to_path_buf());
on_file(path);
}
}
} else if path.is_dir() {
for entry in fs::read_dir(path)? {
let entry = entry?;
gather_source_files(&entry.path(), exts, on_file)?;
}
}
Ok(())
}
fn compile_shim(stage: &mut BuildStage, release: bool) {
let m = stage.indicators.as_ref().unwrap().clone();
let pb = m.insert_after(&stage.last_step,ProgressBar::new_spinner());
stage.last_step = pb.clone();
pb.set_style(build_style());
pb.set_message("Create temp directory for shim project.");
pb.enable_steady_tick(Duration::from_millis(100));
let temp = match TempDir::new().context("creating tempdir") {
Ok(t) => {
set_build_project_dir(t.path().to_path_buf());
t
},
Err(e) => {
pb.set_style(fail_style());
pb.finish_with_message(format!("Failed to create temp dir: {} {}", e, style("✗").red()));
cancel_all("Build cancelled due to IO error.");
stage.fail();
return;
}
};
let project_dir = match write_shim_project(&temp, &"mech_shim") {
Ok(p) => p,
Err(e) => {
pb.set_style(fail_style());
pb.finish_with_message(format!("Failed to write shim project: {} {}", e, style("✗").red()));
cancel_all("Build cancelled due to IO error.");
stage.fail();
return;
}
};
pb.finish_with_message(format!("Created temp shim project at {}", project_dir.canonicalize().unwrap_or_else(|_| project_dir.clone()).display()));
save_temp_dir(temp);
match cargo_build(&project_dir, release, stage) {
Ok(()) => {
}
Err(e) => {
stage.fail();
return;
}
}
stage.build_progress.inc(1);
}
fn write_shim_project(temp: &TempDir, shim_name: &str) -> Result<PathBuf> {
let project_dir = temp.path().join(shim_name);
fs::create_dir_all(project_dir.join("src"))?;
let mut features = get_features().iter().map(|f| f.as_string()).collect::<HashSet<String>>();
features.insert("program".to_string());
features.insert("compiler".to_string());
let feature_list = features.iter().map(|f| format!(r#""{}""#, f)).collect::<Vec<String>>().join(",");
let cargo_toml = format!(
r#"[package]
name = "{name}"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0"
zip = "5.1"
mech-interpreter = {{version = "{version}", default-features = false, features = [{feature_list}] }}
"#,
name = shim_name,
version = VERSION,
feature_list = feature_list
);
fs::write(project_dir.join("Cargo.toml"), cargo_toml)?;
let main_rs = r#"
use anyhow::{Result, Context};
use std::fs::File;
use std::io::{Read, Seek, SeekFrom, Cursor};
use zip::read::ZipArchive;
use mech_interpreter::*;
use mech_interpreter::error::{MechError, MechErrorKind};
use mech_interpreter::value::Value;
use mech_interpreter::types::MResult;
fn run_bytecode(name: &str, bytecode: &[u8]) -> MResult<Value> {
let mut intrp = Interpreter::new(0);
match ParsedProgram::from_bytes(&bytecode) {
Ok(prog) => {
println!("{:#?}", prog);
intrp.run_program(&prog)
},
Err(e) => {
return Err(MechError {file: file!().to_string(), tokens: vec![], msg: format!("{:?}", e), id: line!(), kind: MechErrorKind::GenericError("Unknown".to_string())});
}
}
}
fn main() -> Result<()> {
println!("[shim] started");
let exe_path = std::env::current_exe()?;
println!("[shim] exe: {}", exe_path.display());
let mut f = File::open(&exe_path)?;
let metadata = f.metadata().context("metadata")?;
let len = metadata.len();
if len < 8 {
println!("[shim] no footer present");
return Ok(());
}
// read last 8 bytes => zip size
f.seek(SeekFrom::End(-8))?;
let mut buf = [0u8;8];
f.read_exact(&mut buf)?;
let zip_size = u64::from_le_bytes(buf);
println!("[shim] detected zip size: {}", zip_size);
if len < 8 + zip_size {
println!("[shim] invalid sizes");
return Ok(());
}
f.seek(SeekFrom::End(-(8 + zip_size as i64)))?;
let mut zip_buf = vec![0u8; zip_size as usize];
f.read_exact(&mut zip_buf)?;
println!("[shim] read zip into mem ({} bytes)", zip_buf.len());
let mut za = ZipArchive::new(Cursor::new(zip_buf))?;
for i in 0..za.len() {
let mut entry = za.by_index(i)?;
let name = entry.name().to_string();
println!("[shim] entry: {}", name);
let mut data = Vec::new();
entry.read_to_end(&mut data)?;
let result = run_bytecode(&name, &data);
println!("[shim] result: {:?}", result);
}
println!("[shim] Press Enter to exit...");
std::io::stdin().read_line(&mut String::new()).unwrap();
Ok(())
}
"#;
fs::write(project_dir.join("src").join("main.rs"), main_rs)?;
Ok(project_dir)
}
pub fn cargo_build(
project_dir: &Path,
release: bool,
stage: &mut BuildStage,
) -> MResult<()> {
let m = stage.indicators.as_ref().unwrap().clone();
let build_progress = stage.build_progress.clone();
let header = stage.header.clone();
header.set_message("Building shim");
let pb = m.insert_after(&stage.last_step,ProgressBar::new_spinner());
stage.last_step = pb.clone();
pb.set_style(build_style());
pb.set_message("Running shim build...");
pb.enable_steady_tick(Duration::from_millis(100));
let mut cmd = ProcessCommand::new("cargo");
let current_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let build_dir = current_dir.join(BUILD_DIR);
cmd.current_dir(project_dir)
.arg("build")
.env("CARGO_TARGET_DIR", build_dir)
.arg("--message-format=json")
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if release {
cmd.arg("--release");
}
let mut child = match cmd.spawn().context("failed to spawn shim build") {
Ok(c) => c,
Err(e) => {
pb.set_style(fail_style());
pb.finish_with_message(format!("Failed to start cargo: {} {}", e, style("✗").red()));
cancel_all("Build cancelled due to cargo error.");
return Err(MechError::new(
CargoStartFailed{
details: format!("{}", e)
}, None).with_compiler_loc())
}
};
let stdout = child.stdout.take().unwrap();
let reader = std::io::BufReader::new(stdout);
let mut finished_targets = HashSet::new();
for line in reader.lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
if let Ok(v) = serde_json::from_str::<Value>(&line) {
if let Some(reason) = v["reason"].as_str() {
let target_name = v["target"]["name"].as_str().unwrap_or("");
match reason {
"compiler-artifact" => {
let target_name = v["target"]["name"].as_str().unwrap_or("unknown");
let version = v["package_id"].as_str().unwrap_or("0.0.0");
let version = version.split('@').last().unwrap_or("0.0.0");
finished_targets.insert(target_name.to_string());
pb.set_message(format!("Compiled target: {} v{}", target_name, version));
build_progress.inc(1);
}
"build-finished" => {
}
x => {
}
}
}
}
}
match child.wait_with_output().context("waiting for cargo") {
Ok(output) => {
if !output.status.success() {
pb.set_style(fail_style());
pb.finish_with_message(format!("Shim build failed: {} {} {}", output.status, String::from_utf8_lossy(&output.stderr), style("✗").red()));
cancel_all("Build cancelled due to cargo error.");
return Err(MechError::new(
CargoBuildFailed{
status: output.status.code().unwrap_or(-1),
stderr: String::from_utf8_lossy(&output.stderr).to_string()
}, None).with_compiler_loc())
} else {
pb.finish_with_message("Shim build finished.");
}
},
Err(e) => {
pb.set_style(fail_style());
pb.finish_with_message(format!("Failed to wait for Cargo: {} {}", e, style("✗").red()));
cancel_all("Build cancelled due to Cargo error.");
return Err(MechError::new(
CargoWaitFailed{
details: format!("{}", e)
}, None).with_compiler_loc())
}
}
Ok(())
}
fn package_artifacts(stage: &mut BuildStage, release: bool) {
let m = stage.indicators.as_ref().unwrap().clone();
let header = stage.header.clone();
header.set_message("Packaging artifacts");
let build_progress = stage.build_progress.clone();
let mut steps = VecDeque::new();
let pb = m.insert_after(&stage.last_step,ProgressBar::new_spinner());
stage.last_step = pb.clone();
pb.set_style(pending_style());
pb.set_message("Compress bytecode.");
steps.push_back(pb.clone());
let pb = m.insert_after(&stage.last_step,ProgressBar::new_spinner());
stage.last_step = pb.clone();
pb.set_style(pending_style());
pb.set_message("Read shim.");
steps.push_back(pb.clone());
let pb = m.insert_after(&stage.last_step,ProgressBar::new_spinner());
stage.last_step = pb.clone();
pb.set_style(pending_style());
pb.set_message("Write final executable.");
steps.push_back(pb.clone());
match BUILD_DATA.get() {
Some(data) => {
let pairs = {
let mut data = data.lock().unwrap();
vec![
("program".to_string(), data.bytecode.clone()),
]
};
let pb = steps.pop_front().unwrap();
pb.set_style(build_style());
pb.set_message("Compressing bytecode...");
pb.enable_steady_tick(Duration::from_millis(100));
let zip_bytes = match create_zip_from_pairs(&pairs) {
Ok(b) => {
pb.finish_with_message(format!("Created zip ({} bytes).", b.len()));
b
}
Err(e) => {
pb.set_style(fail_style());
pb.finish_with_message(format!("Failed to create zip: {:?} {}", e, style("✗").red()));
cancel_all("Build cancelled due to internal error.");
stage.fail();
return;
}
};
let pb = steps.pop_front().unwrap();
pb.set_style(build_style());
pb.set_message("Locating built shim executable...");
pb.enable_steady_tick(Duration::from_millis(100));
let build_project_dir = get_build_project_dir();
let current_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let build_dir = current_dir.join(BUILD_DIR);
let built_exe = match find_built_exe(&build_dir, &"mech_shim", None, release) {
Ok(p) => {
pb.finish_with_message(format!("Read built shim executable ({} bytes)", p.metadata().map(|m| m.len()).unwrap_or(0)));
p
}
Err(e) => {
pb.set_style(fail_style());
pb.finish_with_message(format!("Failed to find built shim executable: {:?} {}", e, style("✗").red()));
cancel_all("Build cancelled due to missing shim executable.");
stage.fail();
return;
}
};
let pb = steps.pop_front().unwrap();
pb.set_style(build_style());
pb.set_message("Writing final executable...");
pb.enable_steady_tick(Duration::from_millis(100));
let output_name = get_output_name().unwrap_or("mech_app".to_string());
let mode = if release { "release" } else { "debug" };
let out_path = Path::new(BUILD_DIR).join(mode).join(format!("{}.exe",output_name));
match write_final_exe(&built_exe, &zip_bytes, &out_path) {
Ok(exe_size) => {
pb.finish_with_message(format!("Wrote final executable ({} bytes)", exe_size));
set_final_artifact_path(out_path.clone());
}
Err(e) => {
pb.set_style(fail_style());
pb.finish_with_message(format!("Failed to write final executable to {}: {} {}", out_path.canonicalize().unwrap_or_else(|_| out_path.clone()).display(), e, style("✗").red()));
cancel_all("Build cancelled due to IO error.");
stage.fail();
return;
}
}
build_progress.inc(1);
}
_ => {
pb.set_style(fail_style());
pb.finish_with_message(format!("No build data found. {}", style("✗").red()));
cancel_all("Build cancelled due to internal error.");
stage.fail();
return;
}
}
stage.build_progress.inc(1);
stage.finish();
}
fn create_zip_from_pairs(pairs: &[(String, Vec<u8>)]) -> MResult<Vec<u8>> {
let mut buf = Vec::new();
{
let mut zip = ZipWriter::new(Cursor::new(&mut buf));
let options: FileOptions<'_, ()> =
FileOptions::default().compression_method(CompressionMethod::Stored);
for (name, bytes) in pairs {
let entry_name = format!("{}.mecb", name);
match zip.start_file(&entry_name, options) {
Ok(_) => {}
Err(e) => {
return Err(
MechError::new(
ZipOperationError {
operation: "add",
details: e.to_string(),
},
None,
)
.with_compiler_loc()
);
}
}
match zip.write_all(bytes) {
Ok(_) => {}
Err(e) => {
return Err(
MechError::new(
ZipOperationError {
operation: "write",
details: e.to_string(),
},
None,
)
.with_compiler_loc()
);
}
}
}
match zip.finish() {
Ok(_) => {}
Err(e) => {
return Err(MechError::new(
ZipOperationError {
operation: "finish",
details: e.to_string(),
},
None,
).with_compiler_loc())
}
}
}
Ok(buf)
}
fn find_built_exe(project_dir: &Path, shim_name: &str, target: Option<&str>, release: bool) -> MResult<PathBuf> {
let mut candidate = project_dir.to_path_buf();
if let Some(t) = target {
candidate = candidate.join(t);
}
let mode = if release { "release" } else { "debug" };
candidate = candidate.join(mode).join(shim_name);
#[cfg(windows)]
{
let with_exe = candidate.with_extension("exe");
if with_exe.exists() {
return Ok(with_exe);
} else {
return Err(MechError::new(
MissingExecutableError {
path: with_exe.to_string_lossy().to_string(),
},
None,
).with_compiler_loc());
}
}
if candidate.exists() {
return Ok(candidate);
}
Err(MechError::new(
MissingExecutableError {
path: candidate.to_string_lossy().to_string(),
},
None,
).with_compiler_loc())
}
fn write_final_exe(built_exe: &Path, zip_bytes: &[u8], out_path: &Path) -> Result<u64> {
let mut exe_bytes = Vec::new();
File::open(built_exe)?.read_to_end(&mut exe_bytes)?;
let mut out = File::create(out_path)?;
out.write_all(&exe_bytes)?;
out.write_all(zip_bytes)?;
let zip_size = zip_bytes.len() as u64;
out.write_all(&zip_size.to_le_bytes())?;
out.flush()?;
Ok(exe_bytes.len() as u64 + zip_size + 8)
}
#[derive(Debug, Clone)]
pub struct CargoStartFailed {
pub details: String,
}
impl MechErrorKind for CargoStartFailed {
fn name(&self) -> &str {
"CargoStartFailed"
}
fn message(&self) -> String {
format!("Failed to start cargo: {}", self.details)
}
}
#[derive(Debug, Clone)]
pub struct MissingExecutableError {
pub path: String,
}
impl MechErrorKind for MissingExecutableError {
fn name(&self) -> &str {
"MissingExecutableError"
}
fn message(&self) -> String {
format!("Built executable not found at {}", self.path)
}
}
#[derive(Debug, Clone)]
pub struct ZipOperationError {
pub operation: &'static str,
pub details: String,
}
impl MechErrorKind for ZipOperationError {
fn name(&self) -> &str {
"ZipOperationError"
}
fn message(&self) -> String {
format!("Zip operation '{}' failed: {}", self.operation, self.details)
}
}
#[derive(Debug, Clone)]
pub struct CargoBuildFailed {
pub status: i32,
pub stderr: String,
}
impl MechErrorKind for CargoBuildFailed {
fn name(&self) -> &str {
"CargoBuildFailed"
}
fn message(&self) -> String {
format!(
"Cargo build failed (status: {}): {}",
self.status, self.stderr
)
}
}
#[derive(Debug, Clone)]
pub struct CargoWaitFailed {
pub details: String,
}
impl MechErrorKind for CargoWaitFailed {
fn name(&self) -> &str {
"CargoWaitFailed"
}
fn message(&self) -> String {
format!("Failed to wait for Cargo: {}", self.details)
}
}