Files
nix-js/nix-js/src/runtime.rs

535 lines
17 KiB
Rust

use std::borrow::Cow;
use std::marker::PhantomData;
use std::path::Path;
#[cfg(feature = "inspector")]
use deno_core::PollEventLoopOptions;
use deno_core::{Extension, ExtensionFileSource, JsRuntime, OpState, RuntimeOptions, v8};
use crate::error::{Error, Result, Source};
use crate::store::DaemonStore;
use crate::value::{AttrSet, List, Symbol, Value};
#[cfg(feature = "inspector")]
pub(crate) mod inspector;
mod ops;
use ops::*;
type ScopeRef<'p, 's> = v8::PinnedRef<'p, v8::HandleScope<'s>>;
type LocalValue<'a> = v8::Local<'a, v8::Value>;
type LocalSymbol<'a> = v8::Local<'a, v8::Symbol>;
pub(crate) trait RuntimeContext: 'static {
fn get_current_dir(&self) -> &Path;
fn add_source(&mut self, path: Source);
fn compile(&mut self, source: Source) -> Result<String>;
fn compile_scoped(&mut self, source: Source, scope: Vec<String>) -> Result<String>;
fn get_source(&self, id: usize) -> Source;
fn get_store(&self) -> &DaemonStore;
}
pub(crate) trait OpStateExt<Ctx: RuntimeContext> {
fn get_ctx(&self) -> &Ctx;
fn get_ctx_mut(&mut self) -> &mut Ctx;
}
impl<Ctx: RuntimeContext> OpStateExt<Ctx> for OpState {
fn get_ctx(&self) -> &Ctx {
self.try_borrow::<&'static mut Ctx>()
.expect("RuntimeContext not set")
}
fn get_ctx_mut(&mut self) -> &mut Ctx {
self.try_borrow_mut::<&'static mut Ctx>()
.expect("RuntimeContext not set")
}
}
fn runtime_extension<Ctx: RuntimeContext>() -> Extension {
const ESM: &[ExtensionFileSource] =
&deno_core::include_js_files!(nix_runtime dir "runtime-ts/dist", "runtime.js");
let mut ops = vec![
op_import::<Ctx>(),
op_scoped_import::<Ctx>(),
op_resolve_path(),
op_read_file(),
op_read_file_type(),
op_read_dir(),
op_path_exists(),
op_walk_dir(),
op_make_placeholder(),
op_store_path::<Ctx>(),
op_convert_hash(),
op_hash_string(),
op_hash_file(),
op_parse_hash(),
op_add_path::<Ctx>(),
op_add_filtered_path::<Ctx>(),
op_decode_span::<Ctx>(),
op_to_file::<Ctx>(),
op_copy_path_to_store::<Ctx>(),
op_get_env(),
op_match(),
op_split(),
op_from_json(),
op_from_toml(),
op_finalize_derivation::<Ctx>(),
op_to_xml(),
];
ops.extend(crate::fetcher::register_ops::<Ctx>());
Extension {
name: "nix_runtime",
esm_files: Cow::Borrowed(ESM),
esm_entry_point: Some("ext:nix_runtime/runtime.js"),
ops: Cow::Owned(ops),
enabled: true,
..Default::default()
}
}
mod private {
use deno_error::js_error_wrapper;
#[derive(Debug)]
pub struct SimpleErrorWrapper(pub(crate) String);
impl std::fmt::Display for SimpleErrorWrapper {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
impl std::error::Error for SimpleErrorWrapper {}
js_error_wrapper!(SimpleErrorWrapper, NixRuntimeError, "Error");
impl From<String> for NixRuntimeError {
fn from(value: String) -> Self {
NixRuntimeError(SimpleErrorWrapper(value))
}
}
impl From<&str> for NixRuntimeError {
fn from(value: &str) -> Self {
NixRuntimeError(SimpleErrorWrapper(value.to_string()))
}
}
}
pub(crate) use private::NixRuntimeError;
pub(crate) struct Runtime<Ctx: RuntimeContext> {
js_runtime: JsRuntime,
rt: tokio::runtime::Runtime,
#[cfg(feature = "inspector")]
wait_for_inspector: bool,
is_thunk_symbol: v8::Global<v8::Symbol>,
primop_metadata_symbol: v8::Global<v8::Symbol>,
has_context_symbol: v8::Global<v8::Symbol>,
is_path_symbol: v8::Global<v8::Symbol>,
is_cycle_symbol: v8::Global<v8::Symbol>,
_marker: PhantomData<Ctx>,
}
#[cfg(feature = "inspector")]
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct InspectorOptions {
pub(crate) enable: bool,
pub(crate) wait: bool,
}
impl<Ctx: RuntimeContext> Runtime<Ctx> {
pub(crate) fn new(
#[cfg(feature = "inspector")] inspector_options: InspectorOptions,
) -> Result<Self> {
use std::sync::Once;
static INIT: Once = Once::new();
INIT.call_once(|| {
assert_eq!(
deno_core::v8_set_flags(vec!["".into(), format!("--stack-size={}", 8 * 1024)]),
[""]
);
JsRuntime::init_platform(Some(v8::new_default_platform(0, false).make_shared()));
});
let mut js_runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![runtime_extension::<Ctx>()],
#[cfg(feature = "inspector")]
inspector: inspector_options.enable,
is_main: true,
..Default::default()
});
js_runtime.op_state().borrow_mut().put(RegexCache::new());
js_runtime.op_state().borrow_mut().put(DrvHashCache::new());
let (
is_thunk_symbol,
primop_metadata_symbol,
has_context_symbol,
is_path_symbol,
is_cycle_symbol,
) = {
deno_core::scope!(scope, &mut js_runtime);
Self::get_symbols(scope)?
};
Ok(Self {
js_runtime,
rt: tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("failed to build tokio runtime"),
#[cfg(feature = "inspector")]
wait_for_inspector: inspector_options.wait,
is_thunk_symbol,
primop_metadata_symbol,
has_context_symbol,
is_path_symbol,
is_cycle_symbol,
_marker: PhantomData,
})
}
#[cfg(feature = "inspector")]
pub(crate) fn inspector(&self) -> std::rc::Rc<deno_core::JsRuntimeInspector> {
self.js_runtime.inspector()
}
#[cfg(feature = "inspector")]
pub(crate) fn wait_for_inspector_disconnect(&mut self) {
let _ = self
.rt
.block_on(self.js_runtime.run_event_loop(PollEventLoopOptions {
wait_for_inspector: true,
..Default::default()
}));
}
pub(crate) fn eval(&mut self, script: String, ctx: &mut Ctx) -> Result<Value> {
let ctx: &'static mut Ctx = unsafe { &mut *(ctx as *mut Ctx) };
self.js_runtime.op_state().borrow_mut().put(ctx);
#[cfg(feature = "inspector")]
if self.wait_for_inspector {
self.js_runtime
.inspector()
.wait_for_session_and_break_on_next_statement();
} else {
self.js_runtime.inspector().wait_for_session();
}
let global_value = self
.js_runtime
.execute_script("<eval>", script)
.map_err(|error| {
let op_state = self.js_runtime.op_state();
let op_state_borrow = op_state.borrow();
let ctx: &Ctx = op_state_borrow.get_ctx();
crate::error::parse_js_error(error, ctx)
})?;
let global_value = self
.rt
.block_on(self.js_runtime.resolve(global_value))
.map_err(|error| {
let op_state = self.js_runtime.op_state();
let op_state_borrow = op_state.borrow();
let ctx: &Ctx = op_state_borrow.get_ctx();
crate::error::parse_js_error(error, ctx)
})?;
#[cfg(feature = "inspector")]
{
let _ = self
.rt
.block_on(self.js_runtime.run_event_loop(Default::default()));
}
// Retrieve scope from JsRuntime
deno_core::scope!(scope, self.js_runtime);
let local_value = v8::Local::new(scope, &global_value);
let is_thunk_symbol = v8::Local::new(scope, &self.is_thunk_symbol);
let primop_metadata_symbol = v8::Local::new(scope, &self.primop_metadata_symbol);
let has_context_symbol = v8::Local::new(scope, &self.has_context_symbol);
let is_path_symbol = v8::Local::new(scope, &self.is_path_symbol);
let is_cycle_symbol = v8::Local::new(scope, &self.is_cycle_symbol);
Ok(to_value(
local_value,
scope,
is_thunk_symbol,
primop_metadata_symbol,
has_context_symbol,
is_path_symbol,
is_cycle_symbol,
))
}
/// get (IS_THUNK, PRIMOP_METADATA, HAS_CONTEXT, IS_PATH, IS_CYCLE)
#[allow(clippy::type_complexity)]
fn get_symbols(
scope: &ScopeRef,
) -> Result<(
v8::Global<v8::Symbol>,
v8::Global<v8::Symbol>,
v8::Global<v8::Symbol>,
v8::Global<v8::Symbol>,
v8::Global<v8::Symbol>,
)> {
let global = scope.get_current_context().global(scope);
let nix_key = v8::String::new(scope, "Nix")
.ok_or_else(|| Error::internal("failed to create V8 String".into()))?;
let nix_obj = global
.get(scope, nix_key.into())
.ok_or_else(|| Error::internal("failed to get global Nix object".into()))?
.to_object(scope)
.ok_or_else(|| {
Error::internal("failed to convert global Nix Value to object".into())
})?;
let get_symbol = |symbol| {
let key = v8::String::new(scope, symbol)
.ok_or_else(|| Error::internal("failed to create V8 String".into()))?;
let val = nix_obj
.get(scope, key.into())
.ok_or_else(|| Error::internal(format!("failed to get {symbol} Symbol")))?;
let sym = val.try_cast::<v8::Symbol>().map_err(|err| {
Error::internal(format!(
"failed to convert {symbol} Value to Symbol ({err})"
))
})?;
Result::Ok(v8::Global::new(scope, sym))
};
let is_thunk = get_symbol("IS_THUNK")?;
let primop_metadata = get_symbol("PRIMOP_METADATA")?;
let has_context = get_symbol("HAS_CONTEXT")?;
let is_path = get_symbol("IS_PATH")?;
let is_cycle = get_symbol("IS_CYCLE")?;
Ok((is_thunk, primop_metadata, has_context, is_path, is_cycle))
}
}
fn to_value<'a>(
val: LocalValue<'a>,
scope: &ScopeRef<'a, '_>,
is_thunk_symbol: LocalSymbol<'a>,
primop_metadata_symbol: LocalSymbol<'a>,
has_context_symbol: LocalSymbol<'a>,
is_path_symbol: LocalSymbol<'a>,
is_cycle_symbol: LocalSymbol<'a>,
) -> Value {
match () {
_ if val.is_big_int() => {
let (val, lossless) = val
.to_big_int(scope)
.expect("infallible conversion")
.i64_value();
if !lossless {
panic!("BigInt value out of i64 range: conversion lost precision");
}
Value::Int(val)
}
_ if val.is_number() => {
let val = val.to_number(scope).expect("infallible conversion").value();
Value::Float(val)
}
_ if val.is_true() => Value::Bool(true),
_ if val.is_false() => Value::Bool(false),
_ if val.is_null() => Value::Null,
_ if val.is_string() => {
let val = val.to_string(scope).expect("infallible conversion");
Value::String(val.to_rust_string_lossy(scope))
}
_ if val.is_array() => {
let val = val.try_cast::<v8::Array>().expect("infallible conversion");
let len = val.length();
let list = (0..len)
.map(|i| {
let val = val.get_index(scope, i).expect("infallible index operation");
to_value(
val,
scope,
is_thunk_symbol,
primop_metadata_symbol,
has_context_symbol,
is_path_symbol,
is_cycle_symbol,
)
})
.collect();
Value::List(List::new(list))
}
_ if val.is_function() => {
if let Some(primop) = to_primop(val, scope, primop_metadata_symbol) {
primop
} else {
Value::Func
}
}
_ if val.is_map() => {
let val = val.try_cast::<v8::Map>().expect("infallible conversion");
let size = val.size() as u32;
let array = val.as_array(scope);
let attrs = (0..size)
.map(|i| {
let key = array.get_index(scope, i * 2).expect("infallible index operation");
let key = key.to_rust_string_lossy(scope);
let val = array.get_index(scope, i * 2 + 1).expect("infallible index operation");
let val = to_value(
val,
scope,
is_thunk_symbol,
primop_metadata_symbol,
has_context_symbol,
is_path_symbol,
is_cycle_symbol,
);
(Symbol::new(Cow::Owned(key)), val)
}).collect();
Value::AttrSet(AttrSet::new(attrs))
}
_ if val.is_object() => {
if is_thunk(val, scope, is_thunk_symbol) {
return Value::Thunk;
}
if is_cycle(val, scope, is_cycle_symbol) {
return Value::Repeated;
}
if let Some(path_val) = extract_path(val, scope, is_path_symbol) {
return Value::Path(path_val);
}
if let Some(string_val) = extract_string_with_context(val, scope, has_context_symbol) {
return Value::String(string_val);
}
let val = val.to_object(scope).expect("infallible conversion");
let keys = val
.get_own_property_names(scope, v8::GetPropertyNamesArgsBuilder::new().build())
.expect("infallible operation");
let len = keys.length();
let attrs = (0..len)
.map(|i| {
let key = keys
.get_index(scope, i)
.expect("infallible index operation");
let val = val.get(scope, key).expect("infallible operation");
let key = key.to_rust_string_lossy(scope);
(
Symbol::from(key),
to_value(
val,
scope,
is_thunk_symbol,
primop_metadata_symbol,
has_context_symbol,
is_path_symbol,
is_cycle_symbol,
),
)
})
.collect();
Value::AttrSet(AttrSet::new(attrs))
}
_ => unimplemented!("can not convert {} to NixValue", val.type_repr()),
}
}
fn is_thunk<'a>(val: LocalValue<'a>, scope: &ScopeRef<'a, '_>, symbol: LocalSymbol<'a>) -> bool {
if !val.is_object() {
return false;
}
let obj = val.to_object(scope).expect("infallible conversion");
matches!(obj.get(scope, symbol.into()), Some(v) if v.is_true())
}
fn is_cycle<'a>(val: LocalValue<'a>, scope: &ScopeRef<'a, '_>, symbol: LocalSymbol<'a>) -> bool {
if !val.is_object() {
return false;
}
let obj = val.to_object(scope).expect("infallible conversion");
matches!(obj.get(scope, symbol.into()), Some(v) if v.is_true())
}
fn extract_string_with_context<'a>(
val: LocalValue<'a>,
scope: &ScopeRef<'a, '_>,
symbol: LocalSymbol<'a>,
) -> Option<String> {
if !val.is_object() {
return None;
}
let obj = val.to_object(scope).expect("infallible conversion");
let has_context = obj.get(scope, symbol.into())?;
if !has_context.is_true() {
return None;
}
let value_key = v8::String::new(scope, "value")?;
let value = obj.get(scope, value_key.into())?;
if value.is_string() {
Some(value.to_rust_string_lossy(scope))
} else {
None
}
}
fn extract_path<'a>(
val: LocalValue<'a>,
scope: &ScopeRef<'a, '_>,
symbol: LocalSymbol<'a>,
) -> Option<String> {
if !val.is_object() {
return None;
}
let obj = val.to_object(scope).expect("infallible conversion");
let is_path = obj.get(scope, symbol.into())?;
if !is_path.is_true() {
return None;
}
let value_key = v8::String::new(scope, "value")?;
let value = obj.get(scope, value_key.into())?;
if value.is_string() {
Some(value.to_rust_string_lossy(scope))
} else {
None
}
}
fn to_primop<'a>(
val: LocalValue<'a>,
scope: &ScopeRef<'a, '_>,
symbol: LocalSymbol<'a>,
) -> Option<Value> {
if !val.is_function() {
return None;
}
let obj = val.to_object(scope).expect("infallible conversion");
let metadata = obj.get(scope, symbol.into())?.to_object(scope)?;
let name_key = v8::String::new(scope, "name")?;
let name = metadata
.get(scope, name_key.into())?
.to_rust_string_lossy(scope);
let applied_key = v8::String::new(scope, "applied")?;
let applied_val = metadata.get(scope, applied_key.into())?;
let applied = applied_val.to_number(scope)?.value();
if applied == 0.0 {
Some(Value::PrimOp(name))
} else {
Some(Value::PrimOpApp(name))
}
}