Skip to main content

midenc_session/
lib.rs

1#![no_std]
2#![feature(debug_closure_helpers)]
3#![feature(specialization)]
4// Specialization
5#![allow(incomplete_features)]
6#![deny(warnings)]
7
8#[macro_use]
9extern crate alloc;
10#[cfg(feature = "std")]
11extern crate std;
12
13use alloc::{
14    borrow::ToOwned,
15    format,
16    string::{String, ToString},
17};
18
19mod color;
20pub mod diagnostics;
21#[cfg(feature = "std")]
22mod duration;
23mod emit;
24mod emitter;
25pub mod flags;
26mod inputs;
27mod libs;
28mod options;
29mod outputs;
30#[cfg(feature = "std")]
31mod package_lease;
32pub mod path;
33pub mod registry;
34#[cfg(feature = "std")]
35mod statistics;
36
37use alloc::{boxed::Box, fmt, sync::Arc};
38
39/// The version associated with the current compiler toolchain
40pub const MIDENC_BUILD_VERSION: &str = env!("MIDENC_BUILD_VERSION");
41
42/// The git revision associated with the current compiler toolchain
43pub const MIDENC_BUILD_REV: &str = env!("MIDENC_BUILD_REV");
44
45pub use miden_assembly_syntax;
46pub use miden_mast_package::PackageId;
47pub use miden_package_registry;
48pub use miden_project;
49use midenc_hir_symbol::Symbol;
50
51pub use self::{
52    color::ColorChoice,
53    diagnostics::{DiagnosticsHandler, Emitter, Report, SourceManager},
54    emit::{Emit, Writer},
55    flags::{ArgMatches, CompileFlag, CompileFlags, FlagAction},
56    inputs::{FileName, FileType, InputFile, InputType, InvalidInputError},
57    libs::{LibraryPath, LibraryPathComponent, LinkLibrary, add_target_link_libraries},
58    options::*,
59    outputs::{OutputFile, OutputFiles, OutputMode, OutputType, OutputTypeSpec, OutputTypes},
60    path::{Path, PathBuf},
61};
62#[cfg(feature = "std")]
63pub use self::{duration::HumanDuration, emit::EmitExt, statistics::Statistics};
64
65/// This struct provides access to all of the metadata and configuration
66/// needed during a single compilation session.
67#[derive(Clone)]
68pub struct Session {
69    /// The name of this session
70    pub name: String,
71    /// Configuration for the current compiler session
72    pub options: Box<Options>,
73    /// The current source manager
74    pub source_manager: Arc<dyn SourceManager>,
75    /// The current diagnostics handler
76    pub diagnostics: Arc<DiagnosticsHandler>,
77    /// The inputs being compiled
78    pub input: Option<InputFile>,
79    /// The outputs to be produced by the compiler during compilation
80    pub output_files: OutputFiles,
81    /// Statistics gathered from the current compiler session
82    #[cfg(feature = "std")]
83    pub statistics: Statistics,
84    /// The per-build package-exchange directory, created once for the root build.
85    ///
86    /// Shared by `Arc`, so every clone of this session observes the same lease and no clone
87    /// can mint a second directory. The `Result` memoizes a creation failure, which is
88    /// re-reported identically on every access (fail closed). Dropping the last owner
89    /// deletes the directory; see the `package_lease` module for the lifecycle.
90    #[cfg(feature = "std")]
91    package_cache_lease: package_lease::SharedPackageCacheLease,
92}
93
94impl fmt::Debug for Session {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        f.debug_struct("Session")
97            .field("name", &self.name)
98            .field("options", &self.options)
99            .field("inputs", &self.input)
100            .field("output_files", &self.output_files)
101            .finish_non_exhaustive()
102    }
103}
104
105impl Session {
106    /// Open a session compiling `input` under `options`.
107    ///
108    /// # A project locator is read for its facts, not loaded as a project
109    ///
110    /// A `.toml` input is a *locator*: it names the project to build rather than being something
111    /// the compiler compiles. Three facts about that project are needed before this session
112    /// exists, because this constructor is downstream of none of them:
113    ///
114    /// - the **package name**, which is the artifact name absent `--name`, and which
115    ///   [`OutputFiles`] is built from below;
116    /// - the **library target's kind**, which is what [`Options::target_type`] defaults to, and
117    ///   which [`add_target_link_libraries`] then consults to decide whether the Miden protocol
118    ///   is linked;
119    /// - the **executable targets' names**, from which [`Options::entrypoint`] is defaulted.
120    ///
121    /// All three come from `ProjectManifest`, which parses the manifest's *AST* and reads
122    /// exactly those three things out of it with `miden_project`'s own extractors. What it
123    /// deliberately does not do is build a [`miden_project::Project`]: loading the project is
124    /// `prepare_project`'s, in `midenc-compile`, and the package it loads is the one that gets
125    /// assembled. A session that loaded its own would be loading the same manifest twice to
126    /// produce a package nothing compiles.
127    ///
128    /// **Failing to read the manifest is not an error here.** Which project a locator names, and
129    /// whether it names one at all, is decided and reported downstream — where the locator is
130    /// normalized anyway, and where a Cargo workspace root gets the diagnostic that belongs to
131    /// it rather than a "no such file" for the `miden-project.toml` a workspace root does not
132    /// have. So an unreadable manifest falls back to the same name derivation a source-file
133    /// input uses, and leaves `target_type` and `entrypoint` alone.
134    pub fn new(
135        input: InputFile,
136        mut options: Box<Options>,
137        emitter: Option<Arc<dyn Emitter>>,
138        source_manager: Arc<dyn SourceManager + Send + Sync>,
139    ) -> Result<Self, Report> {
140        let manifest = if matches!(input.file_type(), FileType::Toml) {
141            ProjectManifest::read(&input, source_manager.as_ref())?
142        } else {
143            None
144        };
145
146        if let Some(manifest) = manifest.as_ref() {
147            if options.target_type.is_none() {
148                options.target_type = Some(manifest.library_target_type());
149            }
150            if is_cargo_project_input(&input) {
151                infer_cargo_project_entrypoint(manifest, &mut options)?;
152            }
153        }
154
155        let name = options
156            .name
157            .clone()
158            .or_else(|| manifest.as_ref().map(|manifest| manifest.name.to_string()))
159            .or_else(|| {
160                log::debug!(target: "driver", "no name specified, attempting to derive from output file");
161                options.output_file.as_ref().and_then(|of| of.filestem().map(|stem| stem.to_string()))
162            })
163            .unwrap_or_else(|| {
164                log::debug!(target: "driver", "unable to derive name from output file, deriving from input");
165                match &input {
166                    InputFile {
167                        file: InputType::Real(path),
168                        ..
169                    } => path
170                        .file_stem()
171                        .and_then(|stem| stem.to_str())
172                        .or_else(|| path.extension().and_then(|stem| stem.to_str()))
173                        .unwrap_or_else(|| {
174                            panic!(
175                                "invalid input path: '{}' has no file stem or extension",
176                                path.display()
177                            )
178                        })
179                        .to_string(),
180                        input @ InputFile {
181                            file: InputType::Stdin { name, .. },
182                            ..
183                        } => {
184                        let name = name.as_str();
185                        if matches!(name, "empty" | "stdin") {
186                            log::debug!(target: "driver", "no good input file name to use, using current directory base name");
187                            options
188                                .current_dir
189                                .file_stem()
190                                .and_then(|stem| stem.to_str())
191                                .unwrap_or(name)
192                                .to_string()
193                        } else {
194                            input.filestem().to_owned()
195                        }
196                    }
197                }
198            });
199        log::debug!(target: "driver", "artifact name set to '{name}'");
200
201        // Where `prepare_temporary_cargo_project` copies a standalone Rust source to, mapped back
202        // to where the source came from, so that debug information names the file the user wrote.
203        // Only for a source-file input: a project is built by `cargo` in place, and is never
204        // copied anywhere.
205        if !matches!(input.file_type(), FileType::Toml)
206            && let InputType::Real(path) = &input.file
207        {
208            #[cfg(feature = "std")]
209            {
210                let tmp = std::env::temp_dir().canonicalize().unwrap();
211                let project_dir = tmp.join(&name).join("src");
212                let project_remap_target = if path.is_absolute() {
213                    Some(
214                        path.strip_prefix(&options.current_dir)
215                            .ok()
216                            .or(path.as_path().parent())
217                            .unwrap()
218                            .to_path_buf()
219                            .into_boxed_path(),
220                    )
221                } else {
222                    path.parent().map(|p| p.to_path_buf().into_boxed_path())
223                };
224                options.remap_path_prefixes.push(RemapPathPrefix {
225                    from: project_dir.into_boxed_path(),
226                    to: project_remap_target,
227                });
228            }
229        }
230
231        Ok(Self::new_project(name, Some(input), options, emitter, source_manager))
232    }
233
234    /// Open a session named `name`, for a caller that already knows what it is building.
235    ///
236    /// [`Session::new`] derives `name` from its input; this takes it. Both then do the same
237    /// thing, and neither knows anything about the project being built beyond its name.
238    pub fn new_project(
239        name: String,
240        input: Option<InputFile>,
241        mut options: Box<Options>,
242        emitter: Option<Arc<dyn Emitter>>,
243        source_manager: Arc<dyn SourceManager>,
244    ) -> Self {
245        log::debug!(target: "driver", "creating session {name}");
246        if log::log_enabled!(target: "driver", log::Level::Debug) {
247            if let Some(input) = input.as_ref() {
248                log::debug!(
249                    target: "driver",
250                    " | input = {} ({})",
251                    input.file_name(),
252                    input.file_type(),
253                );
254            }
255            log::debug!(
256                target: "driver",
257                " | outputs_dir = {}",
258                options.output_dir
259                    .as_ref()
260                    .map(|p| p.display().to_string())
261                    .unwrap_or("<unset>".to_string())
262            );
263            log::debug!(
264                target: "driver",
265                " | output_file = {}",
266                options.output_file.as_ref().map(|of| of.to_string()).unwrap_or("<unset>".to_string())
267            );
268            log::debug!(target: "driver", " | target_dir = {}", options.target_dir.display());
269        }
270        let diagnostics = Arc::new(DiagnosticsHandler::new(
271            options.diagnostics,
272            source_manager.clone(),
273            emitter.unwrap_or_else(|| options.default_emitter()),
274        ));
275
276        let profile_target_dir = options.target_dir.join(&options.profile);
277        create_target_dir(&profile_target_dir);
278
279        let output_dir = options
280            .output_dir
281            .as_deref()
282            .or_else(|| options.output_file.as_ref().and_then(|of| of.parent()))
283            .map(|path| path.to_path_buf())
284            .unwrap_or_else(|| profile_target_dir.clone());
285        create_target_dir(&output_dir);
286
287        log::debug!(target: "driver", " | output dir = {}", output_dir.display());
288        log::debug!(target: "driver", " | target = {}", options.target_type.map(|tt| tt.to_string()).unwrap_or("none specified".to_string()));
289        if log::log_enabled!(target: "driver", log::Level::Debug) {
290            for lib in options.link_libraries.iter() {
291                if let Some(path) = lib.path.as_deref() {
292                    log::debug!(target: "driver", " | linking library '{}' from {}", lib.name, path.display());
293                } else {
294                    log::debug!(target: "driver", " | linking library '{}'", lib.name);
295                }
296            }
297        }
298
299        let output_files = OutputFiles::new(
300            name.clone(),
301            options.current_dir.clone(),
302            output_dir.clone(),
303            options.output_file.clone(),
304            profile_target_dir.clone(),
305            options.output_types.clone(),
306        );
307
308        // Link against implicitly required libraries
309        let requires_protocol = options.target_requires_protocol();
310        add_target_link_libraries(&mut options.link_libraries, requires_protocol);
311
312        Self {
313            name,
314            options,
315            source_manager,
316            diagnostics,
317            input,
318            output_files,
319            #[cfg(feature = "std")]
320            statistics: Default::default(),
321            #[cfg(feature = "std")]
322            package_cache_lease: Default::default(),
323        }
324    }
325
326    #[doc(hidden)]
327    pub fn with_output_type(mut self, ty: OutputType, path: Option<OutputFile>) -> Self {
328        self.output_files.outputs.insert(ty, path.clone());
329        self.options.output_types.insert(ty, path.clone());
330        self
331    }
332
333    #[doc(hidden)]
334    pub fn with_extra_flags(mut self, flags: CompileFlags) -> Self {
335        self.options.set_extra_flags(flags);
336        self
337    }
338
339    /// Get the value of a custom flag with action `FlagAction::SetTrue` or `FlagAction::SetFalse`
340    #[inline]
341    pub fn get_flag(&self, name: &str) -> bool {
342        self.options.flags.get_flag(name)
343    }
344
345    /// Get the count of a specific custom flag with action `FlagAction::Count`
346    #[inline]
347    pub fn get_flag_count(&self, name: &str) -> usize {
348        self.options.flags.get_flag_count(name)
349    }
350
351    /// Get the remaining [ArgMatches] left after parsing the base session configuration
352    #[inline]
353    pub fn matches(&self) -> &ArgMatches {
354        self.options.flags.matches()
355    }
356
357    /// The name of this session (used as the name of the project, output file, etc.)
358    pub fn name(&self) -> &str {
359        &self.name
360    }
361
362    /// Get a new package registry instance for this session
363    pub fn package_registry(&self) -> Result<Box<registry::HybridPackageRegistry>, Report> {
364        #[cfg(feature = "std")]
365        let filesystem_cache = self.filesystem_package_cache_dir()?;
366        #[cfg(not(feature = "std"))]
367        let filesystem_cache = None;
368        #[allow(unused_mut)]
369        let mut registry = registry::HybridPackageRegistry::new_with_filesystem_cache(
370            &self.options,
371            filesystem_cache,
372        )?;
373        // The registry publishes into the leased directory and may outlive every clone of
374        // this session; retaining the shared lease keeps the directory alive for as long
375        // as the registry is.
376        #[cfg(feature = "std")]
377        registry.retain_session_package_cache(self.package_cache_lease.clone());
378        Ok(Box::new(registry))
379    }
380
381    /// Where compiled dependency packages of this build are published and looked for.
382    ///
383    /// `Ok(None)` unless this session's input is a project locator: a session compiling a
384    /// standalone source file has no project to exchange packages for. When the calling
385    /// process already exported `MIDENC_PACKAGE_CACHE`, that directory is adopted as-is and
386    /// left in place — the caller owns its location and lifetime (this is how a contract
387    /// `build.rs` keeps the packages readable after the compiler exits). Otherwise the
388    /// directory is a per-build lease with a globally unique name under the session's
389    /// configured target directory (`<target-dir>/packages`), created on first access and
390    /// deleted when the last clone of this session drops; see the `package_lease` module for
391    /// both lifecycles. Anchoring at the target directory honors a caller-supplied
392    /// `--target-dir` — a writable location for a read-only checkout, for example.
393    ///
394    /// Both readers — this session's package registry and the nested `cargo` builds a Rust
395    /// project's dependencies run through — must agree on the answer, which is why there is
396    /// one derivation of it. Only the root compilation session derives the path. Nested
397    /// dependency sessions receive the root value threaded through their build environment,
398    /// rather than deriving paths from their own locators: a dependency with a private
399    /// directory could not see its already-assembled transitive dependencies.
400    ///
401    /// [`Session`] is [`Clone`], and clones share the lease cell, so every clone observes the
402    /// same directory and no clone can mint a second one. Errors when the directory cannot be
403    /// created — fail closed, so the build stops here instead of failing later inside a macro
404    /// expansion with a confusing missing-package diagnostic; the failure is memoized and
405    /// re-reported on every access.
406    ///
407    /// Derived from the input locator rather than from a loaded manifest, which is what
408    /// [`Session::new`] no longer has. That is also a repair: the manifest path was previously
409    /// taken from a package that `fixup_cargo_target` had rebuilt for every executable
410    /// `Cargo.toml` input, and a rebuilt package has no manifest path — so an executable project
411    /// silently got no filesystem cache at all, while a library project of the same shape got one.
412    #[cfg(feature = "std")]
413    pub fn filesystem_package_cache_dir(&self) -> Result<Option<PathBuf>, Report> {
414        if !self.is_project_session() {
415            return Ok(None);
416        }
417        let lease = self
418            .package_cache_lease
419            .get_or_init(|| package_lease::PackageCacheLease::create(&self.options.target_dir));
420        match lease {
421            Ok(lease) => Ok(Some(lease.path().to_path_buf())),
422            Err(message) => Err(Report::msg(message.clone())),
423        }
424    }
425
426    /// Without `std` there is no filesystem package exchange.
427    #[cfg(not(feature = "std"))]
428    pub fn filesystem_package_cache_dir(&self) -> Result<Option<PathBuf>, Report> {
429        Ok(None)
430    }
431
432    /// Whether this session compiles a project — the gate for having a package cache at all.
433    ///
434    /// Mirrors [`Session::filesystem_package_cache_dir`]: a session compiling a standalone
435    /// source file has no project to exchange packages for. The cache itself is anchored at
436    /// the configured target directory.
437    #[cfg(feature = "std")]
438    fn is_project_session(&self) -> bool {
439        self.input
440            .as_ref()
441            .is_some_and(|input| matches!(input.file_type(), FileType::Toml))
442    }
443
444    /// Get the [OutputFile] to write the assembled MAST output to
445    pub fn out_file(&self) -> OutputFile {
446        let out_file = self.output_files.output_file(OutputType::Masp, None);
447
448        if let OutputFile::Real(ref path) = out_file {
449            self.check_file_is_writeable(path);
450        }
451
452        out_file
453    }
454
455    #[cfg(not(feature = "std"))]
456    fn check_file_is_writeable(&self, file: &Path) {
457        panic!(
458            "Compiler exited with a fatal error: cannot write '{}' - compiler was built without \
459             standard library",
460            file.display()
461        );
462    }
463
464    #[cfg(feature = "std")]
465    fn check_file_is_writeable(&self, file: &Path) {
466        if let Ok(m) = file.metadata()
467            && m.permissions().readonly()
468        {
469            panic!("Compiler exited with a fatal error: file is not writeable: {}", file.display());
470        }
471    }
472
473    /// Returns true if the compiler should exit after parsing the input
474    pub fn parse_only(&self) -> bool {
475        self.options.parse_only
476    }
477
478    /// Returns true if the compiler should exit after performing semantic analysis
479    pub fn analyze_only(&self) -> bool {
480        self.options.analyze_only
481    }
482
483    /// Returns true if the compiler should exit after applying rewrites to the IR
484    pub fn rewrite_only(&self) -> bool {
485        let link_or_masm_requested = self.should_link() || self.should_codegen();
486        !self.options.parse_only && !self.options.analyze_only && !link_or_masm_requested
487    }
488
489    /// Returns true if an [OutputType] that requires linking + assembly was requested
490    pub fn should_link(&self) -> bool {
491        self.options.output_types.should_link() && !self.options.no_link
492    }
493
494    /// Returns true if an [OutputType] that requires generating Miden Assembly was requested
495    pub fn should_codegen(&self) -> bool {
496        self.options.output_types.should_codegen() && !self.options.link_only
497    }
498
499    /// Returns true if an [OutputType] that requires assembling MAST was requested
500    pub fn should_assemble(&self) -> bool {
501        self.options.output_types.should_assemble() && !self.options.link_only
502    }
503
504    /// Returns true if the given [OutputType] should be emitted as an output
505    pub fn should_emit(&self, ty: OutputType) -> bool {
506        self.options.output_types.contains_key(&ty)
507    }
508
509    /// Returns true if IR should be printed to stdout, after executing a pass named `pass`
510    pub fn should_print_ir(&self, pass: &str) -> bool {
511        self.options.print_ir_after_all
512            || self.options.print_ir_after_pass.iter().any(|p| p == pass)
513    }
514
515    /// Returns true if IR should be printed to stdout, at the start of `stage`
516    pub fn should_print_ir_before_stage(&self, stage: &str) -> bool {
517        self.options.print_ir_before_stage.iter().any(|s| s == stage)
518    }
519
520    /// Returns true if CFG should be printed to stdout, after executing a pass named `pass`
521    pub fn should_print_cfg(&self, pass: &str) -> bool {
522        self.options.print_cfg_after_all
523            || self.options.print_cfg_after_pass.iter().any(|p| p == pass)
524    }
525
526    /// Print the given emittable IR to stdout, as produced by a pass with name `pass`
527    #[cfg(feature = "std")]
528    pub fn print(&self, ir: impl Emit, pass: &str) -> anyhow::Result<()> {
529        if self.should_print_ir(pass) {
530            ir.write_to_stdout(self)?;
531        }
532        Ok(())
533    }
534
535    /// Get the path to emit the given [OutputType] to
536    pub fn emit_to(&self, ty: OutputType, name: Option<Symbol>) -> Option<PathBuf> {
537        if self.should_emit(ty) {
538            match self.output_files.output_file(ty, name.map(|n| n.as_str())) {
539                OutputFile::Real(path) => Some(path),
540                OutputFile::Directory(_) => {
541                    unreachable!("OutputFiles::output_file never returns OutputFile::Directory")
542                }
543                OutputFile::Stdout => None,
544            }
545        } else {
546            None
547        }
548    }
549
550    /// Emit an item to stdout/file system depending on the current configuration
551    #[cfg(feature = "std")]
552    pub fn emit<E: Emit>(&self, mode: OutputMode, item: &E) -> anyhow::Result<()> {
553        let output_type = item.output_type(mode);
554        let name = item.name().map(|n| n.as_str());
555        match self.output_path_for(output_type, name) {
556            Some(OutputFile::Real(path)) => {
557                item.write_to_file(&path, mode, self)?;
558            }
559            Some(OutputFile::Directory(_)) => {
560                unreachable!("OutputFiles::output_file never returns OutputFile::Directory")
561            }
562            Some(OutputFile::Stdout) => {
563                let stdout = std::io::stdout().lock();
564                item.write_to(stdout, mode, self)?;
565            }
566            None => (),
567        }
568
569        Ok(())
570    }
571
572    /// Given an [OutputType] and an optional name, return the output file path that would be
573    /// written to.
574    ///
575    /// Returns `Some` if the specified output type should be emitted, and `None` if it should not.
576    #[cfg(feature = "std")]
577    pub fn output_path_for(
578        &self,
579        output_type: OutputType,
580        name: Option<&str>,
581    ) -> Option<OutputFile> {
582        if self.should_emit(output_type) {
583            Some(self.output_files.output_file(output_type, name))
584        } else {
585            None
586        }
587    }
588
589    #[cfg(not(feature = "std"))]
590    pub fn emit<E: Emit>(&self, _mode: OutputMode, _item: &E) -> anyhow::Result<()> {
591        Ok(())
592    }
593}
594
595fn is_cargo_project_input(input: &InputFile) -> bool {
596    matches!(
597        &input.file,
598        InputType::Real(path) if path.file_name().is_some_and(|name| name.eq_ignore_ascii_case("Cargo.toml"))
599    )
600}
601
602/// What a project's manifest says about the targets it declares.
603///
604/// The facts are read with `miden_project`'s own extractors — the very ones
605/// `miden_project::Package::parse` uses — so that a target's kind, a target's defaulted name and
606/// the package's name mean here exactly what they mean to a loaded project. None of them is
607/// inheritable from a workspace, which is what makes reading the package manifest alone correct:
608/// `[package] name` is a required key of the package's own file, `[lib] kind` defaults to
609/// `library` there, and a `[[bin]]` with no name takes the package's.
610///
611/// # This is the one place the rules live
612///
613/// Two questions are answered from these facts — [what target type a project builds by
614/// default](Self::library_target_type) and [which executable it builds](Self::selected_executable)
615/// — and both have to be answered identically everywhere, because the answers decide different
616/// halves of one build. `Session::new` uses them to set [`Options::target_type`] and
617/// [`Options::entrypoint`]; the Rust frontend's nested `cargo` build uses the second to reject a
618/// project it could not build, and used to carry its own copy of both rules over a separately
619/// loaded project. Two implementations of one rule can only ever agree by coincidence.
620///
621/// `read` parses a manifest for them, and [`from_package`](Self::from_package) takes them off a
622/// project that is already loaded. That is the whole difference between the callers: where the
623/// facts come from, never what is done with them.
624pub struct ProjectManifest {
625    /// The `[package] name`.
626    name: String,
627    /// The declared library target, if the manifest declares one.
628    library: Option<miden_project::Target>,
629    /// The declared executable targets, with names defaulted to the package's.
630    executables: alloc::vec::Vec<miden_project::Target>,
631}
632
633impl ProjectManifest {
634    /// Take these facts off an already-loaded `package`.
635    ///
636    /// For a caller that has a [`miden_project::Package`] in hand and must not load a second one
637    /// — either because it just loaded that one, or because it came from a workspace and was
638    /// never a file of its own.
639    pub fn from_package(package: &miden_project::Package) -> Self {
640        Self {
641            name: package.name().to_string(),
642            library: package.library_target().map(|lib| lib.inner().clone()),
643            executables: package
644                .executable_targets()
645                .iter()
646                .map(|bin| bin.inner().clone())
647                .collect(),
648        }
649    }
650
651    /// The target type a project declaring these targets builds when nothing selects one.
652    ///
653    /// The library target's kind if there is a library target, and otherwise an executable —
654    /// which is what a package declaring only `[[bin]]`s is.
655    pub fn library_target_type(&self) -> miden_project::TargetType {
656        match self.library.as_ref() {
657            Some(library) => library.ty,
658            None => miden_project::TargetType::Executable,
659        }
660    }
661
662    /// The executable target this build compiles, of the ones declared.
663    ///
664    /// `requested` is `--target`, which names one outright. Without it there must be exactly one
665    /// to choose, because nothing else distinguishes them: a package declaring several says which
666    /// it means, or is asked to.
667    pub fn selected_executable(
668        &self,
669        requested: Option<&str>,
670    ) -> Result<&miden_project::Target, Report> {
671        match requested {
672            Some(name) => self
673                .executables
674                .iter()
675                .find(|target| name == &**target.name.inner())
676                .ok_or_else(|| Report::msg(format!("no executable target name '{name}'"))),
677            None if self.executables.len() == 1 => Ok(&self.executables[0]),
678            None => Err(Report::msg(
679                "ambiguous executable target selection: use --target to select a specific \
680                 executable target",
681            )),
682        }
683    }
684
685    /// Read the manifest the project locator `input` names.
686    ///
687    /// `Ok(None)` means the manifest could not be read or is not a package manifest, which is not
688    /// an error here — see [`Session::new`] for why, and for what a session does instead. A
689    /// locator piped in on standard input is the one exception: nothing downstream re-reads it, so
690    /// there is no better place for its diagnostic than this one.
691    fn read(input: &InputFile, source_manager: &dyn SourceManager) -> Result<Option<Self>, Report> {
692        match &input.file {
693            InputType::Real(path) => {
694                // The same normalization `normalize_locator` performs in `midenc-compile`: a
695                // `Cargo.toml` locates the `miden-project.toml` beside it, which is where
696                // `cargo miden` writes the Miden manifest for a crate.
697                let manifest_path =
698                    if path.file_name().is_some_and(|name| name.eq_ignore_ascii_case("Cargo.toml"))
699                    {
700                        path.with_file_name("miden-project.toml")
701                    } else {
702                        path.clone()
703                    };
704                #[cfg(feature = "std")]
705                {
706                    use miden_debug_types::SourceManagerExt;
707                    let Ok(source) = source_manager.load_file(&manifest_path) else {
708                        return Ok(None);
709                    };
710                    Ok(Self::parse(source).ok())
711                }
712                #[cfg(not(feature = "std"))]
713                {
714                    let _ = manifest_path;
715                    Ok(None)
716                }
717            }
718            InputType::Stdin { name, input } => {
719                let content = core::str::from_utf8(input).map_err(|err| {
720                    Report::msg(format!(
721                        "unable to load source file '{name}' due to invalid utf-8: {err}"
722                    ))
723                })?;
724                let source_file = source_manager.load(
725                    miden_debug_types::SourceLanguage::Other("toml"),
726                    miden_debug_types::Uri::new(name.as_str()),
727                    content.to_string(),
728                );
729                Self::parse(source_file).map(Some)
730            }
731        }
732    }
733
734    fn parse(source: Arc<diagnostics::SourceFile>) -> Result<Self, Report> {
735        let package = match miden_project::ast::MidenProject::parse(source)? {
736            miden_project::ast::MidenProject::Package(package) => package,
737            // A workspace manifest declares members but no package of its own, so it names
738            // nothing to derive an artifact name or a target type from. Which member was meant
739            // has to come from the caller, and saying so is the job of whoever resolves the
740            // locator; there is nothing for a session to do with one.
741            miden_project::ast::MidenProject::Workspace(_) => {
742                return Err(Report::msg(
743                    "expected a package manifest, but found a workspace manifest",
744                ));
745            }
746        };
747        // The spans are dropped: every diagnostic these facts can provoke is raised against the
748        // manifest downstream, by whoever loads it, and none of them is raised here.
749        use miden_debug_types::Span;
750        Ok(Self {
751            name: package.package.name.inner().to_string(),
752            library: package.extract_library_target()?.map(Span::into_inner),
753            executables: package
754                .extract_executable_targets()
755                .into_iter()
756                .map(Span::into_inner)
757                .collect(),
758        })
759    }
760}
761
762fn infer_cargo_project_entrypoint(
763    manifest: &ProjectManifest,
764    options: &mut Options,
765) -> Result<(), Report> {
766    if options.entrypoint.is_some() {
767        return Ok(());
768    }
769
770    match options.target_type {
771        Some(miden_project::TargetType::Executable) => {
772            let target = manifest.selected_executable(options.target.as_deref())?;
773            let masm_module_name = target.name.inner().replace('-', "_");
774            options.entrypoint = Some(format!("{masm_module_name}::entrypoint"));
775        }
776        Some(miden_project::TargetType::TransactionScript) => {
777            options.entrypoint = Some("miden:base/transaction-script@1.0.0::run".to_string());
778        }
779        _ => (),
780    }
781
782    Ok(())
783}
784
785#[cfg(feature = "std")]
786fn create_target_dir(path: &Path) {
787    if !path.exists() {
788        std::fs::create_dir_all(path).unwrap_or_else(|err| {
789            panic!("unable to create --target-dir '{}': {err}", path.display())
790        });
791    }
792}
793
794#[cfg(not(feature = "std"))]
795fn create_target_dir(_path: &Path) {}
796
797#[cfg(test)]
798mod tests {
799    use alloc::sync::Arc;
800
801    use tempfile::TempDir;
802
803    use super::*;
804
805    #[test]
806    fn relative_manifest_locator_uses_the_configured_current_directory() {
807        let temp = TempDir::new().unwrap();
808        let options = Options {
809            current_dir: temp.path().to_path_buf(),
810            target_dir: temp.path().join("target"),
811            ..Options::default()
812        };
813        let input = InputFile::new(FileType::Toml, InputType::Real("Cargo.toml".into()));
814        let session = Session::new_project(
815            "relative-manifest".into(),
816            Some(input),
817            Box::new(options),
818            None,
819            Arc::new(diagnostics::DefaultSourceManager::default()),
820        );
821
822        let cache_dir = session.filesystem_package_cache_dir().unwrap().unwrap();
823        // The lease is anchored at the configured target directory, honoring `--target-dir`.
824        let expected_parent = temp.path().join("target/packages");
825        assert_eq!(cache_dir.parent(), Some(expected_parent.as_path()));
826        assert!(cache_dir.is_dir(), "the lease directory must exist once derived");
827
828        let clone_dir = session.clone().filesystem_package_cache_dir().unwrap().unwrap();
829        assert_eq!(cache_dir, clone_dir, "clones must share one lease, never mint a second");
830
831        drop(session);
832        assert!(!cache_dir.exists(), "dropping the last session must delete the lease");
833    }
834
835    #[test]
836    fn a_registry_keeps_the_leased_cache_alive_after_the_session_drops() {
837        let temp = TempDir::new().unwrap();
838        let options = Options {
839            current_dir: temp.path().to_path_buf(),
840            target_dir: temp.path().join("target"),
841            ..Options::default()
842        };
843        let input = InputFile::new(FileType::Toml, InputType::Real("Cargo.toml".into()));
844        let session = Session::new_project(
845            "registry-outlives".into(),
846            Some(input),
847            Box::new(options),
848            None,
849            Arc::new(diagnostics::DefaultSourceManager::default()),
850        );
851
852        let registry = session.package_registry().unwrap();
853        let cache_dir = registry.filesystem_cache_dir().unwrap().to_path_buf();
854        assert!(cache_dir.is_dir());
855
856        drop(session);
857        assert!(
858            cache_dir.is_dir(),
859            "the registry publishes into the leased directory, so it must keep the lease alive"
860        );
861
862        drop(registry);
863        assert!(!cache_dir.exists(), "dropping the last owner must delete the lease");
864    }
865}