wdl_engine/
eval.rs

1//! Module for evaluation.
2
3use std::borrow::Cow;
4use std::collections::HashMap;
5use std::fmt;
6use std::fs;
7use std::io::BufRead;
8use std::path::Path;
9use std::path::PathBuf;
10use std::sync::Arc;
11
12use anyhow::Context;
13use anyhow::Result;
14use anyhow::bail;
15use cloud_copy::TransferEvent;
16use crankshaft::events::Event as CrankshaftEvent;
17use indexmap::IndexMap;
18use itertools::Itertools;
19use rev_buf_reader::RevBufReader;
20use tokio::sync::broadcast;
21use wdl_analysis::Document;
22use wdl_analysis::document::Task;
23use wdl_analysis::types::Type;
24use wdl_ast::Diagnostic;
25use wdl_ast::Span;
26use wdl_ast::SupportedVersion;
27use wdl_ast::v1::TASK_REQUIREMENT_RETURN_CODES;
28use wdl_ast::v1::TASK_REQUIREMENT_RETURN_CODES_ALIAS;
29
30use crate::CompoundValue;
31use crate::Outputs;
32use crate::PrimitiveValue;
33use crate::TaskExecutionResult;
34use crate::Value;
35use crate::http::Location;
36use crate::http::Transferer;
37use crate::path;
38use crate::path::EvaluationPath;
39use crate::stdlib::download_file;
40
41pub mod trie;
42pub mod v1;
43
44/// The maximum number of stderr lines to display in error messages.
45const MAX_STDERR_LINES: usize = 10;
46
47/// A name used whenever a file system "root" is mapped.
48///
49/// A root might be a root directory like `/` or `C:\`, but it also might be the root of a URL like `https://example.com`.
50const ROOT_NAME: &str = ".root";
51
52/// Represents events that may be sent during evaluation.
53#[derive(Debug, Clone, Default)]
54pub struct Events {
55    /// The Crankshaft events channel.
56    ///
57    /// This is `None` when Crankshaft events are not enabled.
58    crankshaft: Option<broadcast::Sender<CrankshaftEvent>>,
59    /// The transfer events channel.
60    ///
61    /// This is `None` when transfer events are not enabled.
62    transfer: Option<broadcast::Sender<TransferEvent>>,
63}
64
65impl Events {
66    /// Constructs a new `Events` and enables subscribing to all event channels.
67    pub fn all(capacity: usize) -> Self {
68        Self {
69            crankshaft: Some(broadcast::Sender::new(capacity)),
70            transfer: Some(broadcast::Sender::new(capacity)),
71        }
72    }
73
74    /// Constructs a new `Events` and disable subscribing to any event channel.
75    pub fn none() -> Self {
76        Self::default()
77    }
78
79    /// Constructs a new `Events` and enable subscribing to only the Crankshaft
80    /// events channel.
81    pub fn crankshaft_only(capacity: usize) -> Self {
82        Self {
83            crankshaft: Some(broadcast::Sender::new(capacity)),
84            transfer: None,
85        }
86    }
87
88    /// Constructs a new `Events` and enable subscribing to only the transfer
89    /// events channel.
90    pub fn transfer_only(capacity: usize) -> Self {
91        Self {
92            crankshaft: None,
93            transfer: Some(broadcast::Sender::new(capacity)),
94        }
95    }
96
97    /// Subscribes to the Crankshaft events channel.
98    ///
99    /// Returns `None` if Crankshaft events are not enabled.
100    pub fn subscribe_crankshaft(&self) -> Option<broadcast::Receiver<CrankshaftEvent>> {
101        self.crankshaft.as_ref().map(|s| s.subscribe())
102    }
103
104    /// Subscribes to the transfer events channel.
105    ///
106    /// Returns `None` if transfer events are not enabled.
107    pub fn subscribe_transfer(&self) -> Option<broadcast::Receiver<TransferEvent>> {
108        self.transfer.as_ref().map(|s| s.subscribe())
109    }
110
111    /// Gets the sender for the Crankshaft events.
112    pub(crate) fn crankshaft(&self) -> &Option<broadcast::Sender<CrankshaftEvent>> {
113        &self.crankshaft
114    }
115
116    /// Gets the sender for the transfer events.
117    pub(crate) fn transfer(&self) -> &Option<broadcast::Sender<TransferEvent>> {
118        &self.transfer
119    }
120}
121
122/// Represents the location of a call in an evaluation error.
123#[derive(Debug, Clone)]
124pub struct CallLocation {
125    /// The document containing the call statement.
126    pub document: Document,
127    /// The span of the call statement.
128    pub span: Span,
129}
130
131/// Represents an error that originates from WDL source.
132#[derive(Debug)]
133pub struct SourceError {
134    /// The document originating the diagnostic.
135    pub document: Document,
136    /// The evaluation diagnostic.
137    pub diagnostic: Diagnostic,
138    /// The call backtrace for the error.
139    ///
140    /// An empty backtrace denotes that the error was encountered outside of
141    /// a call.
142    ///
143    /// The call locations are stored as most recent to least recent.
144    pub backtrace: Vec<CallLocation>,
145}
146
147/// Represents an error that may occur when evaluating a workflow or task.
148#[derive(Debug)]
149pub enum EvaluationError {
150    /// The error came from WDL source evaluation.
151    Source(Box<SourceError>),
152    /// The error came from another source.
153    Other(anyhow::Error),
154}
155
156impl EvaluationError {
157    /// Creates a new evaluation error from the given document and diagnostic.
158    pub fn new(document: Document, diagnostic: Diagnostic) -> Self {
159        Self::Source(Box::new(SourceError {
160            document,
161            diagnostic,
162            backtrace: Default::default(),
163        }))
164    }
165
166    /// Helper for tests for converting an evaluation error to a string.
167    #[cfg(feature = "codespan-reporting")]
168    #[allow(clippy::inherent_to_string)]
169    pub fn to_string(&self) -> String {
170        use codespan_reporting::diagnostic::Label;
171        use codespan_reporting::diagnostic::LabelStyle;
172        use codespan_reporting::files::SimpleFiles;
173        use codespan_reporting::term::Config;
174        use codespan_reporting::term::termcolor::Buffer;
175        use codespan_reporting::term::{self};
176        use wdl_ast::AstNode;
177
178        match self {
179            Self::Source(e) => {
180                let mut files = SimpleFiles::new();
181                let mut map = HashMap::new();
182
183                let file_id = files.add(e.document.path(), e.document.root().text().to_string());
184
185                let diagnostic =
186                    e.diagnostic
187                        .to_codespan(file_id)
188                        .with_labels_iter(e.backtrace.iter().map(|l| {
189                            let id = l.document.id();
190                            let file_id = *map.entry(id).or_insert_with(|| {
191                                files.add(l.document.path(), l.document.root().text().to_string())
192                            });
193
194                            Label {
195                                style: LabelStyle::Secondary,
196                                file_id,
197                                range: l.span.start()..l.span.end(),
198                                message: "called from this location".into(),
199                            }
200                        }));
201
202                let mut buffer = Buffer::no_color();
203                term::emit(&mut buffer, &Config::default(), &files, &diagnostic)
204                    .expect("failed to emit diagnostic");
205
206                String::from_utf8(buffer.into_inner()).expect("should be UTF-8")
207            }
208            Self::Other(e) => format!("{e:?}"),
209        }
210    }
211}
212
213impl From<anyhow::Error> for EvaluationError {
214    fn from(e: anyhow::Error) -> Self {
215        Self::Other(e)
216    }
217}
218
219/// Represents a result from evaluating a workflow or task.
220pub type EvaluationResult<T> = Result<T, EvaluationError>;
221
222/// Represents a path to a file or directory on the host file system or a URL to
223/// a remote file.
224///
225/// The host in this context is where the WDL evaluation is taking place.
226#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
227pub struct HostPath(pub(crate) Arc<String>);
228
229impl HostPath {
230    /// Constructs a new host path from a string.
231    pub fn new(path: impl Into<String>) -> Self {
232        Self(Arc::new(path.into()))
233    }
234
235    /// Gets the string representation of the host path.
236    pub fn as_str(&self) -> &str {
237        &self.0
238    }
239
240    /// Shell expands the path.
241    ///
242    /// The path is also joined with the provided base directory.
243    pub fn expand(&mut self, base_dir: &EvaluationPath) -> Result<()> {
244        // Perform the expansion
245        if let Cow::Owned(s) = shellexpand::full(self.as_str()).with_context(|| {
246            format!("failed to shell expand path `{path}`", path = self.as_str())
247        })? {
248            *Arc::make_mut(&mut self.0) = s;
249        }
250
251        // Don't join URLs
252        if path::is_url(self.as_str()) {
253            return Ok(());
254        }
255
256        // Perform the join
257        if let Some(s) = base_dir.join(self.as_str())?.to_str() {
258            *Arc::make_mut(&mut self.0) = s.to_string();
259        }
260
261        Ok(())
262    }
263}
264
265impl fmt::Display for HostPath {
266    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267        self.0.fmt(f)
268    }
269}
270
271impl From<Arc<String>> for HostPath {
272    fn from(path: Arc<String>) -> Self {
273        Self(path)
274    }
275}
276
277impl From<HostPath> for Arc<String> {
278    fn from(path: HostPath) -> Self {
279        path.0
280    }
281}
282
283/// Represents a path to a file or directory on the guest.
284///
285/// The guest in this context is the container where tasks are run.
286///
287/// For backends that do not use containers, a guest path is the same as a host
288/// path.
289#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
290pub struct GuestPath(pub(crate) Arc<String>);
291
292impl GuestPath {
293    /// Constructs a new guest path from a string.
294    pub fn new(path: impl Into<String>) -> Self {
295        Self(Arc::new(path.into()))
296    }
297
298    /// Gets the string representation of the guest path.
299    pub fn as_str(&self) -> &str {
300        &self.0
301    }
302}
303
304impl fmt::Display for GuestPath {
305    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
306        self.0.fmt(f)
307    }
308}
309
310impl From<Arc<String>> for GuestPath {
311    fn from(path: Arc<String>) -> Self {
312        Self(path)
313    }
314}
315
316impl From<GuestPath> for Arc<String> {
317    fn from(path: GuestPath) -> Self {
318        path.0
319    }
320}
321
322/// Represents context to an expression evaluator.
323pub trait EvaluationContext: Send + Sync {
324    /// Gets the supported version of the document being evaluated.
325    fn version(&self) -> SupportedVersion;
326
327    /// Gets the value of the given name in scope.
328    fn resolve_name(&self, name: &str, span: Span) -> Result<Value, Diagnostic>;
329
330    /// Resolves a type name to a type.
331    fn resolve_type_name(&self, name: &str, span: Span) -> Result<Type, Diagnostic>;
332
333    /// Gets the base directory for the evaluation.
334    ///
335    /// The base directory is what paths are relative to.
336    ///
337    /// For workflow evaluation, the base directory is the document's directory.
338    ///
339    /// For task evaluation, the base directory is the document's directory or
340    /// the task's working directory if the `output` section is being evaluated.
341    fn base_dir(&self) -> &EvaluationPath;
342
343    /// Gets the temp directory for the evaluation.
344    fn temp_dir(&self) -> &Path;
345
346    /// Gets the value to return for a call to the `stdout` function.
347    ///
348    /// This returns `Some` only when evaluating a task's outputs section.
349    fn stdout(&self) -> Option<&Value> {
350        None
351    }
352
353    /// Gets the value to return for a call to the `stderr` function.
354    ///
355    /// This returns `Some` only when evaluating a task's outputs section.
356    fn stderr(&self) -> Option<&Value> {
357        None
358    }
359
360    /// Gets the task associated with the evaluation context.
361    ///
362    /// This returns `Some` only when evaluating a task's hints sections.
363    fn task(&self) -> Option<&Task> {
364        None
365    }
366
367    /// Gets the transferer to use for evaluating expressions.
368    fn transferer(&self) -> &dyn Transferer;
369
370    /// Gets a guest path representation of a host path.
371    ///
372    /// Returns `None` if there is no guest path representation of the host
373    /// path.
374    fn guest_path(&self, path: &HostPath) -> Option<GuestPath> {
375        let _ = path;
376        None
377    }
378
379    /// Gets a host path representation of a guest path.
380    ///
381    /// Returns `None` if there is no host path representation of the guest
382    /// path.
383    fn host_path(&self, path: &GuestPath) -> Option<HostPath> {
384        let _ = path;
385        None
386    }
387
388    /// Notifies the context that a file was created as a result of a call to a
389    /// stdlib function.
390    ///
391    /// A context may map a guest path for the new host path.
392    fn notify_file_created(&mut self, path: &HostPath) -> Result<()> {
393        let _ = path;
394        Ok(())
395    }
396}
397
398/// Represents an index of a scope in a collection of scopes.
399#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
400pub struct ScopeIndex(usize);
401
402impl ScopeIndex {
403    /// Constructs a new scope index from a raw index.
404    pub const fn new(index: usize) -> Self {
405        Self(index)
406    }
407}
408
409impl From<usize> for ScopeIndex {
410    fn from(index: usize) -> Self {
411        Self(index)
412    }
413}
414
415impl From<ScopeIndex> for usize {
416    fn from(index: ScopeIndex) -> Self {
417        index.0
418    }
419}
420
421/// Represents an evaluation scope in a WDL document.
422#[derive(Default, Debug)]
423pub struct Scope {
424    /// The index of the parent scope.
425    ///
426    /// This is `None` for the root scopes.
427    parent: Option<ScopeIndex>,
428    /// The map of names in scope to their values.
429    names: IndexMap<String, Value>,
430}
431
432impl Scope {
433    /// Creates a new scope given the parent scope.
434    pub fn new(parent: ScopeIndex) -> Self {
435        Self {
436            parent: Some(parent),
437            names: Default::default(),
438        }
439    }
440
441    /// Inserts a name into the scope.
442    pub fn insert(&mut self, name: impl Into<String>, value: impl Into<Value>) {
443        let prev = self.names.insert(name.into(), value.into());
444        assert!(prev.is_none(), "conflicting name in scope");
445    }
446
447    /// Iterates over the local names and values in the scope.
448    pub fn local(&self) -> impl Iterator<Item = (&str, &Value)> + use<'_> {
449        self.names.iter().map(|(k, v)| (k.as_str(), v))
450    }
451
452    /// Gets a mutable reference to an existing name in scope.
453    pub(crate) fn get_mut(&mut self, name: &str) -> Option<&mut Value> {
454        self.names.get_mut(name)
455    }
456
457    /// Clears the scope.
458    pub(crate) fn clear(&mut self) {
459        self.parent = None;
460        self.names.clear();
461    }
462
463    /// Sets the scope's parent.
464    pub(crate) fn set_parent(&mut self, parent: ScopeIndex) {
465        self.parent = Some(parent);
466    }
467}
468
469impl From<Scope> for IndexMap<String, Value> {
470    fn from(scope: Scope) -> Self {
471        scope.names
472    }
473}
474
475/// Represents a reference to a scope.
476#[derive(Debug, Clone, Copy)]
477pub struct ScopeRef<'a> {
478    /// The reference to the scopes collection.
479    scopes: &'a [Scope],
480    /// The index of the scope in the collection.
481    index: ScopeIndex,
482}
483
484impl<'a> ScopeRef<'a> {
485    /// Creates a new scope reference given the scope index.
486    pub fn new(scopes: &'a [Scope], index: impl Into<ScopeIndex>) -> Self {
487        Self {
488            scopes,
489            index: index.into(),
490        }
491    }
492
493    /// Gets the parent scope.
494    ///
495    /// Returns `None` if there is no parent scope.
496    pub fn parent(&self) -> Option<Self> {
497        self.scopes[self.index.0].parent.map(|p| Self {
498            scopes: self.scopes,
499            index: p,
500        })
501    }
502
503    /// Gets all of the name and values available at this scope.
504    pub fn names(&self) -> impl Iterator<Item = (&str, &Value)> + use<'_> {
505        self.scopes[self.index.0]
506            .names
507            .iter()
508            .map(|(n, name)| (n.as_str(), name))
509    }
510
511    /// Iterates over each name and value visible to the scope and calls the
512    /// provided callback.
513    ///
514    /// Stops iterating and returns an error if the callback returns an error.
515    pub fn for_each(&self, mut cb: impl FnMut(&str, &Value) -> Result<()>) -> Result<()> {
516        let mut current = Some(self.index);
517
518        while let Some(index) = current {
519            for (n, v) in self.scopes[index.0].local() {
520                cb(n, v)?;
521            }
522
523            current = self.scopes[index.0].parent;
524        }
525
526        Ok(())
527    }
528
529    /// Gets the value of a name local to this scope.
530    ///
531    /// Returns `None` if a name local to this scope was not found.
532    pub fn local(&self, name: &str) -> Option<&Value> {
533        self.scopes[self.index.0].names.get(name)
534    }
535
536    /// Lookups a name in the scope.
537    ///
538    /// Returns `None` if the name is not available in the scope.
539    pub fn lookup(&self, name: &str) -> Option<&Value> {
540        let mut current = Some(self.index);
541
542        while let Some(index) = current {
543            if let Some(name) = self.scopes[index.0].names.get(name) {
544                return Some(name);
545            }
546
547            current = self.scopes[index.0].parent;
548        }
549
550        None
551    }
552}
553
554/// Represents an evaluated task.
555#[derive(Debug)]
556pub struct EvaluatedTask {
557    /// The task attempt directory.
558    attempt_dir: PathBuf,
559    /// The task execution result.
560    result: TaskExecutionResult,
561    /// The evaluated outputs of the task.
562    ///
563    /// This is `Ok` when the task executes successfully and all of the task's
564    /// outputs evaluated without error.
565    ///
566    /// Otherwise, this contains the error that occurred while attempting to
567    /// evaluate the task's outputs.
568    outputs: EvaluationResult<Outputs>,
569}
570
571impl EvaluatedTask {
572    /// Constructs a new evaluated task.
573    ///
574    /// Returns an error if the stdout or stderr paths are not UTF-8.
575    fn new(attempt_dir: PathBuf, result: TaskExecutionResult) -> anyhow::Result<Self> {
576        Ok(Self {
577            result,
578            attempt_dir,
579            outputs: Ok(Default::default()),
580        })
581    }
582
583    /// Gets the exit code of the evaluated task.
584    pub fn exit_code(&self) -> i32 {
585        self.result.exit_code
586    }
587
588    /// Gets the attempt directory of the task.
589    pub fn attempt_dir(&self) -> &Path {
590        &self.attempt_dir
591    }
592
593    /// Gets the working directory of the evaluated task.
594    pub fn work_dir(&self) -> &EvaluationPath {
595        &self.result.work_dir
596    }
597
598    /// Gets the stdout value of the evaluated task.
599    pub fn stdout(&self) -> &Value {
600        &self.result.stdout
601    }
602
603    /// Gets the stderr value of the evaluated task.
604    pub fn stderr(&self) -> &Value {
605        &self.result.stderr
606    }
607
608    /// Gets the outputs of the evaluated task.
609    ///
610    /// This is `Ok` when the task executes successfully and all of the task's
611    /// outputs evaluated without error.
612    ///
613    /// Otherwise, this contains the error that occurred while attempting to
614    /// evaluate the task's outputs.
615    pub fn outputs(&self) -> &EvaluationResult<Outputs> {
616        &self.outputs
617    }
618
619    /// Converts the evaluated task into an evaluation result.
620    ///
621    /// Returns `Ok(_)` if the task outputs were evaluated.
622    ///
623    /// Returns `Err(_)` if the task outputs could not be evaluated.
624    pub fn into_result(self) -> EvaluationResult<Outputs> {
625        self.outputs
626    }
627
628    /// Handles the exit of a task execution.
629    ///
630    /// Returns an error if the task failed.
631    async fn handle_exit(
632        &self,
633        requirements: &HashMap<String, Value>,
634        transferer: &dyn Transferer,
635    ) -> anyhow::Result<()> {
636        let mut error = true;
637        if let Some(return_codes) = requirements
638            .get(TASK_REQUIREMENT_RETURN_CODES)
639            .or_else(|| requirements.get(TASK_REQUIREMENT_RETURN_CODES_ALIAS))
640        {
641            match return_codes {
642                Value::Primitive(PrimitiveValue::String(s)) if s.as_ref() == "*" => {
643                    error = false;
644                }
645                Value::Primitive(PrimitiveValue::String(s)) => {
646                    bail!(
647                        "invalid return code value `{s}`: only `*` is accepted when the return \
648                         code is specified as a string"
649                    );
650                }
651                Value::Primitive(PrimitiveValue::Integer(ok)) => {
652                    if self.result.exit_code == i32::try_from(*ok).unwrap_or_default() {
653                        error = false;
654                    }
655                }
656                Value::Compound(CompoundValue::Array(codes)) => {
657                    error = !codes.as_slice().iter().any(|v| {
658                        v.as_integer()
659                            .map(|i| i32::try_from(i).unwrap_or_default() == self.result.exit_code)
660                            .unwrap_or(false)
661                    });
662                }
663                _ => unreachable!("unexpected return codes value"),
664            }
665        } else {
666            error = self.result.exit_code != 0;
667        }
668
669        if error {
670            // Read the last `MAX_STDERR_LINES` number of lines from stderr
671            // If there's a problem reading stderr, don't output it
672            let stderr = download_file(
673                transferer,
674                self.work_dir(),
675                self.stderr().as_file().unwrap(),
676            )
677            .await
678            .ok()
679            .and_then(|l| {
680                fs::File::open(l).ok().map(|f| {
681                    // Buffer the last N number of lines
682                    let reader = RevBufReader::new(f);
683                    let lines: Vec<_> = reader
684                        .lines()
685                        .take(MAX_STDERR_LINES)
686                        .map_while(|l| l.ok())
687                        .collect();
688
689                    // Iterate the lines in reverse order as we read them in reverse
690                    lines
691                        .iter()
692                        .rev()
693                        .format_with("\n", |l, f| f(&format_args!("  {l}")))
694                        .to_string()
695                })
696            })
697            .unwrap_or_default();
698
699            // If the work directory is remote,
700            bail!(
701                "process terminated with exit code {code}: see `{stdout_path}` and \
702                 `{stderr_path}` for task output and the related files in \
703                 `{dir}`{header}{stderr}{trailer}",
704                code = self.result.exit_code,
705                dir = self.attempt_dir().display(),
706                stdout_path = self.stdout().as_file().expect("must be file"),
707                stderr_path = self.stderr().as_file().expect("must be file"),
708                header = if stderr.is_empty() {
709                    Cow::Borrowed("")
710                } else {
711                    format!("\n\ntask stderr output (last {MAX_STDERR_LINES} lines):\n\n").into()
712                },
713                trailer = if stderr.is_empty() { "" } else { "\n" }
714            );
715        }
716
717        Ok(())
718    }
719}
720
721/// Gets the kind of an input.
722#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
723pub enum InputKind {
724    /// The input is a single file.
725    File,
726    /// The input is a directory.
727    Directory,
728}
729
730impl From<InputKind> for crankshaft::engine::task::input::Type {
731    fn from(value: InputKind) -> Self {
732        match value {
733            InputKind::File => Self::File,
734            InputKind::Directory => Self::Directory,
735        }
736    }
737}
738
739/// Represents a `File` or `Directory` input to a task.
740#[derive(Debug, Clone)]
741pub struct Input {
742    /// The input kind.
743    kind: InputKind,
744    /// The path for the input.
745    path: EvaluationPath,
746    /// The guest path for the input.
747    ///
748    /// This is `None` when the backend isn't mapping input paths.
749    guest_path: Option<GuestPath>,
750    /// The download location for the input.
751    ///
752    /// This is `Some` if the input has been downloaded to a known location.
753    location: Option<Location>,
754}
755
756impl Input {
757    /// Creates a new input with the given path and access.
758    fn new(kind: InputKind, path: EvaluationPath, guest_path: Option<GuestPath>) -> Self {
759        Self {
760            kind,
761            path,
762            guest_path,
763            location: None,
764        }
765    }
766
767    /// Gets the kind of the input.
768    pub fn kind(&self) -> InputKind {
769        self.kind
770    }
771
772    /// Gets the path to the input.
773    ///
774    /// The path of the input may be local or remote.
775    pub fn path(&self) -> &EvaluationPath {
776        &self.path
777    }
778
779    /// Gets the guest path for the input.
780    ///
781    /// This is `None` for inputs to backends that don't use containers.
782    pub fn guest_path(&self) -> Option<&GuestPath> {
783        self.guest_path.as_ref()
784    }
785
786    /// Gets the local path of the input.
787    ///
788    /// Returns `None` if the input is remote and has not been localized.
789    pub fn local_path(&self) -> Option<&Path> {
790        self.location.as_deref().or_else(|| self.path.as_local())
791    }
792
793    /// Sets the location of the input.
794    ///
795    /// This is used during localization to set a local path for remote inputs.
796    pub fn set_location(&mut self, location: Location) {
797        self.location = Some(location);
798    }
799}