rune/workspace/
manifest.rs

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