feat: add rust-script

This commit is contained in:
2022-08-06 01:14:04 +08:00
parent 70f202e982
commit a65b8f0aae
67 changed files with 5086 additions and 0 deletions

186
external/rust-script/src/consts.rs vendored Normal file
View File

@@ -0,0 +1,186 @@
/*!
This module just contains any big string literals I don't want cluttering up the rest of the code.
*/
pub const PROGRAM_NAME: &str = "rust-script";
/*
What follows are the templates used to wrap script input.
*/
/// Substitution for the script body.
pub const SCRIPT_BODY_SUB: &str = "script";
/// Substitution for the script prelude.
pub const SCRIPT_PRELUDE_SUB: &str = "prelude";
/// The template used for script file inputs.
pub const FILE_TEMPLATE: &str = r#"#{script}"#;
/// The template used for `--expr` input.
pub const EXPR_TEMPLATE: &str = r#"
#{prelude}
use std::any::{Any, TypeId};
fn main() {
let exit_code = match try_main() {
Ok(()) => None,
Err(e) => {
use std::io::{self, Write};
let _ = writeln!(io::stderr(), "Error: {}", e);
Some(1)
},
};
if let Some(exit_code) = exit_code {
std::process::exit(exit_code);
}
}
fn try_main() -> Result<(), Box<dyn std::error::Error>> {
fn _rust_script_is_empty_tuple<T: ?Sized + Any>(_s: &T) -> bool {
TypeId::of::<()>() == TypeId::of::<T>()
}
match {#{script}} {
__rust_script_expr if !_rust_script_is_empty_tuple(&__rust_script_expr) => println!("{:?}", __rust_script_expr),
_ => {}
}
Ok(())
}
"#;
/*
Regarding the loop templates: what I *want* is for the result of the closure to be printed to standard output *only* if it's not `()`.
* TODO: Merge the `LOOP_*` templates so there isn't duplicated code. It's icky.
*/
/// The template used for `--loop` input, assuming no `--count` flag is also given.
pub const LOOP_TEMPLATE: &str = r#"
#![allow(unused_imports)]
#![allow(unused_braces)]
#{prelude}
use std::any::Any;
use std::io::prelude::*;
fn main() {
let mut closure = enforce_closure(
{#{script}}
);
let mut line_buffer = String::new();
let stdin = std::io::stdin();
loop {
line_buffer.clear();
let read_res = stdin.read_line(&mut line_buffer).unwrap_or(0);
if read_res == 0 { break }
let output = closure(&line_buffer);
let display = {
let output_any: &dyn Any = &output;
!output_any.is::<()>()
};
if display {
println!("{:?}", output);
}
}
}
fn enforce_closure<F, T>(closure: F) -> F
where F: FnMut(&str) -> T, T: 'static {
closure
}
"#;
/// The template used for `--count --loop` input.
pub const LOOP_COUNT_TEMPLATE: &str = r#"
#![allow(unused_imports)]
#![allow(unused_braces)]
use std::any::Any;
use std::io::prelude::*;
fn main() {
let mut closure = enforce_closure(
{#{script}}
);
let mut line_buffer = String::new();
let stdin = std::io::stdin();
let mut count = 0;
loop {
line_buffer.clear();
let read_res = stdin.read_line(&mut line_buffer).unwrap_or(0);
if read_res == 0 { break }
count += 1;
let output = closure(&line_buffer, count);
let display = {
let output_any: &dyn Any = &output;
!output_any.is::<()>()
};
if display {
println!("{:?}", output);
}
}
}
fn enforce_closure<F, T>(closure: F) -> F
where F: FnMut(&str, usize) -> T, T: 'static {
closure
}
"#;
/// Substitution for the identifier-safe package name of the script.
pub const MANI_NAME_SUB: &str = "name";
/// Substitution for the identifier-safe bin name of the script.
pub const MANI_BIN_NAME_SUB: &str = "bin_name";
/// Substitution for the filesystem-safe name of the script.
pub const MANI_FILE_SUB: &str = "file";
/**
The default manifest used for packages.
*/
#[rustversion::before(1.59)]
pub const DEFAULT_MANIFEST: &str = r##"
[package]
name = "#{name}"
version = "0.1.0"
authors = ["Anonymous"]
edition = "2018"
[[bin]]
name = "#{bin_name}"
path = "#{file}.rs"
"##;
#[rustversion::since(1.59)]
pub const DEFAULT_MANIFEST: &str = r##"
[package]
name = "#{name}"
version = "0.1.0"
authors = ["Anonymous"]
edition = "2018"
[[bin]]
name = "#{bin_name}"
path = "#{file}.rs"
[profile.release]
strip = true
"##;
/**
When generating a package's unique ID, how many hex nibbles of the digest should be used *at most*?
The largest meaningful value is `40`.
*/
pub const ID_DIGEST_LEN_MAX: usize = 24;
/**
How old can stuff in the cache be before we automatically clear it out?
Measured in milliseconds.
*/
// It's been *one week* since you looked at me,
// cocked your head to the side and said "I'm angry."
pub const MAX_CACHE_AGE_MS: u128 = 7 * 24 * 60 * 60 * 1000;

61
external/rust-script/src/error.rs vendored Normal file
View File

@@ -0,0 +1,61 @@
/*!
Definition of the program's main error type.
*/
use std::borrow::Cow;
use std::error::Error;
use std::fmt;
use std::io;
use std::result::Result;
/// Shorthand for the program's common result type.
pub type MainResult<T> = Result<T, MainError>;
/// An error in the program.
#[derive(Debug)]
pub enum MainError {
Io(io::Error),
Tag(Cow<'static, str>, Box<MainError>),
Other(Box<dyn Error>),
OtherOwned(String),
OtherBorrowed(&'static str),
}
impl fmt::Display for MainError {
fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
use self::MainError::*;
use std::fmt::Display;
match *self {
Io(ref err) => Display::fmt(err, fmt),
Tag(ref msg, ref err) => write!(fmt, "{}: {}", msg, err),
Other(ref err) => Display::fmt(err, fmt),
OtherOwned(ref err) => Display::fmt(err, fmt),
OtherBorrowed(err) => Display::fmt(err, fmt),
}
}
}
impl Error for MainError {}
macro_rules! from_impl {
($src_ty:ty => $dst_ty:ty, $src:ident -> $e:expr) => {
impl From<$src_ty> for $dst_ty {
fn from($src: $src_ty) -> $dst_ty {
$e
}
}
};
}
from_impl! { io::Error => MainError, v -> MainError::Io(v) }
from_impl! { String => MainError, v -> MainError::OtherOwned(v) }
from_impl! { &'static str => MainError, v -> MainError::OtherBorrowed(v) }
impl<T> From<Box<T>> for MainError
where
T: 'static + Error,
{
fn from(src: Box<T>) -> Self {
MainError::Other(src)
}
}

101
external/rust-script/src/file_assoc.rs vendored Normal file
View File

@@ -0,0 +1,101 @@
/*!
This module deals with setting up file associations on Windows
*/
use crate::error::MainResult;
use std::env;
use std::io;
use winreg::{enums as wre, RegKey};
pub fn install_file_association() -> MainResult<()> {
let rust_script_path = env::current_exe()?.canonicalize()?;
if !rust_script_path.exists() {
return Err(format!("{:?} not found", rust_script_path).into());
}
// We have to remove the `\\?\` prefix because, if we don't, the shell freaks out.
let rust_script_path = rust_script_path.to_string_lossy();
let rust_script_path = if let Some(stripped) = rust_script_path.strip_prefix(r#"\\?\"#) {
stripped
} else {
&rust_script_path[..]
};
let res = (|| -> io::Result<()> {
let hlcr = RegKey::predef(wre::HKEY_CLASSES_ROOT);
let (dot_ers, _) = hlcr.create_subkey(".ers")?;
dot_ers.set_value("", &"RustScript.Ers")?;
let (cs_ers, _) = hlcr.create_subkey("RustScript.Ers")?;
cs_ers.set_value("", &"Rust Script")?;
let (sh_o_c, _) = cs_ers.create_subkey(r#"shell\open\command"#)?;
sh_o_c.set_value("", &format!(r#""{}" "%1" %*"#, rust_script_path))?;
Ok(())
})();
match res {
Ok(()) => (),
Err(e) => {
if e.kind() == io::ErrorKind::PermissionDenied {
println!(
"Access denied. Make sure you run this command from an administrator prompt."
);
}
return Err(e.into());
}
}
println!("Created rust-script registry entry.");
println!("- Handler set to: {}", rust_script_path);
Ok(())
}
pub fn uninstall_file_association() -> MainResult<()> {
let mut ignored_missing = false;
{
let mut notify = || ignored_missing = true;
let hlcr = RegKey::predef(wre::HKEY_CLASSES_ROOT);
hlcr.delete_subkey(r#"RustScript.Ers\shell\open\command"#)
.ignore_missing_and(&mut notify)?;
hlcr.delete_subkey(r#"RustScript.Ers\shell\open"#)
.ignore_missing_and(&mut notify)?;
hlcr.delete_subkey(r#"RustScript.Ers\shell"#)
.ignore_missing_and(&mut notify)?;
hlcr.delete_subkey(r#"RustScript.Ers"#)
.ignore_missing_and(&mut notify)?;
}
if ignored_missing {
println!("Ignored some missing registry entries.");
}
println!("Deleted rust-script registry entry.");
Ok(())
}
trait IgnoreMissing {
fn ignore_missing_and<F>(self, f: F) -> Self
where
F: FnOnce();
}
impl IgnoreMissing for io::Result<()> {
fn ignore_missing_and<F>(self, f: F) -> Self
where
F: FnOnce(),
{
match self {
Ok(()) => Ok(()),
Err(e) => {
if e.kind() == io::ErrorKind::NotFound {
f();
Ok(())
} else {
Err(e)
}
}
}
}
}

1230
external/rust-script/src/main.rs vendored Normal file

File diff suppressed because it is too large Load Diff

1214
external/rust-script/src/manifest.rs vendored Normal file

File diff suppressed because it is too large Load Diff

94
external/rust-script/src/platform.rs vendored Normal file
View File

@@ -0,0 +1,94 @@
/*!
This module is for platform-specific stuff.
*/
pub use self::inner::force_cargo_color;
use crate::consts;
use crate::error::MainError;
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
// Last-modified time of a file, in milliseconds since the UNIX epoch.
pub fn file_last_modified(file: &fs::File) -> u128 {
file.metadata()
.and_then(|md| {
md.modified()
.map(|t| t.duration_since(UNIX_EPOCH).unwrap().as_millis())
})
.unwrap_or(0)
}
// Current system time, in milliseconds since the UNIX epoch.
pub fn current_time() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis()
}
#[cfg(not(test))]
pub fn cache_dir() -> Result<PathBuf, MainError> {
dirs_next::cache_dir()
.map(|dir| dir.join(consts::PROGRAM_NAME))
.ok_or_else(|| ("Cannot get cache directory").into())
}
#[cfg(test)]
pub fn cache_dir() -> Result<PathBuf, MainError> {
use lazy_static::lazy_static;
lazy_static! {
static ref TEMP_DIR: tempfile::TempDir = tempfile::TempDir::new().unwrap();
}
Ok(TEMP_DIR.path().to_path_buf())
}
pub fn generated_projects_cache_path() -> Result<PathBuf, MainError> {
cache_dir().map(|dir| dir.join("projects"))
}
pub fn binary_cache_path() -> Result<PathBuf, MainError> {
cache_dir().map(|dir| dir.join("binaries"))
}
pub fn templates_dir() -> Result<PathBuf, MainError> {
if cfg!(debug_assertions) {
if let Ok(path) = std::env::var("RUST_SCRIPT_DEBUG_TEMPLATE_PATH") {
return Ok(path.into());
}
}
dirs_next::data_local_dir()
.map(|dir| dir.join(consts::PROGRAM_NAME).join("templates"))
.ok_or_else(|| ("Cannot get cache directory").into())
}
#[cfg(unix)]
mod inner {
pub use super::*;
/**
Returns `true` if `rust-script` should force Cargo to use coloured output.
This depends on whether `rust-script`'s STDERR is connected to a TTY or not.
*/
pub fn force_cargo_color() -> bool {
atty::is(atty::Stream::Stderr)
}
}
#[cfg(windows)]
pub mod inner {
pub use super::*;
/**
Returns `true` if `rust-script` should force Cargo to use coloured output.
Always returns `false` on Windows because colour is communicated over a side-channel.
*/
pub fn force_cargo_color() -> bool {
false
}
}

138
external/rust-script/src/templates.rs vendored Normal file
View File

@@ -0,0 +1,138 @@
/*!
This module contains code related to template support.
*/
use crate::consts;
use crate::error::{MainError, MainResult};
use crate::platform;
use lazy_static::lazy_static;
use regex::Regex;
use std::borrow::Cow;
use std::collections::HashMap;
use std::fs;
lazy_static! {
static ref RE_SUB: Regex = Regex::new(r#"#\{([A-Za-z_][A-Za-z0-9_]*)}"#).unwrap();
}
pub fn expand(src: &str, subs: &HashMap<&str, &str>) -> MainResult<String> {
// The estimate of final size is the sum of the size of all the input.
let sub_size = subs.iter().map(|(_, v)| v.len()).sum::<usize>();
let est_size = src.len() + sub_size;
let mut anchor = 0;
let mut result = String::with_capacity(est_size);
for m in RE_SUB.captures_iter(src) {
// Concatenate the static bit just before the match.
let (m_start, m_end) = {
let m_0 = m.get(0).unwrap();
(m_0.start(), m_0.end())
};
let prior_slice = anchor..m_start;
anchor = m_end;
result.push_str(&src[prior_slice]);
// Concat the substitution.
let sub_name = m.get(1).unwrap().as_str();
match subs.get(sub_name) {
Some(s) => result.push_str(s),
None => {
return Err(MainError::OtherOwned(format!(
"substitution `{}` in template is unknown",
sub_name
)))
}
}
}
result.push_str(&src[anchor..]);
Ok(result)
}
/**
Attempts to locate and load the contents of the specified template.
*/
pub fn get_template(name: &str) -> MainResult<Cow<'static, str>> {
use std::io::Read;
let base = platform::templates_dir()?;
let file = fs::File::open(base.join(format!("{}.rs", name)))
.map_err(MainError::from)
.map_err(|e| {
MainError::Tag(
format!(
"template file `{}.rs` does not exist in {}",
name,
base.display()
)
.into(),
Box::new(e),
)
});
// If the template is one of the built-in ones, do fallback if it wasn't found on disk.
if file.is_err() {
if let Some(text) = builtin_template(name) {
return Ok(text.into());
}
}
let mut file = file?;
let mut text = String::new();
file.read_to_string(&mut text)?;
Ok(text.into())
}
fn builtin_template(name: &str) -> Option<&'static str> {
Some(match name {
"expr" => consts::EXPR_TEMPLATE,
"file" => consts::FILE_TEMPLATE,
"loop" => consts::LOOP_TEMPLATE,
"loop-count" => consts::LOOP_COUNT_TEMPLATE,
_ => return None,
})
}
pub fn list() -> MainResult<()> {
use std::ffi::OsStr;
let t_path = platform::templates_dir()?;
if !t_path.exists() {
fs::create_dir_all(&t_path)?;
}
println!("Listing templates in {}", t_path.display());
if !t_path.exists() {
return Err(format!(
"cannot list template directory `{}`: it does not exist",
t_path.display()
)
.into());
}
if !t_path.is_dir() {
return Err(format!(
"cannot list template directory `{}`: it is not a directory",
t_path.display()
)
.into());
}
for entry in fs::read_dir(&t_path)? {
let entry = entry?;
if !entry.file_type()?.is_file() {
continue;
}
let f_path = entry.path();
if f_path.extension() != Some(OsStr::new("rs")) {
continue;
}
if let Some(stem) = f_path.file_stem() {
println!("{}", stem.to_string_lossy());
}
}
Ok(())
}

54
external/rust-script/src/util.rs vendored Normal file
View File

@@ -0,0 +1,54 @@
/*!
This module just contains other random implementation stuff.
*/
use log::error;
use std::error::Error;
use std::marker::PhantomData;
/**
Used to defer a closure until the value is dropped.
The closure *must* return a `Result<(), _>`, as a reminder to *not* panic; doing so will abort your whole program if it happens during another panic. If the closure returns an `Err`, then it is logged as an `error`.
A `Defer` can also be "disarmed", preventing the closure from running at all.
*/
#[must_use]
pub struct Defer<'a, F, E>(Option<F>, PhantomData<&'a F>)
where
F: 'a + FnOnce() -> Result<(), E>,
E: Error;
impl<'a, F, E> Defer<'a, F, E>
where
F: 'a + FnOnce() -> Result<(), E>,
E: Error,
{
/**
Create a new `Defer` with the given closure.
*/
pub fn new(f: F) -> Defer<'a, F, E> {
Defer(Some(f), PhantomData)
}
/**
Consume this `Defer` *without* invoking the closure.
*/
pub fn disarm(mut self) {
self.0 = None;
drop(self);
}
}
impl<'a, F, E> ::std::ops::Drop for Defer<'a, F, E>
where
F: 'a + FnOnce() -> Result<(), E>,
E: Error,
{
fn drop(&mut self) {
if let Some(f) = self.0.take() {
if let Err(err) = f() {
error!("deferred function failed: {}", err);
}
}
}
}