o- 0.4.1

Multi-Engine JavaScript Runtime
Documentation
pub mod args;
pub mod app_error;
pub mod conf;
pub mod binengine;
pub mod report;
pub mod toolchain;
pub mod pm;
pub mod lock;

use std::fs;
use std::path::PathBuf;
use args::Commands;
use home::home_dir;
pub use app_error::AppError;
pub use o_core::engine;
use o_core::engine::JSEngine;
pub use o_core::error;
use o_toolchain_spidermonkey::SpiderMonkey;
use o_toolchain_v8::V8Engine;
#[cfg(target_os = "macos")]
use o_toolchain_javascriptcore::JavaScriptCore;

use crate::args::ToolChainCommand;
use crate::binengine::BinEngine;
use crate::pm::global_install;
use crate::report::Report;

pub fn process(args: Commands, toolchain: &str) -> Result<(), AppError> {
    match args {
        Commands::Run { path } => {
            let selected_toolchain = select_toolchain(toolchain)?;
            run(&path, selected_toolchain);
            Ok(())
        }
        Commands::Toolchain { command } => {
            let report = run_toolchain(command)?;
            report::print(&report);
            Ok(())
        }
        Commands::Install { global, package } => {
            if global {
                let report = global_install(package.as_deref()).map_err(AppError::PackageManager)?;
                report::print(&report);
                Ok(())
            } else {
                let report = pm::install().map_err(AppError::PackageManager)?;
                report::print(&report);
                Ok(())
            }
        }
        Commands::Uninstall { name } => {
            let report = pm::uninstall(&name).map_err(AppError::PackageManager)?;
            report::print(&report);
            Ok(())
        }
    }
}

fn run_toolchain(command: ToolChainCommand) -> Result<Report, AppError> {
    match command {
        ToolChainCommand::Add { user, repo } => {
            let mut installed = toolchain::install("github.com", &user, &repo).map_err(|source| {
                AppError::InstallToolchain {
                    user: user.clone(),
                    repo: repo.clone(),
                    source: source.to_string(),
                }
            })?;
            installed.push("bin");
            installed.push(&repo);
            let mut target = home_dir().ok_or(AppError::HomeDirUnavailable)?;
            target.push(".config");
            target.push("o-");
            target.push("toolchains");
            target.push("bin");
            target.push(&repo);
            if let Some(parent) = target.parent() {
                fs::create_dir_all(parent).map_err(|source| AppError::CreateDir {
                    path: parent.to_path_buf(),
                    source,
                })?;
            }
            fs::rename(&installed, &target).map_err(|source| AppError::MoveToolchain {
                from: installed.clone(),
                to: target.clone(),
                source,
            })?;

            Ok(Report::new(format!("installed toolchain `{repo}`"))
                .detail(format!("source: {user}/{repo}"))
                .detail(format!("binary: {}", target.display())))
        }
        ToolChainCommand::Remove { toolchain } => {
            let mut target = home::home_dir().ok_or(AppError::HomeDirUnavailable)?;
            target.push(".config");
            target.push("o-");
            target.push("toolchains");
            target.push("bin");
            target.push(&toolchain);
            fs::remove_file(&target).map_err(|source| AppError::RemoveToolchain {
                path: target.clone(),
                source,
            })?;

            Ok(Report::new(format!("removed toolchain `{toolchain}`"))
                .detail(format!("path: {}", target.display())))
        }
    }
}

fn resolve_toolchain(name: &str) -> Result<String, AppError> {
    let mut toolchain: PathBuf = home::home_dir().ok_or(AppError::HomeDirUnavailable)?;
    toolchain.push(".config");
    toolchain.push("o-");
    toolchain.push("toolchains");
    toolchain.push(name);
    Ok(toolchain.to_string_lossy().into_owned())
}

fn select_toolchain(toolchain: &str) -> Result<Box<dyn JSEngine>, AppError> {
    let engine: Box<dyn JSEngine> = match toolchain.trim() {
        #[cfg(target_os = "macos")]
        "javascriptcore" | "jsc" => Box::new(JavaScriptCore::new()),
        #[cfg(not(target_os = "macos"))]
        "javascriptcore" | "jsc" => {
            return Err(AppError::UnsupportedToolchain {
                toolchain: "javascriptcore".to_string(),
                detail: "the published Linux build excludes JavaScriptCore to avoid linker conflicts with V8",
            });
        }
        "v8" => Box::new(V8Engine::new()),
        "spidermonkey" | "" => Box::new(SpiderMonkey::new()),
        other => Box::new(BinEngine::new(resolve_toolchain(other)?)),
    };
    Ok(engine)
}

fn run(path: &str, toolchain: Box<dyn JSEngine>) {
    let file = match fs::read_to_string(path) {
        Ok(file) => file,
        Err(source) => {
            let error = AppError::ReadScript {
                path: PathBuf::from(path),
                source,
            };
            report::print_error(&error.report());
            return;
        }
    };

    if let Err(err) = toolchain.run(&file, path) {
        eprintln!("{err}");
    }
}