diff --git a/Rune.toml b/Rune.toml new file mode 100644 index 000000000..0703e2970 --- /dev/null +++ b/Rune.toml @@ -0,0 +1,5 @@ +[workspace] +members = [ + "benches", + "examples", +] diff --git a/benches/Rune.toml b/benches/Rune.toml new file mode 100644 index 000000000..095f23bdc --- /dev/null +++ b/benches/Rune.toml @@ -0,0 +1,3 @@ +[package] +name = "rune-benches" +version = "0.0.0" diff --git a/crates/rune-cli/Cargo.toml b/crates/rune-cli/Cargo.toml index 283d9abdd..80417bbee 100644 --- a/crates/rune-cli/Cargo.toml +++ b/crates/rune-cli/Cargo.toml @@ -24,8 +24,8 @@ codespan-reporting = "0.11.1" anyhow = { version = "1.0.49", features = ["std"] } structopt = { version = "0.3.25", default-features = false, features = ["wrap_help", "suggestions", "color"] } -rune = {version = "0.10.0", path = "../rune"} -rune-modules = {version = "0.10.0", path = "../rune-modules", features = ["full", "experiments"]} +rune = { version = "0.10.0", path = "../rune", features = ["workspace"] } +rune-modules = { version = "0.10.0", path = "../rune-modules", features = ["full", "experiments"] } [build-dependencies] anyhow = "1.0.49" diff --git a/crates/rune-cli/src/benches.rs b/crates/rune-cli/src/benches.rs index 47ee4ba77..798334fd4 100644 --- a/crates/rune-cli/src/benches.rs +++ b/crates/rune-cli/src/benches.rs @@ -1,7 +1,6 @@ -use crate::{ExitCode, SharedFlags}; +use crate::{ExitCode, Io, SharedFlags}; use rune::compile::{Item, Meta}; use rune::runtime::{Function, Unit, Value}; -use rune::termcolor::StandardStream; use rune::{Any, Context, ContextError, Hash, Module, Sources}; use rune_modules::capture_io::CaptureIo; use std::fmt; @@ -45,10 +44,10 @@ pub(crate) fn test_module() -> Result { /// Run benchmarks. pub(crate) async fn run( - o: &mut StandardStream, + io: &mut Io<'_>, args: &Flags, context: &Context, - io: Option<&CaptureIo>, + capture_io: Option<&CaptureIo>, unit: Arc, sources: &Sources, fns: &[(Hash, Meta)], @@ -56,7 +55,7 @@ pub(crate) async fn run( let runtime = Arc::new(context.runtime()); let mut vm = rune::Vm::new(runtime, unit); - writeln!(o, "Found {} benches...", fns.len())?; + writeln!(io.stdout, "Found {} benches...", fns.len())?; let mut any_error = false; @@ -65,14 +64,14 @@ pub(crate) async fn run( let mut bencher = Bencher::default(); if let Err(error) = vm.call(*hash, (&mut bencher,)) { - writeln!(o, "{}: Error in benchmark", item)?; - error.emit(o, sources)?; + writeln!(io.stdout, "{}: Error in benchmark", item)?; + error.emit(io.stdout, sources)?; any_error = true; - if let Some(io) = io { - writeln!(o, "-- output --")?; - io.drain_into(&mut *o)?; - writeln!(o, "-- end output --")?; + if let Some(capture_io) = capture_io { + writeln!(io.stdout, "-- output --")?; + capture_io.drain_into(&mut *io.stdout)?; + writeln!(io.stdout, "-- end output --")?; } continue; @@ -81,13 +80,13 @@ pub(crate) async fn run( let multiple = bencher.fns.len() > 1; for (i, f) in bencher.fns.iter().enumerate() { - if let Err(e) = bench_fn(o, i, item, args, f, multiple) { - writeln!(o, "{}: Error in bench iteration: {}", item, e)?; + if let Err(e) = bench_fn(io, i, item, args, f, multiple) { + writeln!(io.stdout, "{}: Error in bench iteration: {}", item, e)?; - if let Some(io) = io { - writeln!(o, "-- output --")?; - io.drain_into(&mut *o)?; - writeln!(o, "-- end output --")?; + if let Some(capture_io) = capture_io { + writeln!(io.stdout, "-- output --")?; + capture_io.drain_into(&mut *io.stdout)?; + writeln!(io.stdout, "-- end output --")?; } any_error = true; @@ -103,7 +102,7 @@ pub(crate) async fn run( } fn bench_fn( - o: &mut StandardStream, + io: &mut Io<'_>, i: usize, item: &Item, args: &Flags, @@ -145,9 +144,9 @@ fn bench_fn( }; if multiple { - writeln!(o, "bench {}#{}: {}", item, i, format)?; + writeln!(io.stdout, "bench {}#{}: {}", item, i, format)?; } else { - writeln!(o, "bench {}: {}", item, format)?; + writeln!(io.stdout, "bench {}: {}", item, format)?; } Ok(()) diff --git a/crates/rune-cli/src/check.rs b/crates/rune-cli/src/check.rs index c15b049f8..f55336495 100644 --- a/crates/rune-cli/src/check.rs +++ b/crates/rune-cli/src/check.rs @@ -1,7 +1,6 @@ -use crate::{visitor, ExitCode, SharedFlags}; +use crate::{visitor, Config, ExitCode, Io, SharedFlags}; use anyhow::{Context, Result}; use rune::compile::FileSourceLoader; -use rune::termcolor::StandardStream; use rune::{Diagnostics, Options, Source, Sources}; use std::io::Write; use std::path::Path; @@ -18,14 +17,15 @@ pub(crate) struct Flags { } pub(crate) fn run( - o: &mut StandardStream, + io: &mut Io<'_>, + c: &Config, flags: &Flags, options: &Options, path: &Path, ) -> Result { - writeln!(o, "Checking: {}", path.display())?; + writeln!(io.stdout, "Checking: {}", path.display())?; - let context = flags.shared.context()?; + let context = flags.shared.context(c)?; let source = Source::from_path(path).with_context(|| format!("reading file: {}", path.display()))?; @@ -51,7 +51,7 @@ pub(crate) fn run( .with_source_loader(&mut source_loader) .build(); - diagnostics.emit(o, &sources)?; + diagnostics.emit(&mut io.stdout.lock(), &sources)?; if diagnostics.has_error() || flags.warnings_are_errors && diagnostics.has_warning() { Ok(ExitCode::Failure) diff --git a/crates/rune-cli/src/loader.rs b/crates/rune-cli/src/loader.rs index 07a6f688e..4ce31c0df 100644 --- a/crates/rune-cli/src/loader.rs +++ b/crates/rune-cli/src/loader.rs @@ -1,15 +1,13 @@ -use crate::{visitor, Args}; +use crate::{visitor, Args, Io}; use anyhow::{anyhow, Context as _, Result}; use rune::compile::FileSourceLoader; use rune::compile::Meta; -use rune::termcolor::StandardStream; use rune::Diagnostics; use rune::{Context, Hash, Options, Source, Sources, Unit}; use std::collections::VecDeque; use std::ffi::OsStr; use std::fs; use std::io; -use std::path::PathBuf; use std::{path::Path, sync::Arc}; pub(crate) struct Load { @@ -20,14 +18,14 @@ pub(crate) struct Load { /// Load context and code for a given path pub(crate) fn load( - o: &mut StandardStream, + io: &mut Io<'_>, context: &Context, args: &Args, options: &Options, path: &Path, attribute: visitor::Attribute, ) -> Result { - let shared = args.shared(); + let shared = args.cmd.shared(); let bytecode_path = path.with_extension("rnc"); @@ -79,7 +77,7 @@ pub(crate) fn load( .with_source_loader(&mut source_loader) .build(); - diagnostics.emit(o, &sources)?; + diagnostics.emit(io.stdout, &sources)?; let unit = result?; if options.bytecode { @@ -112,11 +110,12 @@ fn should_cache_be_used(source: &Path, cached: &Path) -> io::Result { Ok(source.modified()? < cached.modified()?) } -pub(crate) fn walk_paths( +pub(crate) fn recurse_paths( recursive: bool, - paths: Vec, -) -> impl Iterator> { - let mut queue = paths.into_iter().collect::>(); + first: Box, +) -> impl Iterator>> { + let mut queue = VecDeque::with_capacity(1); + queue.push_back(first); std::iter::from_fn(move || loop { let path = queue.pop_front()?; @@ -144,7 +143,7 @@ pub(crate) fn walk_paths( Err(error) => return Some(Err(error)), }; - queue.push_back(e.path()); + queue.push_back(e.path().into()); } }) } diff --git a/crates/rune-cli/src/main.rs b/crates/rune-cli/src/main.rs index 9ecf12bad..f7b8b4674 100644 --- a/crates/rune-cli/src/main.rs +++ b/crates/rune-cli/src/main.rs @@ -49,9 +49,10 @@ //! [Rune Language]: https://rune-rs.github.io //! [rune]: https://github.com/rune-rs/rune -use anyhow::Result; +use anyhow::{anyhow, Result}; use rune::compile::ParseOptionError; use rune::termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; +use rune::workspace::WorkspaceFilter; use rune::{Context, ContextError, Options}; use rune_modules::capture_io::CaptureIo; use std::error::Error; @@ -68,6 +69,11 @@ mod visitor; pub const VERSION: &str = include_str!(concat!(env!("OUT_DIR"), "/version.txt")); +struct Io<'a> { + stdout: &'a mut StandardStream, + stderr: &'a mut StandardStream, +} + #[derive(StructOpt, Debug, Clone)] enum Command { /// Run checks but do not execute @@ -81,20 +87,94 @@ enum Command { } impl Command { - fn propagate_related_flags(&mut self) { + fn propagate_related_flags(&mut self, c: &mut Config) { match self { Command::Check(_) => {} - Command::Test(args) => { - args.shared.test = true; + Command::Test(..) => { + c.test = true; } - Command::Bench(args) => { - args.shared.test = true; + Command::Bench(..) => { + c.test = true; } Command::Run(args) => { args.propagate_related_flags(); } } } + + fn describe(&self) -> &'static str { + match self { + Command::Check(..) => "Checking", + Command::Test(..) => "Testing", + Command::Bench(..) => "Benchmarking", + Command::Run(..) => "Running", + } + } + + fn shared(&self) -> &SharedFlags { + match self { + Command::Check(args) => &args.shared, + Command::Test(args) => &args.shared, + Command::Bench(args) => &args.shared, + Command::Run(args) => &args.shared, + } + } + + fn bins_test(&self) -> Option> { + if !matches!(self, Command::Run(..) | Command::Check(..)) { + return None; + } + + let shared = self.shared(); + + Some(if let Some(name) = &shared.bin { + WorkspaceFilter::Name(name) + } else { + WorkspaceFilter::All + }) + } + + fn tests_test(&self) -> Option> { + if !matches!(self, Command::Test(..) | Command::Check(..)) { + return None; + } + + let shared = self.shared(); + + Some(if let Some(name) = &shared.test { + WorkspaceFilter::Name(name) + } else { + WorkspaceFilter::All + }) + } + + fn examples_test(&self) -> Option> { + if !matches!(self, Command::Run(..) | Command::Check(..)) { + return None; + } + + let shared = self.shared(); + + Some(if let Some(name) = &shared.example { + WorkspaceFilter::Name(name) + } else { + WorkspaceFilter::All + }) + } + + fn benches_test(&self) -> Option> { + if !matches!(self, Command::Bench(..) | Command::Check(..)) { + return None; + } + + let shared = self.shared(); + + Some(if let Some(name) = &shared.bench { + WorkspaceFilter::Name(name) + } else { + WorkspaceFilter::All + }) + } } #[derive(StructOpt, Debug, Clone)] @@ -105,10 +185,6 @@ struct SharedFlags { #[structopt(long)] experimental: bool, - /// Enabled the std::test experimental module. - #[structopt(long)] - test: bool, - /// Recursively load all files in the given directory. #[structopt(long)] recursive: bool, @@ -131,22 +207,64 @@ struct SharedFlags { #[structopt(name = "option", short = "O", number_of_values = 1)] compiler_options: Vec, + /// Run with the following binary from a loaded manifest. This requires a + /// `Rune.toml` manifest. + #[structopt(long = "bin")] + bin: Option, + + /// Run with the following test from a loaded manifest. This requires a + /// `Rune.toml` manifest. + #[structopt(long = "test")] + test: Option, + + /// Run with the following example from a loaded manifest. This requires a + /// `Rune.toml` manifest. + #[structopt(long = "example")] + example: Option, + + /// Run with the following benchmark by name from a loaded manifest. This + /// requires a `Rune.toml` manifest. + #[structopt(long = "bench")] + bench: Option, + /// All paths to include in the command. By default, the tool searches the /// current directory and some known files for candidates. #[structopt(parse(from_os_str))] paths: Vec, } +struct Package { + /// The name of the package the path belongs to. + name: Box, +} + +enum Entry { + /// A plain path entry. + Path(Box), + /// A path from a specific package. + PackagePath(Package, Box), +} + +#[derive(Default)] +struct Config { + /// Whether or not the test module should be included. + test: bool, + /// Whether or not to use verbose output. + verbose: bool, + /// The explicit paths to load. + entries: Vec, +} + impl SharedFlags { /// Construct a rune context according to the specified argument. - fn context(&self) -> Result { + fn context(&self, c: &Config) -> Result { let mut context = rune_modules::default_context()?; if self.experimental { context.install(&rune_modules::experiments::module(true)?)?; } - if self.test { + if c.test { context.install(&benches::test_module()?)?; } @@ -154,7 +272,7 @@ impl SharedFlags { } /// Setup a context that captures output. - fn context_with_capture(&self, io: &CaptureIo) -> Result { + fn context_with_capture(&self, c: &Config, io: &CaptureIo) -> Result { let mut context = rune_modules::with_config(false)?; context.install(&rune_modules::capture_io::module(io)?)?; @@ -163,7 +281,7 @@ impl SharedFlags { context.install(&rune_modules::experiments::module(true)?)?; } - if self.test { + if c.test { context.install(&benches::test_module()?)?; } @@ -205,32 +323,12 @@ impl Args { Command::Bench(_) | Command::Run(_) => (), } - for option in &self.shared().compiler_options { + for option in &self.cmd.shared().compiler_options { options.parse_option(option)?; } Ok(options) } - - /// Access shared arguments. - fn shared(&self) -> &SharedFlags { - match &self.cmd { - Command::Check(args) => &args.shared, - Command::Test(args) => &args.shared, - Command::Bench(args) => &args.shared, - Command::Run(args) => &args.shared, - } - } - - /// Access shared arguments mutably. - fn shared_mut(&mut self) -> &mut SharedFlags { - match &mut self.cmd { - Command::Check(args) => &mut args.shared, - Command::Test(args) => &mut args.shared, - Command::Bench(args) => &mut args.shared, - Command::Run(args) => &mut args.shared, - } - } } const SPECIAL_FILES: &[&str] = &[ @@ -315,12 +413,20 @@ async fn try_main() -> Result { _ => ColorChoice::Auto, }; - let mut o = StandardStream::stdout(choice); + let mut stdout = StandardStream::stdout(choice); + let mut stderr = StandardStream::stderr(choice); + + let mut io = Io { + stdout: &mut stdout, + stderr: &mut stderr, + }; + env_logger::init(); - match main_with_out(&mut o, args).await { + match main_with_out(&mut io, args).await { Ok(code) => Ok(code), Err(error) => { + let mut o = io.stdout.lock(); o.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?; let result = format_errors(&mut o, error.as_ref()); o.set_color(&ColorSpec::new())?; @@ -330,40 +436,129 @@ async fn try_main() -> Result { } } -async fn main_with_out(o: &mut StandardStream, mut args: Args) -> Result { - args.cmd.propagate_related_flags(); +fn populate_config(io: &mut Io<'_>, c: &mut Config, args: &Args) -> Result<()> { + c.entries.extend( + args.cmd + .shared() + .paths + .iter() + .map(|p| Entry::Path(p.as_path().into())), + ); + + if !c.entries.is_empty() { + return Ok(()); + } - let shared = args.shared_mut(); + for file in SPECIAL_FILES { + let path = Path::new(file); - if shared.paths.is_empty() { - for file in SPECIAL_FILES { - let path = PathBuf::from(file); - if path.exists() && path.is_file() { - shared.paths.push(path); - break; - } + if path.is_file() { + c.entries.push(Entry::Path(path.into())); + return Ok(()); + } + } + + let path = Path::new(rune::workspace::MANIFEST_FILE); + + if !path.is_file() { + return Err(anyhow!( + "Invalid usage: No input file nor project (`Rune.toml`) found" + )); + } + + // When building or running a workspace we need to be more verbose so that + // users understand what exactly happens. + c.verbose = true; + + let mut sources = rune::Sources::new(); + sources.insert(rune::Source::from_path(path)?); + + let mut diagnostics = rune::workspace::Diagnostics::new(); + + let result = rune::workspace::prepare(&mut sources) + .with_diagnostics(&mut diagnostics) + .build(); + + diagnostics.emit(io.stdout, &sources)?; + + let manifest = result?; + + if let Some(bin) = args.cmd.bins_test() { + for found in manifest.find_bins(bin)? { + let package = Package { + name: found.package.name.clone(), + }; + c.entries.push(Entry::PackagePath(package, found.path)); + } + } + + if let Some(test) = args.cmd.tests_test() { + for found in manifest.find_tests(test)? { + let package = Package { + name: found.package.name.clone(), + }; + c.entries.push(Entry::PackagePath(package, found.path)); + } + } + + if let Some(example) = args.cmd.examples_test() { + for found in manifest.find_examples(example)? { + let package = Package { + name: found.package.name.clone(), + }; + c.entries.push(Entry::PackagePath(package, found.path)); } + } - if shared.paths.is_empty() { - writeln!( - o, - "Invalid usage: No input path given and no main or lib file found" - )?; - return Ok(ExitCode::Failure); + if let Some(bench) = args.cmd.benches_test() { + for found in manifest.find_benches(bench)? { + let package = Package { + name: found.package.name.clone(), + }; + c.entries.push(Entry::PackagePath(package, found.path)); } } - let paths = loader::walk_paths(shared.recursive, std::mem::take(&mut shared.paths)); + Ok(()) +} + +async fn main_with_out(io: &mut Io<'_>, mut args: Args) -> Result { + let mut c = Config::default(); + args.cmd.propagate_related_flags(&mut c); + populate_config(io, &mut c, &args)?; + let entries = std::mem::take(&mut c.entries); let options = args.options()?; - for path in paths { - let path = path?; + let what = args.cmd.describe(); + let verbose = c.verbose; + let recursive = args.cmd.shared().recursive; + + for entry in entries { + let path = match entry { + Entry::Path(path) => path, + Entry::PackagePath(p, path) => { + if verbose { + let mut o = io.stderr.lock(); + o.set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true))?; + let result = write!(o, "{:>12}", what); + o.set_color(&ColorSpec::new())?; + result?; + writeln!(o, " `{}` (from {})", path.display(), p.name)?; + } + + path + } + }; + + for path in loader::recurse_paths(recursive, path) { + let path = path?; - match run_path(o, &args, &options, &path).await? { - ExitCode::Success => (), - other => { - return Ok(other); + match run_path(io, &c, &args, &options, &path).await? { + ExitCode::Success => (), + other => { + return Ok(other); + } } } } @@ -373,24 +568,25 @@ async fn main_with_out(o: &mut StandardStream, mut args: Args) -> Result, + c: &Config, args: &Args, options: &Options, path: &Path, ) -> Result { match &args.cmd { - Command::Check(flags) => check::run(o, flags, options, path), + Command::Check(flags) => check::run(io, c, flags, options, path), Command::Test(flags) => { - let io = rune_modules::capture_io::CaptureIo::new(); - let context = flags.shared.context_with_capture(&io)?; + let capture_io = rune_modules::capture_io::CaptureIo::new(); + let context = flags.shared.context_with_capture(c, &capture_io)?; - let load = loader::load(o, &context, args, options, path, visitor::Attribute::Test)?; + let load = loader::load(io, &context, args, options, path, visitor::Attribute::Test)?; tests::run( - o, + io, flags, &context, - Some(&io), + Some(&capture_io), load.unit, &load.sources, &load.functions, @@ -398,16 +594,16 @@ async fn run_path( .await } Command::Bench(flags) => { - let io = rune_modules::capture_io::CaptureIo::new(); - let context = flags.shared.context_with_capture(&io)?; + let capture_io = rune_modules::capture_io::CaptureIo::new(); + let context = flags.shared.context_with_capture(c, &capture_io)?; - let load = loader::load(o, &context, args, options, path, visitor::Attribute::Bench)?; + let load = loader::load(io, &context, args, options, path, visitor::Attribute::Bench)?; benches::run( - o, + io, flags, &context, - Some(&io), + Some(&capture_io), load.unit, &load.sources, &load.functions, @@ -415,11 +611,9 @@ async fn run_path( .await } Command::Run(flags) => { - let context = flags.shared.context()?; - - let load = loader::load(o, &context, args, options, path, visitor::Attribute::None)?; - - run::run(o, flags, &context, load.unit, &load.sources).await + let context = flags.shared.context(c)?; + let load = loader::load(io, &context, args, options, path, visitor::Attribute::None)?; + run::run(io, c, flags, &context, load.unit, &load.sources).await } } } diff --git a/crates/rune-cli/src/run.rs b/crates/rune-cli/src/run.rs index 0e696b252..78a683c44 100644 --- a/crates/rune-cli/src/run.rs +++ b/crates/rune-cli/src/run.rs @@ -1,7 +1,6 @@ -use crate::{ExitCode, SharedFlags}; +use crate::{Config, ExitCode, Io, SharedFlags}; use anyhow::Result; use rune::runtime::{VmError, VmExecution}; -use rune::termcolor::StandardStream; use rune::{Context, Sources, Unit, Value, Vm}; use std::io::Write; use std::sync::Arc; @@ -95,32 +94,33 @@ impl From for TraceError { } pub(crate) async fn run( - o: &mut StandardStream, + io: &mut Io<'_>, + c: &Config, args: &Flags, context: &Context, unit: Arc, sources: &Sources, ) -> Result { if args.dump_native_functions { - writeln!(o, "# functions")?; + writeln!(io.stdout, "# functions")?; for (i, (hash, f)) in context.iter_functions().enumerate() { - writeln!(o, "{:04} = {} ({})", i, f, hash)?; + writeln!(io.stdout, "{:04} = {} ({})", i, f, hash)?; } } if args.dump_native_types { - writeln!(o, "# types")?; + writeln!(io.stdout, "# types")?; for (i, (hash, ty)) in context.iter_types().enumerate() { - writeln!(o, "{:04} = {} ({})", i, ty, hash)?; + writeln!(io.stdout, "{:04} = {} ({})", i, ty, hash)?; } } if args.dump_unit() { if args.emit_instructions() { + let mut o = io.stdout.lock(); writeln!(o, "# instructions")?; - let mut o = o.lock(); unit.emit_instructions(&mut o, sources, args.with_source)?; } @@ -130,38 +130,38 @@ pub(crate) async fn run( let mut constants = unit.iter_constants().peekable(); if args.dump_functions && functions.peek().is_some() { - writeln!(o, "# dynamic functions")?; + writeln!(io.stdout, "# dynamic functions")?; for (hash, kind) in functions { if let Some(signature) = unit.debug_info().and_then(|d| d.functions.get(&hash)) { - writeln!(o, "{} = {}", hash, signature)?; + writeln!(io.stdout, "{} = {}", hash, signature)?; } else { - writeln!(o, "{} = {}", hash, kind)?; + writeln!(io.stdout, "{} = {}", hash, kind)?; } } } if strings.peek().is_some() { - writeln!(o, "# strings")?; + writeln!(io.stdout, "# strings")?; for string in strings { - writeln!(o, "{} = {:?}", string.hash(), string)?; + writeln!(io.stdout, "{} = {:?}", string.hash(), string)?; } } if args.dump_constants && constants.peek().is_some() { - writeln!(o, "# constants")?; + writeln!(io.stdout, "# constants")?; for constant in constants { - writeln!(o, "{} = {:?}", constant.0, constant.1)?; + writeln!(io.stdout, "{} = {:?}", constant.0, constant.1)?; } } if keys.peek().is_some() { - writeln!(o, "# object keys")?; + writeln!(io.stdout, "# object keys")?; for (hash, keys) in keys { - writeln!(o, "{} = {:?}", hash, keys)?; + writeln!(io.stdout, "{} = {:?}", hash, keys)?; } } } @@ -174,7 +174,7 @@ pub(crate) async fn run( let mut execution: VmExecution<_> = vm.execute(&["main"], ())?; let result = if args.trace { match do_trace( - o, + io, &mut execution, sources, args.dump_stack, @@ -195,18 +195,26 @@ pub(crate) async fn run( match result { Ok(result) => { let duration = Instant::now().duration_since(last); - writeln!(o, "== {:?} ({:?})", result, duration)?; + + if c.verbose { + writeln!(io.stderr, "== {:?} ({:?})", result, duration)?; + } + errored = None; } Err(error) => { let duration = Instant::now().duration_since(last); - writeln!(o, "== ! ({}) ({:?})", error, duration)?; + + if c.verbose { + writeln!(io.stderr, "== ! ({}) ({:?})", error, duration)?; + } + errored = Some(error); } }; if args.dump_stack { - writeln!(o, "# full stack dump after halting")?; + writeln!(io.stdout, "# full stack dump after halting")?; let vm = execution.vm(); @@ -225,33 +233,44 @@ pub(crate) async fn run( .get(frame.stack_bottom()..stack_top) .expect("bad stack slice"); - writeln!(o, " frame #{} (+{})", count, frame.stack_bottom())?; + writeln!(io.stdout, " frame #{} (+{})", count, frame.stack_bottom())?; if values.is_empty() { - writeln!(o, " *empty*")?; + writeln!(io.stdout, " *empty*")?; } for (n, value) in stack.iter().enumerate() { - writeln!(o, "{}+{} = {:?}", frame.stack_bottom(), n, value)?; + writeln!(io.stdout, "{}+{} = {:?}", frame.stack_bottom(), n, value)?; } } // NB: print final frame - writeln!(o, " frame #{} (+{})", frames.len(), stack.stack_bottom())?; + writeln!( + io.stdout, + " frame #{} (+{})", + frames.len(), + stack.stack_bottom() + )?; let values = stack.get(stack.stack_bottom()..).expect("bad stack slice"); if values.is_empty() { - writeln!(o, " *empty*")?; + writeln!(io.stdout, " *empty*")?; } for (n, value) in values.iter().enumerate() { - writeln!(o, " {}+{} = {:?}", stack.stack_bottom(), n, value)?; + writeln!( + io.stdout, + " {}+{} = {:?}", + stack.stack_bottom(), + n, + value + )?; } } if let Some(error) = errored { - error.emit(o, sources)?; + error.emit(io.stdout, sources)?; Ok(ExitCode::VmError) } else { Ok(ExitCode::Success) @@ -260,7 +279,7 @@ pub(crate) async fn run( /// Perform a detailed trace of the program. async fn do_trace( - o: &mut StandardStream, + io: &mut Io<'_>, execution: &mut VmExecution, sources: &Sources, dump_stack: bool, @@ -274,7 +293,7 @@ where loop { { let vm = execution.vm(); - let mut o = o.lock(); + let mut o = io.stdout.lock(); if let Some((hash, signature)) = vm.unit().debug_info().and_then(|d| d.function_at(vm.ip())) @@ -316,7 +335,7 @@ where Err(e) => return Err(TraceError::VmError(e)), }; - let mut o = o.lock(); + let mut o = io.stdout.lock(); if dump_stack { let vm = execution.vm(); diff --git a/crates/rune-cli/src/tests.rs b/crates/rune-cli/src/tests.rs index d608b517d..d30f7ddf0 100644 --- a/crates/rune-cli/src/tests.rs +++ b/crates/rune-cli/src/tests.rs @@ -1,8 +1,7 @@ -use crate::{ExitCode, SharedFlags}; +use crate::{ExitCode, Io, SharedFlags}; use anyhow::Result; use rune::compile::Meta; use rune::runtime::{Unit, Value, Vm, VmError}; -use rune::termcolor::StandardStream; use rune::{Context, Hash, Sources}; use rune_modules::capture_io::CaptureIo; use std::io::Write; @@ -51,13 +50,13 @@ impl<'a> TestCase<'a> { async fn execute( &mut self, - o: &mut StandardStream, + io: &mut Io<'_>, vm: &mut Vm, quiet: bool, - io: Option<&CaptureIo>, + capture_io: Option<&CaptureIo>, ) -> Result { if !quiet { - write!(o, "Test {:30} ", self.meta.item.item)?; + write!(io.stdout, "Test {:30} ", self.meta.item.item)?; } let result = match vm.execute(self.hash, ()) { @@ -65,8 +64,8 @@ impl<'a> TestCase<'a> { Ok(mut execution) => execution.async_complete().await, }; - if let Some(io) = io { - let _ = io.drain_into(&mut self.buf); + if let Some(capture_io) = capture_io { + let _ = capture_io.drain_into(&mut self.buf); } self.outcome = match result { @@ -90,31 +89,31 @@ impl<'a> TestCase<'a> { if quiet { match &self.outcome { Some(FailureReason::Crash { .. }) => { - write!(o, "F")?; + write!(io.stdout, "F")?; } Some(FailureReason::ReturnedErr { .. }) => { - write!(o, "f")?; + write!(io.stdout, "f")?; } Some(FailureReason::ReturnedNone { .. }) => { - write!(o, "n")?; + write!(io.stdout, "n")?; } None => { - write!(o, ".")?; + write!(io.stdout, ".")?; } } } else { match &self.outcome { Some(FailureReason::Crash { .. }) => { - writeln!(o, "failed")?; + writeln!(io.stdout, "failed")?; } Some(FailureReason::ReturnedErr { .. }) => { - writeln!(o, "returned error")?; + writeln!(io.stdout, "returned error")?; } Some(FailureReason::ReturnedNone { .. }) => { - writeln!(o, "returned none")?; + writeln!(io.stdout, "returned none")?; } None => { - writeln!(o, "passed")?; + writeln!(io.stdout, "passed")?; } } } @@ -123,22 +122,22 @@ impl<'a> TestCase<'a> { Ok(self.outcome.is_none()) } - fn emit(&self, o: &mut StandardStream, sources: &Sources) -> Result<()> { + fn emit(&self, io: &mut Io<'_>, sources: &Sources) -> Result<()> { if let Some(outcome) = &self.outcome { match outcome { FailureReason::Crash(err) => { - writeln!(o, "----------------------------------------")?; - writeln!(o, "Test: {}\n", self.meta.item.item)?; - err.emit(o, sources)?; + writeln!(io.stdout, "----------------------------------------")?; + writeln!(io.stdout, "Test: {}\n", self.meta.item.item)?; + err.emit(io.stdout, sources)?; } FailureReason::ReturnedNone { .. } => {} FailureReason::ReturnedErr { output, error, .. } => { - writeln!(o, "----------------------------------------")?; - writeln!(o, "Test: {}\n", self.meta.item.item)?; - writeln!(o, "Error: {:?}\n", error)?; - writeln!(o, "-- output --")?; - o.write_all(output)?; - writeln!(o, "-- end of output --")?; + writeln!(io.stdout, "----------------------------------------")?; + writeln!(io.stdout, "Test: {}\n", self.meta.item.item)?; + writeln!(io.stdout, "Error: {:?}\n", error)?; + writeln!(io.stdout, "-- output --")?; + io.stdout.write_all(output)?; + writeln!(io.stdout, "-- end of output --")?; } } } @@ -148,10 +147,10 @@ impl<'a> TestCase<'a> { } pub(crate) async fn run( - o: &mut StandardStream, + io: &mut Io<'_>, flags: &Flags, context: &Context, - io: Option<&CaptureIo>, + capture_io: Option<&CaptureIo>, unit: Arc, sources: &Sources, fns: &[(Hash, Meta)], @@ -163,7 +162,7 @@ pub(crate) async fn run( .map(|v| TestCase::from_parts(v.0, &v.1)) .collect::>(); - writeln!(o, "Found {} tests...", cases.len())?; + writeln!(io.stdout, "Found {} tests...", cases.len())?; let start = Instant::now(); let mut failure_count = 0; @@ -174,7 +173,7 @@ pub(crate) async fn run( for test in &mut cases { executed_count += 1; - let success = test.execute(o, &mut vm, flags.quiet, io).await?; + let success = test.execute(io, &mut vm, flags.quiet, capture_io).await?; if !success { failure_count += 1; @@ -186,18 +185,18 @@ pub(crate) async fn run( } if flags.quiet { - writeln!(o)?; + writeln!(io.stdout)?; } let elapsed = start.elapsed(); for case in &cases { - case.emit(o, sources)?; + case.emit(io, sources)?; } - writeln!(o, "====")?; + writeln!(io.stdout, "====")?; writeln!( - o, + io.stdout, "Executed {} tests with {} failures ({} skipped) in {:.3} seconds", executed_count, failure_count, diff --git a/crates/rune/Cargo.toml b/crates/rune/Cargo.toml index bf4d6ff7f..18dc5c77f 100644 --- a/crates/rune/Cargo.toml +++ b/crates/rune/Cargo.toml @@ -18,6 +18,7 @@ An embeddable dynamic programming language for Rust. default = ["emit"] emit = ["codespan-reporting"] bench = [] +workspace = ["toml", "toml-spanned-value", "semver", "relative-path", "serde-hashkey"] [dependencies] thiserror = "1.0.30" @@ -38,6 +39,11 @@ futures-util = "0.3.18" anyhow = "1.0.49" twox-hash = { version = "1.6.1", default-features = false } num-bigint = "0.4.3" +toml = { version = "0.5.8", optional = true } +toml-spanned-value = { version = "0.1.0", optional = true } +semver = { version = "1.0.4", optional = true, features = ["serde"] } +relative-path = { version = "1.6.0", optional = true, features = ["serde"] } +serde-hashkey = { version = "0.4.0", optional = true } rune-macros = {version = "0.10.0", path = "../rune-macros"} diff --git a/crates/rune/src/internal_macros.rs b/crates/rune/src/internal_macros.rs index 834451925..ed18c0cf1 100644 --- a/crates/rune/src/internal_macros.rs +++ b/crates/rune/src/internal_macros.rs @@ -33,7 +33,7 @@ macro_rules! error { /// broken for some reason. pub fn msg(spanned: S, message: &'static str) -> Self where - S: Spanned, + S: $crate::ast::Spanned, { Self::new(spanned, $kind::Custom { message }) } @@ -69,6 +69,8 @@ macro_rules! error { impl From<$crate::shared::Custom> for $error_ty { fn from(error: $crate::shared::Custom) -> Self { + use $crate::ast::Spanned; + Self::new( error.span(), $kind::Custom { @@ -163,3 +165,13 @@ macro_rules! cfg_emit { )* } } + +macro_rules! cfg_workspace { + ($($item:item)*) => { + $( + #[cfg(feature = "workspace")] + #[cfg_attr(docsrs, doc(cfg(feature = "workspace")))] + $item + )* + } +} diff --git a/crates/rune/src/lib.rs b/crates/rune/src/lib.rs index d4484c754..157f84713 100644 --- a/crates/rune/src/lib.rs +++ b/crates/rune/src/lib.rs @@ -252,6 +252,10 @@ pub use self::sources::Sources; mod worker; +cfg_workspace! { + pub mod workspace; +} + #[doc(hidden)] pub mod testing; diff --git a/crates/rune/src/source.rs b/crates/rune/src/source.rs index d951d268d..e11664be2 100644 --- a/crates/rune/src/source.rs +++ b/crates/rune/src/source.rs @@ -29,8 +29,6 @@ impl Source { pub fn from_path(path: &Path) -> io::Result { let name = path.display().to_string(); let source = fs::read_to_string(&path)?; - let path = path.canonicalize()?; - Ok(Self::with_path(name, source, Some(path))) } diff --git a/crates/rune/src/workspace/build.rs b/crates/rune/src/workspace/build.rs new file mode 100644 index 000000000..ab3583c7f --- /dev/null +++ b/crates/rune/src/workspace/build.rs @@ -0,0 +1,67 @@ +use crate::Sources; +use crate::workspace::Diagnostics; +use thiserror::Error; +use crate::workspace::manifest::{self, Loader, Manifest}; + +/// Failed to build workspace. +#[derive(Debug, Error)] +#[error("failed to load workspace")] +pub struct BuildError; + +/// Prepare a workspace build. +pub fn prepare(sources: &mut Sources) -> Build<'_> { + Build { + sources, + diagnostics: None, + } +} + +/// A prepared build. +pub struct Build<'a> { + sources: &'a mut Sources, + diagnostics: Option<&'a mut Diagnostics>, +} + +impl<'a> Build<'a> { + /// Associate a specific diagnostic with the build. + pub fn with_diagnostics(self, diagnostics: &'a mut Diagnostics) -> Self { + Self { + diagnostics: Some(diagnostics), + ..self + } + } + + /// Perform the build. + pub fn build(self) -> Result { + let mut diagnostics; + + let diagnostics = match self.diagnostics { + Some(diagnostics) => diagnostics, + None => { + diagnostics = Diagnostics::new(); + &mut diagnostics + } + }; + + let mut manifest = Manifest { + packages: Vec::new(), + }; + + for id in self.sources.source_ids() { + let mut l = Loader { + id, + sources: self.sources, + diagnostics, + manifest: &mut manifest, + }; + + manifest::load_manifest(&mut l); + } + + if diagnostics.has_errors() { + return Err(BuildError); + } + + Ok(manifest) + } +} diff --git a/crates/rune/src/workspace/diagnostics.rs b/crates/rune/src/workspace/diagnostics.rs new file mode 100644 index 000000000..6e5c1ea1c --- /dev/null +++ b/crates/rune/src/workspace/diagnostics.rs @@ -0,0 +1,47 @@ +use crate::{SourceId}; +use crate::workspace::WorkspaceError; + +/// A reported diagnostic error. +pub(crate) struct FatalDiagnostic { + pub(crate) source_id: SourceId, + pub(crate) error: WorkspaceError, +} + +/// A single workspace diagnostic. +pub(crate) enum Diagnostic { + /// An error in a workspace. + Fatal(FatalDiagnostic), +} + +/// Diagnostics emitted about a workspace. +#[derive(Default)] +pub struct Diagnostics { + pub(crate) diagnostics: Vec, +} + +impl Diagnostics { + /// Report a single workspace error. + pub fn fatal(&mut self, source_id: SourceId, error: WorkspaceError) { + self.diagnostics.push(Diagnostic::Fatal(FatalDiagnostic { + source_id, + error, + })) + } + + /// Test if diagnostics has errors. + pub fn has_errors(&self) -> bool { + self.diagnostics.iter().any(|e| matches!(e, Diagnostic::Fatal(..))) + } + + /// Test if diagnostics is empty. + pub fn is_empty(&self) -> bool { + self.diagnostics.is_empty() + } +} + +impl Diagnostics { + /// Construct an empty diagnostics container. + pub fn new() -> Self { + Self::default() + } +} diff --git a/crates/rune/src/workspace/emit.rs b/crates/rune/src/workspace/emit.rs new file mode 100644 index 000000000..6c0b4a562 --- /dev/null +++ b/crates/rune/src/workspace/emit.rs @@ -0,0 +1,80 @@ +//! Runtime helpers for loading code and emitting diagnostics. + +use crate::{Sources}; +use crate::ast::{Spanned}; +use std::fmt; +use std::io; +use thiserror::Error; +use codespan_reporting::diagnostic as d; +use codespan_reporting::term; +use codespan_reporting::term::termcolor::WriteColor; +pub use codespan_reporting::term::termcolor; +use crate::workspace::{Diagnostics, Diagnostic, FatalDiagnostic}; + +/// Errors that can be raised when formatting diagnostics. +#[derive(Debug, Error)] +pub enum EmitError { + /// Source Error. + #[error("I/O error")] + Io(#[from] io::Error), + /// Source Error. + #[error("formatting error")] + Fmt(#[from] fmt::Error), + /// Codespan reporting error. + #[error("codespan reporting error")] + CodespanReporting(#[from] codespan_reporting::files::Error), +} + +impl Diagnostics { + /// Generate formatted diagnostics capable of referencing source lines and + /// hints. + /// + /// See [prepare][crate::prepare] for how to use. + pub fn emit( + &self, + out: &mut O, + sources: &Sources, + ) -> Result<(), EmitError> + where + O: WriteColor, + { + if self.is_empty() { + return Ok(()); + } + + let config = codespan_reporting::term::Config::default(); + + for diagnostic in &self.diagnostics { + match diagnostic { + Diagnostic::Fatal(e) => { + error_diagnostics_emit(e, out, sources, &config)?; + } + } + } + + Ok(()) + } +} + +/// Custom shared helper for emitting diagnostics for a single error. +fn error_diagnostics_emit( + this: &FatalDiagnostic, + out: &mut O, + sources: &Sources, + config: &codespan_reporting::term::Config, +) -> Result<(), EmitError> +where + O: WriteColor, +{ + let mut labels = Vec::new(); + + let span = this.error.span(); + labels.push(d::Label::primary(this.source_id, span.range()).with_message(this.error.to_string())); + + let diagnostic = d::Diagnostic::error() + .with_message(this.error.to_string()) + .with_labels(labels); + + term::emit(out, config, sources, &diagnostic)?; + Ok(()) +} diff --git a/crates/rune/src/workspace/error.rs b/crates/rune/src/workspace/error.rs new file mode 100644 index 000000000..351230626 --- /dev/null +++ b/crates/rune/src/workspace/error.rs @@ -0,0 +1,49 @@ +use std::path::Path; +use thiserror::Error; +use crate::{SourceId}; +use crate::ast::Span; + +error! { + /// An error raised when interacting with workspaces. + #[derive(Debug)] + pub struct WorkspaceError { + kind: WorkspaceErrorKind, + } +} + +impl WorkspaceError { + pub(crate) fn missing_field(span: Span, field: &'static str) -> Self { + Self::new(span, WorkspaceErrorKind::MissingField { field }) + } + + pub(crate) fn expected_array(span: Span) -> Self { + Self::new(span, WorkspaceErrorKind::ExpectedArray) + } +} + +/// A workspace error. +#[derive(Debug, Error)] +#[allow(missing_docs)] +#[non_exhaustive] +pub enum WorkspaceErrorKind { + #[error("manifest deserialization: {error}")] + Toml { #[from] #[source] error: toml::de::Error }, + #[error("manifest serializationo: {error}")] + Key { #[from] #[source] error: serde_hashkey::Error }, + #[error("failed to read `{path}`: {error}")] + SourceError { path: Box, error: std::io::Error }, + #[error("custom: {message}")] + Custom { message: &'static str }, + #[error("missing source id `{source_id}`")] + MissingSourceId { source_id: SourceId }, + #[error("missing required field `{field}`")] + MissingField { field: &'static str }, + #[error("expected array")] + ExpectedArray, + #[error("[workspace] elements can only be used in manifests with a valid path")] + MissingManifestPath, + #[error("expected table")] + ExpectedTable, + #[error("key not supported")] + UnsupportedKey, +} diff --git a/crates/rune/src/workspace/manifest.rs b/crates/rune/src/workspace/manifest.rs new file mode 100644 index 000000000..cf8381ee2 --- /dev/null +++ b/crates/rune/src/workspace/manifest.rs @@ -0,0 +1,423 @@ +use std::collections::VecDeque; +use std::mem; +use std::path::{PathBuf, Path}; +use relative_path::{RelativePathBuf, RelativePath, Component}; +use semver::Version; +use serde::de::{IntoDeserializer}; +use toml_spanned_value::spanned_value::{ValueKind, Table, Array}; +use crate::{Sources, SourceId, Source}; +use crate::ast::{Span, Spanned}; +use crate::workspace::{MANIFEST_FILE, WorkspaceErrorKind, Diagnostics, WorkspaceError}; +use toml_spanned_value::SpannedValue; +use serde::Deserialize; +use serde_hashkey as key; +use std::io; +use std::fs; +use std::ffi::OsStr; + +/// A workspace filter which in combination with functions such as +/// [Manifest::find_bins] can be used to selectively find things in the +/// workspace. +#[derive(Debug, Clone, Copy)] +#[non_exhaustive] +pub enum WorkspaceFilter<'a> { + /// Look for one specific named thing. + Name(&'a str), + /// Look for all things. + All, +} + +/// A found item in the workspace. +#[derive(Debug)] +#[non_exhaustive] +pub struct Found<'a> { + /// A found path that can be built. + pub path: Box, + /// The package the found path belongs to. + pub package: &'a Package, +} + +impl WorkspaceFilter<'_> { + fn matches(self, name: &str) -> bool { + match self { + WorkspaceFilter::Name(expected) => name == expected, + WorkspaceFilter::All => true, + } + } +} + +impl Spanned for toml::Spanned { + fn span(&self) -> Span { + let (start, end) = toml::Spanned::span(self); + Span::new(start, end) + } +} + +/// The manifest of a workspace. +#[derive(Debug)] +#[non_exhaustive] +pub struct Manifest { + /// List of packages found. + pub packages: Vec, +} + +impl Manifest { + fn find_paths(&self, m: WorkspaceFilter<'_>, auto_path: &Path, auto_find: impl Fn(&Package) -> bool) -> io::Result>> { + let mut output = Vec::new(); + + for package in &self.packages { + if let (Some(path), true) = (&package.root, auto_find(package)) { + let path = path.join(auto_path); + let results = find_rune_files(&path)?; + + for result in results { + let (base, path) = result?; + + if m.matches(&*base) { + output.push(Found { path, package }); + } + } + } + } + + Ok(output) + } + + /// Find all binaries matching the given name in the workspace. + pub fn find_bins(&self, m: WorkspaceFilter<'_>) -> io::Result>> { + self.find_paths(m, Path::new("bin"), |p| p.auto_bins) + } + + /// Find all tests associated with the given base name. + pub fn find_tests(&self, m: WorkspaceFilter<'_>) -> io::Result>> { + self.find_paths(m, Path::new("tests"), |p| p.auto_tests) + } + + /// Find all examples matching the given name in the workspace. + pub fn find_examples(&self, m: WorkspaceFilter<'_>) -> io::Result>> { + self.find_paths(m, Path::new("examples"), |p| p.auto_examples) + } + + /// Find all benches matching the given name in the workspace. + pub fn find_benches(&self, m: WorkspaceFilter<'_>) -> io::Result>> { + self.find_paths(m, Path::new("benches"), |p| p.auto_benches) + } +} + +/// A single package. +#[derive(Debug)] +#[non_exhaustive] +pub struct Package { + /// The name of the package. + pub name: Box, + /// The version of the package.. + pub version: Version, + /// The root of the package. + pub root: Option>, + /// Automatically detect binaries. + pub auto_bins: bool, + /// Automatically detect tests. + pub auto_tests: bool, + /// Automatically detect examples. + pub auto_examples: bool, + /// Automatically detect benches. + pub auto_benches: bool, +} + +pub(crate) struct Loader<'a> { + pub(crate) id: SourceId, + pub(crate) sources: &'a mut Sources, + pub(crate) diagnostics: &'a mut Diagnostics, + pub(crate) manifest: &'a mut Manifest, +} + +/// Load a manifest. +pub(crate) fn load_manifest(l: &mut Loader<'_>) { + let (value, root) = match l.sources.get(l.id) { + Some(source) => { + let root: Option> = source.path().and_then(|p| p.parent()).map(|p| p.into()); + + let value: SpannedValue = match toml::from_str(source.as_str()) { + Ok(value) => value, + Err(e) => { + let span = Span::new(0, source.len()); + l.diagnostics.fatal(l.id, WorkspaceError::new(span, e)); + return; + } + }; + + (value, root) + } + None => { + l.diagnostics.fatal(l.id, WorkspaceError::new(Span::empty(), WorkspaceErrorKind::MissingSourceId { source_id: l.id })); + return; + } + }; + + if let Some((mut table, _)) = into_table(l, value) { + // If manifest is a package, add it here. + if let Some(package) = table.remove("package") { + if let Some((mut package, span)) = into_table(l, package) { + if let Some(package) = load_package(l, &mut package, span, root.as_deref()) { + l.manifest.packages.push(package); + } + + ensure_empty(l, package); + } + } + + // Load the [workspace] section. + if let Some(workspace) = table.remove("workspace") { + if let Some((mut table, span)) = into_table(l, workspace) { + match &root { + Some(root) => { + if let Some(members) = load_members(l, &mut table, root) { + for (span, path) in members { + load_member(l, span, &path); + } + } + }, + None => { + l.diagnostics.fatal(l.id, WorkspaceError::new(span, WorkspaceErrorKind::MissingManifestPath)); + } + } + + ensure_empty(l, table); + } + } + + ensure_empty(l, table); + } +} + +/// Load members from the given workspace configuration. +fn load_members(l: &mut Loader<'_>, table: &mut Table, root: &Path) -> Option> { + let members = match table.remove("members") { + Some(members) => members, + None => return None, + }; + + let (members, _) = into_array(l, members)?; + let mut output = Vec::new(); + + for value in members { + let span = Spanned::span(&value); + + match deserialize::(value) { + Ok(member) => { + glob_relative_path(l, &mut output, span, &member, root); + } + Err(error) => { + l.diagnostics.fatal(l.id, error); + } + }; + } + + Some(output) +} + +/// Glob a relative path. +/// +/// Currently only supports expanding `*` and required interacting with the +/// filesystem. +fn glob_relative_path(l: &mut Loader<'_>, output: &mut Vec<(Span, PathBuf)>, span: Span, member: &RelativePath, root: &Path) { + let mut queue = VecDeque::new(); + queue.push_back((root.to_owned(), member.components())); + + while let Some((mut path, mut it)) = queue.pop_front() { + loop { + let c = match it.next() { + Some(c) => c, + None => { + path.push(MANIFEST_FILE); + output.push((span, path)); + break; + } + }; + + match c { + Component::CurDir => {}, + Component::ParentDir => { + path.push(".."); + }, + Component::Normal("*") => { + let result = match source_error(l, span, &path, fs::read_dir(&path)) { + Some(result) => result, + None => continue, + }; + + for e in result { + let e = match source_error(l, span, &path, e) { + Some(e) => e, + None => continue, + }; + + let path = e.path(); + + let m = match source_error(l, span, &path, e.metadata()) { + Some(m) => m, + None => continue, + }; + + if m.is_dir() { + queue.push_back((path, it.clone())); + } + } + + break; + }, + Component::Normal(normal) => { + path.push(normal); + }, + } + } + } +} + +/// Helper to convert an [io::Error] into a [WorkspaceErrorKind::SourceError]. +fn source_error(l: &mut Loader<'_>, span: Span, path: &Path, result: io::Result) -> Option { + match result { + Ok(result) => Some(result), + Err(error) => { + l.diagnostics.fatal(l.id, WorkspaceError::new(span, WorkspaceErrorKind::SourceError { + path: path.into(), + error, + })); + + None + } + } +} + +/// Try to load the given path as a member in the current manifest. +fn load_member(l: &mut Loader<'_>, span: Span, path: &Path) { + let source = match source_error(l, span, path, Source::from_path(path)) { + Some(source) => source, + None => return, + }; + + let id = l.sources.insert(source); + let old = mem::replace(&mut l.id, id); + load_manifest(l); + l.id = old; +} + +/// Load a package from a value. +fn load_package(l: &mut Loader<'_>, table: &mut Table, span: Span, root: Option<&Path>) -> Option { + let name = field(l, table, span, "name"); + let version = field(l, table, span, "version"); + + Some(Package { + name: name?, + version: version?, + root: root.map(|p| p.into()), + auto_bins: true, + auto_tests: true, + auto_examples: true, + auto_benches: true, + }) +} + +/// Ensure that a table is empty and mark any additional elements as erroneous. +fn ensure_empty(l: &mut Loader<'_>, table: Table) { + for (key, _) in table { + let span = Spanned::span(&key); + l.diagnostics.fatal(l.id, WorkspaceError::new(span, WorkspaceErrorKind::UnsupportedKey)); + } +} + +/// Ensure that value is a table. +fn into_table(l: &mut Loader<'_>, value: SpannedValue) -> Option<(Table, Span)> { + let span = Spanned::span(&value); + + match value.into_inner() { + ValueKind::Table(table) => Some((table, span)), + _ => { + let error = WorkspaceError::new(span, WorkspaceErrorKind::ExpectedTable); + l.diagnostics.fatal(l.id, error); + None + } + } +} + +/// Coerce into an array or error. +fn into_array(l: &mut Loader<'_>, value: SpannedValue) -> Option<(Array, Span)> { + let span = Spanned::span(&value); + + match value.into_inner() { + ValueKind::Array(array) => Some((array, span)), + _ => { + let error = WorkspaceError::expected_array(span); + l.diagnostics.fatal(l.id, error); + None + } + } +} + +/// Helper to load a single field. +fn field(l: &mut Loader<'_>, table: &mut Table, span: Span, field: &'static str) -> Option where T: for<'de> Deserialize<'de> { + match table.remove(field) { + Some(value) => { + match deserialize(value) { + Ok(value) => Some(value), + Err(error) => { + l.diagnostics.fatal(l.id, error); + None + } + } + }, + None => { + let error = WorkspaceError::missing_field(span, field); + l.diagnostics.fatal(l.id, error); + None + } + } +} + +/// Helper to load a single field. +fn deserialize(value: SpannedValue) -> Result where T: for<'de> Deserialize<'de> { + let span = Spanned::span(&value); + let f = key::to_key(value.get_ref()).map_err(|e| WorkspaceError::new(span, e))?; + let deserializer = f.into_deserializer(); + let value = T::deserialize(deserializer).map_err(|e| WorkspaceError::new(span, e))?; + Ok(value) +} + +/// Find all rune files in the given path. +fn find_rune_files(path: &Path) -> io::Result, Box)>>> { + let mut dir = match fs::read_dir(path) { + Ok(dir) => Some(dir), + Err(e) if e.kind() == io::ErrorKind::NotFound => None, + Err(e) => return Err(e), + }; + + Ok(std::iter::from_fn(move || { + loop { + let e = dir.as_mut()?.next()?; + + let e = match e { + Ok(e) => e, + Err(err) => return Some(Err(err)), + }; + + let m = match e.metadata() { + Ok(m) => m, + Err(err) => return Some(Err(err)), + }; + + if !m.is_file() { + continue; + } + + let path = e.path(); + + if let (Some(base), Some(ext)) = (path.file_stem(), path.extension()) { + if ext == OsStr::new("rn") { + if let Some(base) = base.to_str() { + return Some(Ok((base.into(), path.into()))); + } + } + } + } + })) +} diff --git a/crates/rune/src/workspace/mod.rs b/crates/rune/src/workspace/mod.rs new file mode 100644 index 000000000..a325cbf38 --- /dev/null +++ b/crates/rune/src/workspace/mod.rs @@ -0,0 +1,24 @@ +//! Types for dealing with workspaces of rune code. + +/// The name of the toplevel manifest `Rune.toml`. +pub const MANIFEST_FILE: &str = "Rune.toml"; + +mod build; +pub use self::build::{prepare, Build, BuildError}; + +cfg_emit! { + mod emit; + #[doc(inline)] + pub use self::emit::EmitError; +} + +mod error; + +pub use self::error::{WorkspaceErrorKind, WorkspaceError}; + +mod manifest; +pub use self::manifest::{Manifest, WorkspaceFilter}; + +mod diagnostics; +pub use self::diagnostics::{Diagnostics}; +pub(crate) use self::diagnostics::{Diagnostic, FatalDiagnostic}; diff --git a/examples/Rune.toml b/examples/Rune.toml new file mode 100644 index 000000000..a23fc5f91 --- /dev/null +++ b/examples/Rune.toml @@ -0,0 +1,3 @@ +[package] +name = "rune-examples" +version = "0.0.0" \ No newline at end of file diff --git a/examples/examples/test.rn b/examples/examples/test.rn new file mode 100644 index 000000000..056fcd406 --- /dev/null +++ b/examples/examples/test.rn @@ -0,0 +1,3 @@ +pub fn main() { + println!("A project"); +}