mirror of
https://github.com/lune-org/lune.git
synced 2024-12-12 13:00:37 +00:00
Refactor CLI to use subcommands, update changelog
This commit is contained in:
parent
3565c897c0
commit
d7c603e881
9 changed files with 415 additions and 372 deletions
34
CHANGELOG.md
34
CHANGELOG.md
|
@ -10,8 +10,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
- The Lune CLI now uses subcommands instead of flag options: <br/>
|
||||||
|
|
||||||
|
- `lune script_name arg1 arg2 arg3` -> `lune run script_name arg1 arg2 arg3`
|
||||||
|
- `lune --list` -> `lune list`
|
||||||
|
- `lune --setup` -> `lune setup`
|
||||||
|
|
||||||
|
This unfortunately hurts ergonomics for quickly running scripts but is a necessary change to allow us to add more commands, such as the new `build` subcommand.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Added support for compiling single Lune scripts into standalone executables! ([#140])
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- my_cool_script.luau
|
||||||
|
print("Hello, standalone!")
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
> lune build my_cool_script
|
||||||
|
# Creates `my_cool_script.exe` (Windows) or `my_cool_script` (macOS / Linux)
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
> ./my_cool_script.exe # Windows
|
||||||
|
> ./my_cool_script # macOS / Linux
|
||||||
|
> "Hello, standalone!"
|
||||||
|
```
|
||||||
|
|
||||||
|
To compile scripts that use `require` and reference multiple files, a bundler such as [darklua](https://github.com/seaofvoices/darklua) will need to be used first. This limitation will be lifted in the future and Lune will automatically bundle any referenced scripts.
|
||||||
|
|
||||||
- Added support for multiple values for a single query, and multiple values for a single header, in `net.request`. This is a part of the HTTP specification that is not widely used but that may be useful in certain cases. To clarify:
|
- Added support for multiple values for a single query, and multiple values for a single header, in `net.request`. This is a part of the HTTP specification that is not widely used but that may be useful in certain cases. To clarify:
|
||||||
|
|
||||||
- Single values remain unchanged and will work exactly the same as before. <br/>
|
- Single values remain unchanged and will work exactly the same as before. <br/>
|
||||||
|
@ -42,6 +74,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
[#140]: https://github.com/filiptibell/lune/pull/140
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Update to Luau version `0.606`.
|
- Update to Luau version `0.606`.
|
||||||
|
|
|
@ -1,51 +1,70 @@
|
||||||
use std::{env, path::Path, process::ExitCode};
|
use std::{
|
||||||
|
env::{self, consts::EXE_EXTENSION},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
process::ExitCode,
|
||||||
|
};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
|
use clap::Parser;
|
||||||
use console::style;
|
use console::style;
|
||||||
use mlua::Compiler as LuaCompiler;
|
use mlua::Compiler as LuaCompiler;
|
||||||
use tokio::{fs, io::AsyncWriteExt as _};
|
use tokio::{fs, io::AsyncWriteExt as _};
|
||||||
|
|
||||||
use crate::executor::MetaChunk;
|
use crate::executor::MetaChunk;
|
||||||
|
|
||||||
/**
|
/// Build a standalone executable
|
||||||
Compiles and embeds the bytecode of a given lua file to form a standalone
|
#[derive(Debug, Clone, Parser)]
|
||||||
binary, then writes it to an output file, with the required permissions.
|
pub struct BuildCommand {
|
||||||
*/
|
/// The path to the input file
|
||||||
#[allow(clippy::similar_names)]
|
pub input: PathBuf,
|
||||||
pub async fn build_standalone(
|
|
||||||
input_path: impl AsRef<Path>,
|
|
||||||
output_path: impl AsRef<Path>,
|
|
||||||
source_code: impl AsRef<[u8]>,
|
|
||||||
) -> Result<ExitCode> {
|
|
||||||
let input_path_displayed = input_path.as_ref().display();
|
|
||||||
let output_path_displayed = output_path.as_ref().display();
|
|
||||||
|
|
||||||
// First, we read the contents of the lune interpreter as our starting point
|
/// The path to the output file - defaults to the
|
||||||
println!(
|
/// input file path with an executable extension
|
||||||
"Creating standalone binary using {}",
|
#[clap(short, long)]
|
||||||
style(input_path_displayed).green()
|
pub output: Option<PathBuf>,
|
||||||
);
|
}
|
||||||
let mut patched_bin = fs::read(env::current_exe()?).await?;
|
|
||||||
|
|
||||||
// Compile luau input into bytecode
|
impl BuildCommand {
|
||||||
let bytecode = LuaCompiler::new()
|
pub async fn run(self) -> Result<ExitCode> {
|
||||||
.set_optimization_level(2)
|
let output_path = self
|
||||||
.set_coverage_level(0)
|
.output
|
||||||
.set_debug_level(1)
|
.unwrap_or_else(|| self.input.with_extension(EXE_EXTENSION));
|
||||||
.compile(source_code);
|
|
||||||
|
|
||||||
// Append the bytecode / metadata to the end
|
let input_path_displayed = self.input.display();
|
||||||
let meta = MetaChunk { bytecode };
|
let output_path_displayed = output_path.display();
|
||||||
patched_bin.extend_from_slice(&meta.to_bytes());
|
|
||||||
|
|
||||||
// And finally write the patched binary to the output file
|
// Try to read the input file
|
||||||
println!(
|
let source_code = fs::read(&self.input)
|
||||||
"Writing standalone binary to {}",
|
.await
|
||||||
style(output_path_displayed).blue()
|
.context("failed to read input file")?;
|
||||||
);
|
|
||||||
write_executable_file_to(output_path, patched_bin).await?;
|
|
||||||
|
|
||||||
Ok(ExitCode::SUCCESS)
|
// Read the contents of the lune interpreter as our starting point
|
||||||
|
println!(
|
||||||
|
"Creating standalone binary using {}",
|
||||||
|
style(input_path_displayed).green()
|
||||||
|
);
|
||||||
|
let mut patched_bin = fs::read(env::current_exe()?).await?;
|
||||||
|
|
||||||
|
// Compile luau input into bytecode
|
||||||
|
let bytecode = LuaCompiler::new()
|
||||||
|
.set_optimization_level(2)
|
||||||
|
.set_coverage_level(0)
|
||||||
|
.set_debug_level(1)
|
||||||
|
.compile(source_code);
|
||||||
|
|
||||||
|
// Append the bytecode / metadata to the end
|
||||||
|
let meta = MetaChunk { bytecode };
|
||||||
|
patched_bin.extend_from_slice(&meta.to_bytes());
|
||||||
|
|
||||||
|
// And finally write the patched binary to the output file
|
||||||
|
println!(
|
||||||
|
"Writing standalone binary to {}",
|
||||||
|
style(output_path_displayed).blue()
|
||||||
|
);
|
||||||
|
write_executable_file_to(output_path, patched_bin).await?;
|
||||||
|
|
||||||
|
Ok(ExitCode::SUCCESS)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn write_executable_file_to(path: impl AsRef<Path>, bytes: impl AsRef<[u8]>) -> Result<()> {
|
async fn write_executable_file_to(path: impl AsRef<Path>, bytes: impl AsRef<[u8]>) -> Result<()> {
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
|
||||||
use directories::UserDirs;
|
|
||||||
use futures_util::future::try_join_all;
|
|
||||||
use include_dir::Dir;
|
|
||||||
use tokio::fs::{create_dir_all, write};
|
|
||||||
|
|
||||||
pub async fn generate_typedef_files_from_definitions(dir: &Dir<'_>) -> Result<String> {
|
|
||||||
let contents = read_typedefs_dir_contents(dir);
|
|
||||||
write_typedef_files(contents).await
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_typedefs_dir_contents(dir: &Dir<'_>) -> HashMap<String, Vec<u8>> {
|
|
||||||
let mut definitions = HashMap::new();
|
|
||||||
|
|
||||||
for entry in dir.find("*.luau").unwrap() {
|
|
||||||
let entry_file = entry.as_file().unwrap();
|
|
||||||
let entry_name = entry_file.path().file_name().unwrap().to_string_lossy();
|
|
||||||
|
|
||||||
let typedef_name = entry_name.trim_end_matches(".luau");
|
|
||||||
let typedef_contents = entry_file.contents().to_vec();
|
|
||||||
|
|
||||||
definitions.insert(typedef_name.to_string(), typedef_contents);
|
|
||||||
}
|
|
||||||
|
|
||||||
definitions
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn write_typedef_files(typedef_files: HashMap<String, Vec<u8>>) -> Result<String> {
|
|
||||||
let version_string = env!("CARGO_PKG_VERSION");
|
|
||||||
let mut dirs_to_write = Vec::new();
|
|
||||||
let mut files_to_write = Vec::new();
|
|
||||||
// Create the typedefs dir in the users cache dir
|
|
||||||
let cache_dir = UserDirs::new()
|
|
||||||
.context("Failed to find user home directory")?
|
|
||||||
.home_dir()
|
|
||||||
.join(".lune")
|
|
||||||
.join(".typedefs")
|
|
||||||
.join(version_string);
|
|
||||||
dirs_to_write.push(cache_dir.clone());
|
|
||||||
// Make typedef files
|
|
||||||
for (builtin_name, builtin_typedef) in typedef_files {
|
|
||||||
let path = cache_dir
|
|
||||||
.join(builtin_name.to_ascii_lowercase())
|
|
||||||
.with_extension("luau");
|
|
||||||
files_to_write.push((builtin_name.to_lowercase(), path, builtin_typedef));
|
|
||||||
}
|
|
||||||
// Write all dirs and files only when we know generation was successful
|
|
||||||
let futs_dirs = dirs_to_write
|
|
||||||
.drain(..)
|
|
||||||
.map(create_dir_all)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let futs_files = files_to_write
|
|
||||||
.iter()
|
|
||||||
.map(|(_, path, contents)| write(path, contents))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
try_join_all(futs_dirs).await?;
|
|
||||||
try_join_all(futs_files).await?;
|
|
||||||
Ok(version_string.to_string())
|
|
||||||
}
|
|
47
src/cli/list.rs
Normal file
47
src/cli/list.rs
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
use std::{fmt::Write as _, process::ExitCode};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
use super::utils::listing::{find_lune_scripts, sort_lune_scripts, write_lune_scripts_list};
|
||||||
|
|
||||||
|
/// List scripts available to run
|
||||||
|
#[derive(Debug, Clone, Parser)]
|
||||||
|
pub struct ListCommand {}
|
||||||
|
|
||||||
|
impl ListCommand {
|
||||||
|
pub async fn run(self) -> Result<ExitCode> {
|
||||||
|
let sorted_relative = find_lune_scripts(false).await.map(sort_lune_scripts);
|
||||||
|
|
||||||
|
let sorted_home_dir = find_lune_scripts(true).await.map(sort_lune_scripts);
|
||||||
|
if sorted_relative.is_err() && sorted_home_dir.is_err() {
|
||||||
|
eprintln!("{}", sorted_relative.unwrap_err());
|
||||||
|
return Ok(ExitCode::FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sorted_relative = sorted_relative.unwrap_or(Vec::new());
|
||||||
|
let sorted_home_dir = sorted_home_dir.unwrap_or(Vec::new());
|
||||||
|
|
||||||
|
let mut buffer = String::new();
|
||||||
|
if !sorted_relative.is_empty() {
|
||||||
|
if sorted_home_dir.is_empty() {
|
||||||
|
write!(&mut buffer, "Available scripts:")?;
|
||||||
|
} else {
|
||||||
|
write!(&mut buffer, "Available scripts in current directory:")?;
|
||||||
|
}
|
||||||
|
write_lune_scripts_list(&mut buffer, sorted_relative)?;
|
||||||
|
}
|
||||||
|
if !sorted_home_dir.is_empty() {
|
||||||
|
write!(&mut buffer, "Available global scripts:")?;
|
||||||
|
write_lune_scripts_list(&mut buffer, sorted_home_dir)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if buffer.is_empty() {
|
||||||
|
println!("No scripts found.");
|
||||||
|
} else {
|
||||||
|
print!("{buffer}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ExitCode::SUCCESS)
|
||||||
|
}
|
||||||
|
}
|
196
src/cli/mod.rs
196
src/cli/mod.rs
|
@ -1,180 +1,54 @@
|
||||||
use std::{env, fmt::Write as _, path::PathBuf, process::ExitCode};
|
use std::process::ExitCode;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::Result;
|
||||||
use clap::Parser;
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
use lune::Lune;
|
|
||||||
use tokio::{
|
|
||||||
fs::read as read_to_vec,
|
|
||||||
io::{stdin, AsyncReadExt},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub(crate) mod build;
|
pub(crate) mod build;
|
||||||
pub(crate) mod gen;
|
pub(crate) mod list;
|
||||||
pub(crate) mod repl;
|
pub(crate) mod repl;
|
||||||
|
pub(crate) mod run;
|
||||||
pub(crate) mod setup;
|
pub(crate) mod setup;
|
||||||
pub(crate) mod utils;
|
pub(crate) mod utils;
|
||||||
|
|
||||||
use setup::run_setup;
|
pub use self::{
|
||||||
use utils::{
|
build::BuildCommand, list::ListCommand, repl::ReplCommand, run::RunCommand, setup::SetupCommand,
|
||||||
files::{discover_script_path_including_lune_dirs, strip_shebang},
|
|
||||||
listing::{find_lune_scripts, sort_lune_scripts, write_lune_scripts_list},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::build::build_standalone;
|
#[derive(Debug, Clone, Subcommand)]
|
||||||
|
pub enum CliSubcommand {
|
||||||
/// A Luau script runner
|
Run(RunCommand),
|
||||||
#[derive(Parser, Debug, Default, Clone)]
|
List(ListCommand),
|
||||||
#[command(version, long_about = None)]
|
Setup(SetupCommand),
|
||||||
pub struct Cli {
|
Build(BuildCommand),
|
||||||
/// Script name or full path to the file to run
|
Repl(ReplCommand),
|
||||||
script_path: Option<String>,
|
}
|
||||||
/// Arguments to pass to the script, stored in process.args
|
|
||||||
script_args: Vec<String>,
|
impl Default for CliSubcommand {
|
||||||
/// List scripts found inside of a nearby `lune` directory
|
fn default() -> Self {
|
||||||
#[clap(long, short = 'l')]
|
Self::Repl(ReplCommand::default())
|
||||||
list: bool,
|
}
|
||||||
/// Set up type definitions and settings for development
|
}
|
||||||
#[clap(long)]
|
|
||||||
setup: bool,
|
/// Lune, a standalone Luau runtime
|
||||||
/// Build a Luau file to an OS-Native standalone executable
|
#[derive(Parser, Debug, Default, Clone)]
|
||||||
#[clap(long)]
|
#[command(version, about, long_about = None)]
|
||||||
build: bool,
|
pub struct Cli {
|
||||||
|
#[clap(subcommand)]
|
||||||
|
subcommand: Option<CliSubcommand>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
impl Cli {
|
impl Cli {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::parse()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_path<S>(mut self, path: S) -> Self
|
|
||||||
where
|
|
||||||
S: Into<String>,
|
|
||||||
{
|
|
||||||
self.script_path = Some(path.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_args<A>(mut self, args: A) -> Self
|
|
||||||
where
|
|
||||||
A: Into<Vec<String>>,
|
|
||||||
{
|
|
||||||
self.script_args = args.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setup(mut self) -> Self {
|
|
||||||
self.setup = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list(mut self) -> Self {
|
|
||||||
self.list = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
pub async fn run(self) -> Result<ExitCode> {
|
pub async fn run(self) -> Result<ExitCode> {
|
||||||
// List files in `lune` and `.lune` directories, if wanted
|
match self.subcommand.unwrap_or_default() {
|
||||||
// This will also exit early and not run anything else
|
CliSubcommand::Run(cmd) => cmd.run().await,
|
||||||
if self.list {
|
CliSubcommand::List(cmd) => cmd.run().await,
|
||||||
let sorted_relative = find_lune_scripts(false).await.map(sort_lune_scripts);
|
CliSubcommand::Setup(cmd) => cmd.run().await,
|
||||||
|
CliSubcommand::Build(cmd) => cmd.run().await,
|
||||||
let sorted_home_dir = find_lune_scripts(true).await.map(sort_lune_scripts);
|
CliSubcommand::Repl(cmd) => cmd.run().await,
|
||||||
if sorted_relative.is_err() && sorted_home_dir.is_err() {
|
|
||||||
eprintln!("{}", sorted_relative.unwrap_err());
|
|
||||||
return Ok(ExitCode::FAILURE);
|
|
||||||
}
|
|
||||||
|
|
||||||
let sorted_relative = sorted_relative.unwrap_or(Vec::new());
|
|
||||||
let sorted_home_dir = sorted_home_dir.unwrap_or(Vec::new());
|
|
||||||
|
|
||||||
let mut buffer = String::new();
|
|
||||||
if !sorted_relative.is_empty() {
|
|
||||||
if sorted_home_dir.is_empty() {
|
|
||||||
write!(&mut buffer, "Available scripts:")?;
|
|
||||||
} else {
|
|
||||||
write!(&mut buffer, "Available scripts in current directory:")?;
|
|
||||||
}
|
|
||||||
write_lune_scripts_list(&mut buffer, sorted_relative)?;
|
|
||||||
}
|
|
||||||
if !sorted_home_dir.is_empty() {
|
|
||||||
write!(&mut buffer, "Available global scripts:")?;
|
|
||||||
write_lune_scripts_list(&mut buffer, sorted_home_dir)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if buffer.is_empty() {
|
|
||||||
println!("No scripts found.");
|
|
||||||
} else {
|
|
||||||
print!("{buffer}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(ExitCode::SUCCESS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate (save) definition files, if wanted
|
|
||||||
if self.setup {
|
|
||||||
run_setup().await;
|
|
||||||
}
|
|
||||||
if self.script_path.is_none() {
|
|
||||||
// Only generating typedefs without running a script is completely
|
|
||||||
// fine, and we should just exit the program normally afterwards
|
|
||||||
if self.setup {
|
|
||||||
return Ok(ExitCode::SUCCESS);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not in a standalone context and we don't have any arguments
|
|
||||||
// display the interactive REPL interface
|
|
||||||
return repl::show_interface().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Figure out if we should read from stdin or from a file,
|
|
||||||
// reading from stdin is marked by passing a single "-"
|
|
||||||
// (dash) as the script name to run to the cli
|
|
||||||
let script_path = self.script_path.unwrap();
|
|
||||||
|
|
||||||
let (script_display_name, script_contents) = if script_path == "-" {
|
|
||||||
let mut stdin_contents = Vec::new();
|
|
||||||
stdin()
|
|
||||||
.read_to_end(&mut stdin_contents)
|
|
||||||
.await
|
|
||||||
.context("Failed to read script contents from stdin")?;
|
|
||||||
("stdin".to_string(), stdin_contents)
|
|
||||||
} else {
|
|
||||||
let file_path = discover_script_path_including_lune_dirs(&script_path)?;
|
|
||||||
let file_contents = read_to_vec(&file_path).await?;
|
|
||||||
// NOTE: We skip the extension here to remove it from stack traces
|
|
||||||
let file_display_name = file_path.with_extension("").display().to_string();
|
|
||||||
(file_display_name, file_contents)
|
|
||||||
};
|
|
||||||
|
|
||||||
if self.build {
|
|
||||||
let output_path =
|
|
||||||
PathBuf::from(script_path.clone()).with_extension(env::consts::EXE_EXTENSION);
|
|
||||||
|
|
||||||
return Ok(
|
|
||||||
match build_standalone(script_path, output_path, script_contents).await {
|
|
||||||
Ok(exitcode) => exitcode,
|
|
||||||
Err(err) => {
|
|
||||||
eprintln!("{err}");
|
|
||||||
ExitCode::FAILURE
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new lune object with all globals & run the script
|
|
||||||
let result = Lune::new()
|
|
||||||
.with_args(self.script_args)
|
|
||||||
.run(&script_display_name, strip_shebang(script_contents))
|
|
||||||
.await;
|
|
||||||
Ok(match result {
|
|
||||||
Err(err) => {
|
|
||||||
eprintln!("{err}");
|
|
||||||
ExitCode::FAILURE
|
|
||||||
}
|
|
||||||
Ok(code) => code,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
169
src/cli/repl.rs
169
src/cli/repl.rs
|
@ -1,6 +1,7 @@
|
||||||
use std::{path::PathBuf, process::ExitCode};
|
use std::{path::PathBuf, process::ExitCode};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
use clap::Parser;
|
||||||
use directories::UserDirs;
|
use directories::UserDirs;
|
||||||
use rustyline::{error::ReadlineError, DefaultEditor};
|
use rustyline::{error::ReadlineError, DefaultEditor};
|
||||||
|
|
||||||
|
@ -14,87 +15,93 @@ enum PromptState {
|
||||||
Continuation,
|
Continuation,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn show_interface() -> Result<ExitCode> {
|
/// Launch an interactive REPL (default)
|
||||||
println!("{MESSAGE_WELCOME}");
|
#[derive(Debug, Clone, Default, Parser)]
|
||||||
|
pub struct ReplCommand {}
|
||||||
|
|
||||||
let history_file_path: &PathBuf = &UserDirs::new()
|
impl ReplCommand {
|
||||||
.context("Failed to find user home directory")?
|
pub async fn run(self) -> Result<ExitCode> {
|
||||||
.home_dir()
|
println!("{MESSAGE_WELCOME}");
|
||||||
.join(".lune_history");
|
|
||||||
if !history_file_path.exists() {
|
let history_file_path: &PathBuf = &UserDirs::new()
|
||||||
tokio::fs::write(history_file_path, &[]).await?;
|
.context("Failed to find user home directory")?
|
||||||
|
.home_dir()
|
||||||
|
.join(".lune_history");
|
||||||
|
if !history_file_path.exists() {
|
||||||
|
tokio::fs::write(history_file_path, &[]).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut repl = DefaultEditor::new()?;
|
||||||
|
repl.load_history(history_file_path)?;
|
||||||
|
|
||||||
|
let mut interrupt_counter = 0;
|
||||||
|
let mut prompt_state = PromptState::Regular;
|
||||||
|
let mut source_code = String::new();
|
||||||
|
|
||||||
|
let mut lune_instance = Lune::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let prompt = match prompt_state {
|
||||||
|
PromptState::Regular => "> ",
|
||||||
|
PromptState::Continuation => ">> ",
|
||||||
|
};
|
||||||
|
|
||||||
|
match repl.readline(prompt) {
|
||||||
|
Ok(code) => {
|
||||||
|
interrupt_counter = 0;
|
||||||
|
|
||||||
|
// TODO: Should we add history entries for each separate line?
|
||||||
|
// Or should we add and save history only when we have complete
|
||||||
|
// lua input that may or may not be multiple lines long?
|
||||||
|
repl.add_history_entry(&code)?;
|
||||||
|
repl.save_history(history_file_path)?;
|
||||||
|
|
||||||
|
match prompt_state {
|
||||||
|
PromptState::Regular => source_code = code,
|
||||||
|
PromptState::Continuation => source_code.push_str(&code),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(ReadlineError::Eof) => break,
|
||||||
|
Err(ReadlineError::Interrupted) => {
|
||||||
|
interrupt_counter += 1;
|
||||||
|
|
||||||
|
// NOTE: We actually want the user to do ^C twice to exit,
|
||||||
|
// and if we get an interrupt we should continue to the next
|
||||||
|
// readline loop iteration so we don't run input code twice
|
||||||
|
if interrupt_counter == 1 {
|
||||||
|
println!("{MESSAGE_INTERRUPT}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("REPL ERROR: {err}");
|
||||||
|
return Ok(ExitCode::FAILURE);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Preserve context here somehow?
|
||||||
|
let eval_result = lune_instance.run("REPL", &source_code).await;
|
||||||
|
|
||||||
|
match eval_result {
|
||||||
|
Ok(_) => prompt_state = PromptState::Regular,
|
||||||
|
|
||||||
|
Err(err) => {
|
||||||
|
if err.is_incomplete_input() {
|
||||||
|
prompt_state = PromptState::Continuation;
|
||||||
|
source_code.push('\n');
|
||||||
|
} else {
|
||||||
|
eprintln!("{err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
repl.save_history(history_file_path)?;
|
||||||
|
|
||||||
|
Ok(ExitCode::SUCCESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut repl = DefaultEditor::new()?;
|
|
||||||
repl.load_history(history_file_path)?;
|
|
||||||
|
|
||||||
let mut interrupt_counter = 0;
|
|
||||||
let mut prompt_state = PromptState::Regular;
|
|
||||||
let mut source_code = String::new();
|
|
||||||
|
|
||||||
let mut lune_instance = Lune::new();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let prompt = match prompt_state {
|
|
||||||
PromptState::Regular => "> ",
|
|
||||||
PromptState::Continuation => ">> ",
|
|
||||||
};
|
|
||||||
|
|
||||||
match repl.readline(prompt) {
|
|
||||||
Ok(code) => {
|
|
||||||
interrupt_counter = 0;
|
|
||||||
|
|
||||||
// TODO: Should we add history entries for each separate line?
|
|
||||||
// Or should we add and save history only when we have complete
|
|
||||||
// lua input that may or may not be multiple lines long?
|
|
||||||
repl.add_history_entry(&code)?;
|
|
||||||
repl.save_history(history_file_path)?;
|
|
||||||
|
|
||||||
match prompt_state {
|
|
||||||
PromptState::Regular => source_code = code,
|
|
||||||
PromptState::Continuation => source_code.push_str(&code),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(ReadlineError::Eof) => break,
|
|
||||||
Err(ReadlineError::Interrupted) => {
|
|
||||||
interrupt_counter += 1;
|
|
||||||
|
|
||||||
// NOTE: We actually want the user to do ^C twice to exit,
|
|
||||||
// and if we get an interrupt we should continue to the next
|
|
||||||
// readline loop iteration so we don't run input code twice
|
|
||||||
if interrupt_counter == 1 {
|
|
||||||
println!("{MESSAGE_INTERRUPT}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(err) => {
|
|
||||||
eprintln!("REPL ERROR: {err}");
|
|
||||||
return Ok(ExitCode::FAILURE);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Preserve context here somehow?
|
|
||||||
let eval_result = lune_instance.run("REPL", &source_code).await;
|
|
||||||
|
|
||||||
match eval_result {
|
|
||||||
Ok(_) => prompt_state = PromptState::Regular,
|
|
||||||
|
|
||||||
Err(err) => {
|
|
||||||
if err.is_incomplete_input() {
|
|
||||||
prompt_state = PromptState::Continuation;
|
|
||||||
source_code.push('\n');
|
|
||||||
} else {
|
|
||||||
eprintln!("{err}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
repl.save_history(history_file_path)?;
|
|
||||||
|
|
||||||
Ok(ExitCode::SUCCESS)
|
|
||||||
}
|
}
|
||||||
|
|
56
src/cli/run.rs
Normal file
56
src/cli/run.rs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
use std::process::ExitCode;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use clap::Parser;
|
||||||
|
use tokio::{
|
||||||
|
fs::read as read_to_vec,
|
||||||
|
io::{stdin, AsyncReadExt as _},
|
||||||
|
};
|
||||||
|
|
||||||
|
use lune::Lune;
|
||||||
|
|
||||||
|
use super::utils::files::{discover_script_path_including_lune_dirs, strip_shebang};
|
||||||
|
|
||||||
|
/// Run a script
|
||||||
|
#[derive(Debug, Clone, Parser)]
|
||||||
|
pub struct RunCommand {
|
||||||
|
/// Script name or full path to the file to run
|
||||||
|
script_path: String,
|
||||||
|
/// Arguments to pass to the script, stored in process.args
|
||||||
|
script_args: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RunCommand {
|
||||||
|
pub async fn run(self) -> Result<ExitCode> {
|
||||||
|
// Figure out if we should read from stdin or from a file,
|
||||||
|
// reading from stdin is marked by passing a single "-"
|
||||||
|
// (dash) as the script name to run to the cli
|
||||||
|
let (script_display_name, script_contents) = if &self.script_path == "-" {
|
||||||
|
let mut stdin_contents = Vec::new();
|
||||||
|
stdin()
|
||||||
|
.read_to_end(&mut stdin_contents)
|
||||||
|
.await
|
||||||
|
.context("Failed to read script contents from stdin")?;
|
||||||
|
("stdin".to_string(), stdin_contents)
|
||||||
|
} else {
|
||||||
|
let file_path = discover_script_path_including_lune_dirs(&self.script_path)?;
|
||||||
|
let file_contents = read_to_vec(&file_path).await?;
|
||||||
|
// NOTE: We skip the extension here to remove it from stack traces
|
||||||
|
let file_display_name = file_path.with_extension("").display().to_string();
|
||||||
|
(file_display_name, file_contents)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a new lune object with all globals & run the script
|
||||||
|
let result = Lune::new()
|
||||||
|
.with_args(self.script_args)
|
||||||
|
.run(&script_display_name, strip_shebang(script_contents))
|
||||||
|
.await;
|
||||||
|
Ok(match result {
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("{err}");
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
Ok(code) => code,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
129
src/cli/setup.rs
129
src/cli/setup.rs
|
@ -1,6 +1,12 @@
|
||||||
use std::{borrow::BorrowMut, env::current_dir, io::ErrorKind, path::PathBuf};
|
use std::{
|
||||||
|
borrow::BorrowMut, collections::HashMap, env::current_dir, io::ErrorKind, path::PathBuf,
|
||||||
|
process::ExitCode,
|
||||||
|
};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
|
use clap::Parser;
|
||||||
|
use directories::UserDirs;
|
||||||
|
use futures_util::future::try_join_all;
|
||||||
use include_dir::{include_dir, Dir};
|
use include_dir::{include_dir, Dir};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
@ -8,13 +14,50 @@ use tokio::fs;
|
||||||
// TODO: Use a library that supports json with comments since VSCode settings may contain comments
|
// TODO: Use a library that supports json with comments since VSCode settings may contain comments
|
||||||
use serde_json::Value as JsonValue;
|
use serde_json::Value as JsonValue;
|
||||||
|
|
||||||
use super::gen::generate_typedef_files_from_definitions;
|
|
||||||
|
|
||||||
pub(crate) static TYPEDEFS_DIR: Dir<'_> = include_dir!("types");
|
pub(crate) static TYPEDEFS_DIR: Dir<'_> = include_dir!("types");
|
||||||
|
|
||||||
pub(crate) static SETTING_NAME_MODE: &str = "luau-lsp.require.mode";
|
pub(crate) static SETTING_NAME_MODE: &str = "luau-lsp.require.mode";
|
||||||
pub(crate) static SETTING_NAME_ALIASES: &str = "luau-lsp.require.directoryAliases";
|
pub(crate) static SETTING_NAME_ALIASES: &str = "luau-lsp.require.directoryAliases";
|
||||||
|
|
||||||
|
/// Set up type definitions for your editor
|
||||||
|
#[derive(Debug, Clone, Parser)]
|
||||||
|
pub struct SetupCommand {}
|
||||||
|
|
||||||
|
impl SetupCommand {
|
||||||
|
pub async fn run(self) -> Result<ExitCode> {
|
||||||
|
generate_typedef_files_from_definitions(&TYPEDEFS_DIR)
|
||||||
|
.await
|
||||||
|
.expect("Failed to generate typedef files");
|
||||||
|
|
||||||
|
// TODO: Let the user interactively choose what editor to set up
|
||||||
|
let res = async {
|
||||||
|
let settings = read_or_create_vscode_settings_json().await?;
|
||||||
|
let modified = add_values_to_vscode_settings_json(settings);
|
||||||
|
write_vscode_settings_json(modified).await?;
|
||||||
|
Ok::<_, SetupError>(())
|
||||||
|
}
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let message = match res {
|
||||||
|
Ok(()) => "These settings have been added to your workspace for Visual Studio Code:",
|
||||||
|
Err(_) => "To finish setting up your editor, add these settings to your workspace:",
|
||||||
|
};
|
||||||
|
|
||||||
|
let version_string = lune_version();
|
||||||
|
println!(
|
||||||
|
"Lune has now been set up and editor type definitions have been generated.\
|
||||||
|
\n{message}\
|
||||||
|
\n\
|
||||||
|
\n\"{SETTING_NAME_MODE}\": \"relativeToFile\",\
|
||||||
|
\n\"{SETTING_NAME_ALIASES}\": {{\
|
||||||
|
\n \"@lune/\": \"~/.lune/.typedefs/{version_string}/\"\
|
||||||
|
\n}}",
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(ExitCode::SUCCESS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Error)]
|
#[derive(Debug, Clone, Copy, Error)]
|
||||||
enum SetupError {
|
enum SetupError {
|
||||||
#[error("Failed to read settings")]
|
#[error("Failed to read settings")]
|
||||||
|
@ -99,30 +142,56 @@ fn add_values_to_vscode_settings_json(value: JsonValue) -> JsonValue {
|
||||||
settings_json
|
settings_json
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run_setup() {
|
async fn generate_typedef_files_from_definitions(dir: &Dir<'_>) -> Result<String> {
|
||||||
generate_typedef_files_from_definitions(&TYPEDEFS_DIR)
|
let contents = read_typedefs_dir_contents(dir);
|
||||||
.await
|
write_typedef_files(contents).await
|
||||||
.expect("Failed to generate typedef files");
|
}
|
||||||
// TODO: Let the user interactively choose what editor to set up
|
|
||||||
let res = async {
|
fn read_typedefs_dir_contents(dir: &Dir<'_>) -> HashMap<String, Vec<u8>> {
|
||||||
let settings = read_or_create_vscode_settings_json().await?;
|
let mut definitions = HashMap::new();
|
||||||
let modified = add_values_to_vscode_settings_json(settings);
|
|
||||||
write_vscode_settings_json(modified).await?;
|
for entry in dir.find("*.luau").unwrap() {
|
||||||
Ok::<_, SetupError>(())
|
let entry_file = entry.as_file().unwrap();
|
||||||
}
|
let entry_name = entry_file.path().file_name().unwrap().to_string_lossy();
|
||||||
.await;
|
|
||||||
let message = match res {
|
let typedef_name = entry_name.trim_end_matches(".luau");
|
||||||
Ok(()) => "These settings have been added to your workspace for Visual Studio Code:",
|
let typedef_contents = entry_file.contents().to_vec();
|
||||||
Err(_) => "To finish setting up your editor, add these settings to your workspace:",
|
|
||||||
};
|
definitions.insert(typedef_name.to_string(), typedef_contents);
|
||||||
let version_string = lune_version();
|
}
|
||||||
println!(
|
|
||||||
"Lune has now been set up and editor type definitions have been generated.\
|
definitions
|
||||||
\n{message}\
|
}
|
||||||
\n\
|
|
||||||
\n\"{SETTING_NAME_MODE}\": \"relativeToFile\",\
|
async fn write_typedef_files(typedef_files: HashMap<String, Vec<u8>>) -> Result<String> {
|
||||||
\n\"{SETTING_NAME_ALIASES}\": {{\
|
let version_string = env!("CARGO_PKG_VERSION");
|
||||||
\n \"@lune/\": \"~/.lune/.typedefs/{version_string}/\"\
|
let mut dirs_to_write = Vec::new();
|
||||||
\n}}",
|
let mut files_to_write = Vec::new();
|
||||||
);
|
// Create the typedefs dir in the users cache dir
|
||||||
|
let cache_dir = UserDirs::new()
|
||||||
|
.context("Failed to find user home directory")?
|
||||||
|
.home_dir()
|
||||||
|
.join(".lune")
|
||||||
|
.join(".typedefs")
|
||||||
|
.join(version_string);
|
||||||
|
dirs_to_write.push(cache_dir.clone());
|
||||||
|
// Make typedef files
|
||||||
|
for (builtin_name, builtin_typedef) in typedef_files {
|
||||||
|
let path = cache_dir
|
||||||
|
.join(builtin_name.to_ascii_lowercase())
|
||||||
|
.with_extension("luau");
|
||||||
|
files_to_write.push((builtin_name.to_lowercase(), path, builtin_typedef));
|
||||||
|
}
|
||||||
|
// Write all dirs and files only when we know generation was successful
|
||||||
|
let futs_dirs = dirs_to_write
|
||||||
|
.drain(..)
|
||||||
|
.map(fs::create_dir_all)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let futs_files = files_to_write
|
||||||
|
.iter()
|
||||||
|
.map(|(_, path, contents)| fs::write(path, contents))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
try_join_all(futs_dirs).await?;
|
||||||
|
try_join_all(futs_files).await?;
|
||||||
|
Ok(version_string.to_string())
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,6 @@
|
||||||
|
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
use clap::Parser;
|
|
||||||
|
|
||||||
pub(crate) mod cli;
|
pub(crate) mod cli;
|
||||||
pub(crate) mod executor;
|
pub(crate) mod executor;
|
||||||
|
|
||||||
|
@ -36,7 +34,7 @@ async fn main() -> ExitCode {
|
||||||
return executor::run_standalone(bin).await.unwrap();
|
return executor::run_standalone(bin).await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
match Cli::parse().run().await {
|
match Cli::new().run().await {
|
||||||
Ok(code) => code,
|
Ok(code) => code,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
|
|
Loading…
Reference in a new issue