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#[derive(Debug, Clone, Copy)]
25#[non_exhaustive]
26pub enum WorkspaceFilter<'a> {
27 Name(&'a str),
29 All,
31}
32
33#[derive(Debug, Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
35#[non_exhaustive]
36pub enum FoundKind {
37 Binary,
39 Library,
41 Test,
43 Example,
45 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#[derive(Debug, TryClone)]
76#[non_exhaustive]
77pub struct Found {
78 #[try_clone(copy)]
80 pub kind: FoundKind,
81 pub path: PathBuf,
83 pub name: String,
85}
86
87#[derive(Debug, TryClone)]
89#[non_exhaustive]
90pub struct FoundPackage<'a> {
91 pub found: Found,
93 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#[derive(Default, Debug)]
116#[non_exhaustive]
117pub struct Manifest {
118 pub packages: Vec<Package>,
120}
121
122impl Manifest {
123 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 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#[derive(Debug)]
152#[non_exhaustive]
153pub struct Package {
154 pub name: String,
156 pub version: Version,
158 pub root: Option<PathBuf>,
160 pub auto_bins: bool,
162 pub auto_libs: bool,
164 pub auto_tests: bool,
166 pub auto_examples: bool,
168 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 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 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 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 pub fn find_tests(&self, filter: WorkspaceFilter<'_>) -> Result<Vec<Found>> {
305 self.find_in_directory(filter, FoundKind::Test, "tests")
306 }
307
308 pub fn find_examples(&self, filter: WorkspaceFilter<'_>) -> Result<Vec<Found>> {
310 self.find_in_directory(filter, FoundKind::Example, "examples")
311 }
312
313 pub fn find_benches(&self, filter: WorkspaceFilter<'_>) -> Result<Vec<Found>> {
315 self.find_in_directory(filter, FoundKind::Bench, "benches")
316 }
317}
318
319pub 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 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 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 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 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 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 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 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 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 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 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 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 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 fn fatal(&mut self, error: WorkspaceError) -> alloc::Result<()> {
620 self.diagnostics.fatal(self.id, error)
621 }
622}
623
624fn 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
636fn 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}