rune/workspace/
manifest.rs

1use std::ffi::OsStr;
2use std::fmt;
3use std::fs;
4use std::io;
5use std::iter;
6use std::path::{Path, PathBuf};
7
8use anyhow::Result;
9use relative_path::{RelativePath, RelativePathBuf};
10use semver::Version;
11use serde::de::IntoDeserializer;
12use serde::Deserialize;
13use serde_hashkey as key;
14
15use crate as rune;
16use crate::alloc::prelude::*;
17use crate::alloc::{self, String, Vec};
18use crate::ast::{Span, Spanned};
19use crate::workspace::spanned_value::{Array, SpannedValue, Table, Value};
20use crate::workspace::{
21    glob, Diagnostics, SourceLoader, WorkspaceError, WorkspaceErrorKind, MANIFEST_FILE,
22};
23use crate::{SourceId, Sources};
24
25const BIN: &str = "bin";
26const TESTS: &str = "tests";
27const EXAMPLES: &str = "examples";
28const BENCHES: &str = "benches";
29
30/// A workspace filter which in combination with functions such as
31/// [Manifest::find_bins] can be used to selectively find things in the
32/// workspace.
33#[derive(Debug, Clone, Copy)]
34#[non_exhaustive]
35pub enum WorkspaceFilter<'a> {
36    /// Look for one specific named thing.
37    Name(&'a str),
38    /// Look for all things.
39    All,
40}
41
42/// The kind of a found entry.
43#[derive(Debug, Clone, Copy)]
44#[non_exhaustive]
45pub enum FoundKind {
46    /// The found entry is a binary.
47    Binary,
48    /// The found entry is a test.
49    Test,
50    /// The found entry is an example.
51    Example,
52    /// The found entry is a benchmark.
53    Bench,
54}
55
56impl fmt::Display for FoundKind {
57    #[inline]
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        match self {
60            FoundKind::Binary => "bin".fmt(f),
61            FoundKind::Test => "test".fmt(f),
62            FoundKind::Example => "example".fmt(f),
63            FoundKind::Bench => "bench".fmt(f),
64        }
65    }
66}
67
68/// A found item in the workspace.
69#[derive(Debug, TryClone)]
70#[non_exhaustive]
71pub struct Found {
72    /// The kind found.
73    #[try_clone(copy)]
74    pub kind: FoundKind,
75    /// A found path that can be built.
76    pub path: PathBuf,
77    /// Name of the found thing.
78    pub name: String,
79}
80
81/// A found item in the workspace associated with a package.
82#[derive(Debug, TryClone)]
83#[non_exhaustive]
84pub struct FoundPackage<'a> {
85    /// A found path that can be built.
86    pub found: Found,
87    /// Index of the package build belongs to.
88    pub package: &'a Package,
89}
90
91impl WorkspaceFilter<'_> {
92    fn matches(self, name: &str) -> bool {
93        match self {
94            WorkspaceFilter::Name(expected) => name == expected,
95            WorkspaceFilter::All => true,
96        }
97    }
98}
99
100impl<T> Spanned for toml::Spanned<T> {
101    #[inline]
102    fn span(&self) -> Span {
103        let range = toml::Spanned::span(self);
104        Span::new(range.start, range.end)
105    }
106}
107
108/// The manifest of a workspace.
109#[derive(Default, Debug)]
110#[non_exhaustive]
111pub struct Manifest {
112    /// List of packages found.
113    pub packages: Vec<Package>,
114}
115
116impl Manifest {
117    fn find_paths<'m>(
118        &'m self,
119        m: WorkspaceFilter<'_>,
120        kind: FoundKind,
121        auto_path: &Path,
122        auto_find: fn(&Package) -> bool,
123    ) -> Result<Vec<FoundPackage<'m>>> {
124        let mut output = Vec::new();
125
126        for package in self.packages.iter() {
127            for found in package.find_paths(m, kind, auto_path, auto_find)? {
128                output.try_push(FoundPackage { found, package })?;
129            }
130        }
131
132        Ok(output)
133    }
134
135    /// Find every single entrypoint available.
136    pub fn find_all(&self, m: WorkspaceFilter<'_>) -> Result<Vec<FoundPackage<'_>>> {
137        let mut output = Vec::new();
138        output.try_extend(self.find_bins(m)?)?;
139        output.try_extend(self.find_tests(m)?)?;
140        output.try_extend(self.find_examples(m)?)?;
141        output.try_extend(self.find_benches(m)?)?;
142        Ok(output)
143    }
144
145    /// Find all binaries matching the given name in the workspace.
146    pub fn find_bins(&self, m: WorkspaceFilter<'_>) -> Result<Vec<FoundPackage<'_>>> {
147        self.find_paths(m, FoundKind::Binary, Path::new(BIN), |p| p.auto_bins)
148    }
149
150    /// Find all tests associated with the given base name.
151    pub fn find_tests(&self, m: WorkspaceFilter<'_>) -> Result<Vec<FoundPackage<'_>>> {
152        self.find_paths(m, FoundKind::Test, Path::new(TESTS), |p| p.auto_tests)
153    }
154
155    /// Find all examples matching the given name in the workspace.
156    pub fn find_examples(&self, m: WorkspaceFilter<'_>) -> Result<Vec<FoundPackage<'_>>> {
157        self.find_paths(m, FoundKind::Example, Path::new(EXAMPLES), |p| {
158            p.auto_examples
159        })
160    }
161
162    /// Find all benches matching the given name in the workspace.
163    pub fn find_benches(&self, m: WorkspaceFilter<'_>) -> Result<Vec<FoundPackage<'_>>> {
164        self.find_paths(m, FoundKind::Bench, Path::new(BENCHES), |p| p.auto_benches)
165    }
166}
167
168/// A single package.
169#[derive(Debug)]
170#[non_exhaustive]
171pub struct Package {
172    /// The name of the package.
173    pub name: String,
174    /// The version of the package..
175    pub version: Version,
176    /// The root of the package.
177    pub root: Option<PathBuf>,
178    /// Automatically detect binaries.
179    pub auto_bins: bool,
180    /// Automatically detect tests.
181    pub auto_tests: bool,
182    /// Automatically detect examples.
183    pub auto_examples: bool,
184    /// Automatically detect benches.
185    pub auto_benches: bool,
186}
187
188impl Package {
189    fn find_paths(
190        &self,
191        m: WorkspaceFilter<'_>,
192        kind: FoundKind,
193        auto_path: &Path,
194        auto_find: fn(&Package) -> bool,
195    ) -> Result<Vec<Found>> {
196        let mut output = Vec::new();
197
198        if let (Some(path), true) = (&self.root, auto_find(self)) {
199            let path = path.join(auto_path);
200            let results = find_rune_files(&path)?;
201
202            for result in results {
203                let (path, name) = result?;
204
205                if m.matches(&name) {
206                    output.try_push(Found { kind, path, name })?;
207                }
208            }
209        }
210
211        Ok(output)
212    }
213
214    /// Find every single entrypoint available.
215    pub fn find_all(&self, m: WorkspaceFilter<'_>) -> Result<Vec<Found>> {
216        let mut output = Vec::new();
217        output.try_extend(self.find_bins(m)?)?;
218        output.try_extend(self.find_tests(m)?)?;
219        output.try_extend(self.find_examples(m)?)?;
220        output.try_extend(self.find_benches(m)?)?;
221        Ok(output)
222    }
223
224    /// Find all binaries matching the given name in the workspace.
225    pub fn find_bins(&self, m: WorkspaceFilter<'_>) -> Result<Vec<Found>> {
226        self.find_paths(m, FoundKind::Binary, Path::new(BIN), |p| p.auto_bins)
227    }
228
229    /// Find all tests associated with the given base name.
230    pub fn find_tests(&self, m: WorkspaceFilter<'_>) -> Result<Vec<Found>> {
231        self.find_paths(m, FoundKind::Test, Path::new(TESTS), |p| p.auto_tests)
232    }
233
234    /// Find all examples matching the given name in the workspace.
235    pub fn find_examples(&self, m: WorkspaceFilter<'_>) -> Result<Vec<Found>> {
236        self.find_paths(m, FoundKind::Example, Path::new(EXAMPLES), |p| {
237            p.auto_examples
238        })
239    }
240
241    /// Find all benches matching the given name in the workspace.
242    pub fn find_benches(&self, m: WorkspaceFilter<'_>) -> Result<Vec<Found>> {
243        self.find_paths(m, FoundKind::Bench, Path::new(BENCHES), |p| p.auto_benches)
244    }
245}
246
247pub(crate) struct Loader<'a> {
248    id: SourceId,
249    sources: &'a mut Sources,
250    diagnostics: &'a mut Diagnostics,
251    source_loader: &'a mut dyn SourceLoader,
252    manifest: &'a mut Manifest,
253}
254
255impl<'a> Loader<'a> {
256    pub(crate) fn new(
257        id: SourceId,
258        sources: &'a mut Sources,
259        diagnostics: &'a mut Diagnostics,
260        source_loader: &'a mut dyn SourceLoader,
261        manifest: &'a mut Manifest,
262    ) -> Self {
263        Self {
264            id,
265            sources,
266            diagnostics,
267            source_loader,
268            manifest,
269        }
270    }
271
272    /// Load a manifest.
273    pub(crate) fn load_manifest(&mut self) -> Result<()> {
274        let Some(source) = self.sources.get(self.id) else {
275            self.fatal(WorkspaceError::new(
276                Span::empty(),
277                WorkspaceErrorKind::MissingSourceId { source_id: self.id },
278            ))?;
279            return Ok(());
280        };
281
282        let value: SpannedValue = match toml::from_str(source.as_str()) {
283            Ok(value) => value,
284            Err(e) => {
285                let span = match e.span() {
286                    Some(span) => Span::new(span.start, span.end),
287                    None => Span::new(0, source.len()),
288                };
289
290                self.fatal(WorkspaceError::new(span, e))?;
291                return Ok(());
292            }
293        };
294
295        let root = source
296            .path()
297            .and_then(|p| p.parent().map(TryToOwned::try_to_owned))
298            .transpose()?;
299        let root = root.as_deref();
300
301        let Some((mut table, _)) = self.ensure_table(value)? else {
302            return Ok(());
303        };
304
305        // If manifest is a package, add it here.
306        if let Some((package, span)) = table
307            .remove("package")
308            .map(|value| self.ensure_table(value))
309            .transpose()?
310            .flatten()
311        {
312            if let Some(package) = self.load_package(package, span, root)? {
313                self.manifest.packages.try_push(package)?;
314            }
315        }
316
317        // Load the [workspace] section.
318        if let Some((mut table, span)) = table
319            .remove("workspace")
320            .map(|value| self.ensure_table(value))
321            .transpose()?
322            .flatten()
323        {
324            match &root {
325                Some(root) => {
326                    if let Some(members) = self.load_members(&mut table, root)? {
327                        for (span, path) in members {
328                            self.load_member(span, &path)?;
329                        }
330                    }
331                }
332                None => {
333                    self.fatal(WorkspaceError::new(
334                        span,
335                        WorkspaceErrorKind::MissingManifestPath,
336                    ))?;
337                }
338            }
339
340            self.ensure_empty(table)?;
341        }
342
343        self.ensure_empty(table)?;
344        Ok(())
345    }
346
347    /// Load members from the given workspace configuration.
348    fn load_members(
349        &mut self,
350        table: &mut Table,
351        root: &Path,
352    ) -> Result<Option<Vec<(Span, PathBuf)>>> {
353        let Some(members) = table.remove("members") else {
354            return Ok(None);
355        };
356
357        let Some((members, _)) = self.ensure_array(members)? else {
358            return Ok(None);
359        };
360
361        let mut output = Vec::new();
362
363        for value in members {
364            let span = Spanned::span(&value);
365
366            match deserialize::<RelativePathBuf>(value) {
367                Ok(member) => {
368                    self.glob_relative_path(&mut output, span, &member, root)?;
369                }
370                Err(error) => {
371                    self.fatal(error)?;
372                }
373            };
374        }
375
376        Ok(Some(output))
377    }
378
379    /// Glob a relative path.
380    ///
381    /// Currently only supports expanding `*` and required interacting with the
382    /// filesystem.
383    fn glob_relative_path(
384        &mut self,
385        output: &mut Vec<(Span, PathBuf)>,
386        span: Span,
387        member: &RelativePath,
388        root: &Path,
389    ) -> Result<()> {
390        let glob = glob::Glob::new(root, member)?;
391
392        for m in glob.matcher()? {
393            let Some(mut path) = self.glob_error(span, root, m)? else {
394                continue;
395            };
396
397            path.push(MANIFEST_FILE);
398
399            if !path.is_file() {
400                continue;
401            }
402
403            output.try_push((span, path))?;
404        }
405
406        Ok(())
407    }
408
409    /// Helper to convert an [io::Error] into a [WorkspaceErrorKind::SourceError].
410    fn glob_error<T>(
411        &mut self,
412        span: Span,
413        path: &Path,
414        result: Result<T, glob::GlobError>,
415    ) -> alloc::Result<Option<T>> {
416        Ok(match result {
417            Ok(result) => Some(result),
418            Err(error) => {
419                self.fatal(WorkspaceError::new(
420                    span,
421                    WorkspaceErrorKind::GlobError {
422                        path: path.try_into()?,
423                        error,
424                    },
425                ))?;
426
427                None
428            }
429        })
430    }
431
432    /// Try to load the given path as a member in the current manifest.
433    fn load_member(&mut self, span: Span, path: &Path) -> Result<()> {
434        let source = match self.source_loader.load(span, path) {
435            Ok(source) => source,
436            Err(error) => {
437                self.fatal(error)?;
438                return Ok(());
439            }
440        };
441
442        let id = self.sources.insert(source)?;
443        let old = std::mem::replace(&mut self.id, id);
444        self.load_manifest()?;
445        self.id = old;
446        Ok(())
447    }
448
449    /// Load a package from a value.
450    fn load_package(
451        &mut self,
452        mut table: Table,
453        span: Span,
454        root: Option<&Path>,
455    ) -> alloc::Result<Option<Package>> {
456        let name = self.field(&mut table, span, "name")?;
457        let version = self.field(&mut table, span, "version")?;
458        self.ensure_empty(table)?;
459
460        let (Some(name), Some(version)) = (name, version) else {
461            return Ok(None);
462        };
463
464        Ok(Some(Package {
465            name,
466            version,
467            root: root.map(|p| p.into()),
468            auto_bins: true,
469            auto_tests: true,
470            auto_examples: true,
471            auto_benches: true,
472        }))
473    }
474
475    /// Ensure that a table is empty and mark any additional elements as erroneous.
476    fn ensure_empty(&mut self, table: Table) -> alloc::Result<()> {
477        for (key, _) in table {
478            let span = Spanned::span(&key);
479            self.fatal(WorkspaceError::new(
480                span,
481                WorkspaceErrorKind::UnsupportedKey {
482                    key: key.get_ref().as_str().try_into()?,
483                },
484            ))?;
485        }
486
487        Ok(())
488    }
489
490    /// Ensure that value is a table.
491    fn ensure_table(&mut self, value: SpannedValue) -> alloc::Result<Option<(Table, Span)>> {
492        let span = Spanned::span(&value);
493
494        Ok(match value.into_inner() {
495            Value::Table(table) => Some((table, span)),
496            _ => {
497                let error = WorkspaceError::new(span, WorkspaceErrorKind::ExpectedTable);
498                self.fatal(error)?;
499                None
500            }
501        })
502    }
503
504    /// Coerce into an array or error.
505    fn ensure_array(&mut self, value: SpannedValue) -> alloc::Result<Option<(Array, Span)>> {
506        let span = Spanned::span(&value);
507
508        Ok(match value.into_inner() {
509            Value::Array(array) => Some((array, span)),
510            _ => {
511                let error = WorkspaceError::expected_array(span);
512                self.fatal(error)?;
513                None
514            }
515        })
516    }
517
518    /// Helper to load a single field.
519    fn field<T>(
520        &mut self,
521        table: &mut Table,
522        span: Span,
523        field: &'static str,
524    ) -> alloc::Result<Option<T>>
525    where
526        T: for<'de> Deserialize<'de>,
527    {
528        Ok(match table.remove(field) {
529            Some(value) => match deserialize(value) {
530                Ok(value) => Some(value),
531                Err(error) => {
532                    self.fatal(error)?;
533                    None
534                }
535            },
536            None => {
537                let error = WorkspaceError::missing_field(span, field);
538                self.fatal(error)?;
539                None
540            }
541        })
542    }
543
544    /// Report a fatal diagnostic.
545    fn fatal(&mut self, error: WorkspaceError) -> alloc::Result<()> {
546        self.diagnostics.fatal(self.id, error)
547    }
548}
549
550/// Helper to load a single field.
551fn deserialize<T>(value: SpannedValue) -> Result<T, WorkspaceError>
552where
553    T: for<'de> Deserialize<'de>,
554{
555    let span = Spanned::span(&value);
556    let f = key::to_key(value.get_ref()).map_err(|e| WorkspaceError::new(span, e))?;
557    let deserializer = f.into_deserializer();
558    let value = T::deserialize(deserializer).map_err(|e| WorkspaceError::new(span, e))?;
559    Ok(value)
560}
561
562/// Find all rune files in the given path.
563fn find_rune_files(path: &Path) -> Result<impl Iterator<Item = Result<(PathBuf, String)>>> {
564    let mut dir = match fs::read_dir(path) {
565        Ok(dir) => Some(dir),
566        Err(e) if e.kind() == io::ErrorKind::NotFound => None,
567        Err(e) => return Err(e.into()),
568    };
569
570    Ok(iter::from_fn(move || loop {
571        let e = dir.as_mut()?.next()?;
572
573        let e = match e {
574            Ok(e) => e,
575            Err(err) => return Some(Err(err.into())),
576        };
577
578        let m = match e.metadata() {
579            Ok(m) => m,
580            Err(err) => return Some(Err(err.into())),
581        };
582
583        if !m.is_file() {
584            continue;
585        }
586
587        let path = e.path();
588
589        let (Some(name), Some(ext)) = (path.file_stem().and_then(OsStr::to_str), path.extension())
590        else {
591            continue;
592        };
593
594        if ext != OsStr::new("rn") {
595            continue;
596        }
597
598        let name = match String::try_from(name) {
599            Ok(name) => name,
600            Err(error) => return Some(Err(error.into())),
601        };
602
603        return Some(Ok((path, name)));
604    }))
605}