Skip to main content

leo_package/
package.rs

1// Copyright (C) 2019-2026 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Leo library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17use crate::*;
18
19use leo_ast::DiGraph;
20use leo_errors::Result;
21use leo_span::Symbol;
22
23use indexmap::{IndexMap, map::Entry};
24use snarkvm::prelude::anyhow;
25use std::path::{Path, PathBuf};
26
27/// Either the bytecode of an Aleo program (if it was a network dependency) or
28/// a path to its source (if it was local).
29#[derive(Clone, Debug)]
30pub enum ProgramData {
31    Bytecode(String),
32    /// For a local dependency, `directory` is the directory of the package
33    /// For a test dependency, `directory` is the directory of the test file.
34    SourcePath {
35        directory: PathBuf,
36        source: PathBuf,
37    },
38}
39
40/// A Leo package.
41#[derive(Clone, Debug)]
42pub struct Package {
43    /// The directory on the filesystem where the package is located, canonicalized.
44    pub base_directory: PathBuf,
45
46    /// A topologically sorted list of all compilation units in this package, whether
47    /// dependencies or the main program.
48    ///
49    /// Any unit's dependent unit will appear before it, so that compiling
50    /// them in order should give access to all stubs necessary to compile each
51    /// compilation unit.
52    pub compilation_units: Vec<CompilationUnit>,
53
54    /// The manifest file of this package.
55    pub manifest: Manifest,
56
57    /// The dependency graph of the package.
58    pub dep_graph: DiGraph<Symbol>,
59}
60
61impl Package {
62    /// The root of the build directory.
63    ///
64    /// This is the single place that knows where build artifacts are rooted;
65    /// every per-unit path below is composed from it.
66    pub fn build_directory(&self) -> PathBuf {
67        self.base_directory.join(BUILD_DIRECTORY)
68    }
69
70    /// The package's own compilation unit, identified via the manifest.
71    /// Robust under `--build-tests` (unlike `compilation_units.last()`).
72    pub fn primary_unit(&self) -> Option<&CompilationUnit> {
73        let primary = bare_unit_name(&self.manifest.program);
74        self.compilation_units.iter().find(|u| !u.kind.is_test() && bare_unit_name(&u.name.to_string()) == primary)
75    }
76
77    /// The `build/<name>/` directory for a single compilation unit - a program,
78    /// library, or test - whether it is this package's own unit, a local
79    /// dependency, or a fetched network import.
80    pub fn unit_build_directory(&self, name: &str) -> PathBuf {
81        self.build_directory().join(bare_unit_name(name))
82    }
83
84    /// Path to a unit's compiled Aleo bytecode: `build/<name>/<name>.aleo`.
85    /// Only programs and tests produce bytecode; libraries do not.
86    pub fn unit_bytecode_path(&self, name: &str) -> PathBuf {
87        let bare = bare_unit_name(name);
88        self.unit_build_directory(name).join(format!("{bare}.aleo"))
89    }
90
91    /// Path to a unit's Leo ABI: `build/<name>/abi.json`.
92    pub fn unit_abi_path(&self, name: &str) -> PathBuf {
93        self.unit_build_directory(name).join(ABI_FILENAME)
94    }
95
96    /// Path to a unit's interface ABI directory: `build/<name>/interfaces/`.
97    /// Both programs and libraries can declare interfaces.
98    pub fn unit_interfaces_directory(&self, name: &str) -> PathBuf {
99        self.unit_build_directory(name).join(INTERFACES_DIRNAME)
100    }
101
102    /// Path to a unit's AST-snapshot directory: `build/<name>/snapshots/`.
103    /// Populated only when a snapshot CLI flag is set; created lazily by the
104    /// compiler on the first write, so absent on builds that don't request snapshots.
105    pub fn unit_snapshots_directory(&self, name: &str) -> PathBuf {
106        self.unit_build_directory(name).join(SNAPSHOTS_DIRNAME)
107    }
108
109    pub fn source_directory(&self) -> PathBuf {
110        self.base_directory.join(SOURCE_DIRECTORY)
111    }
112
113    pub fn tests_directory(&self) -> PathBuf {
114        self.base_directory.join(TESTS_DIRECTORY)
115    }
116
117    /// Create a Leo package by the name `package_name` in a subdirectory of `path`.
118    pub fn initialize<P: AsRef<Path>>(package_name: &str, path: P, is_library: bool) -> Result<PathBuf> {
119        Self::initialize_impl(package_name, path.as_ref(), is_library)
120    }
121
122    fn initialize_impl(package_name: &str, path: &Path, is_library: bool) -> Result<PathBuf> {
123        let package_name = if is_library {
124            if !crate::is_valid_library_name(package_name) {
125                return Err(crate::errors::cli_invalid_package_name("library", package_name).into());
126            }
127
128            package_name.to_string()
129        } else {
130            let program_name =
131                if package_name.ends_with(".aleo") { package_name.to_string() } else { format!("{package_name}.aleo") };
132
133            if !crate::is_valid_program_name(&program_name) {
134                return Err(crate::errors::cli_invalid_package_name("program", &program_name).into());
135            }
136
137            program_name
138        };
139
140        let path = path.canonicalize().map_err(|e| crate::errors::failed_path(path.display(), e))?;
141        let full_path = path.join(package_name.strip_suffix(".aleo").unwrap_or(&package_name));
142
143        // Verify that there is no existing directory at the path.
144        if full_path.exists() {
145            return Err(
146                crate::errors::failed_to_initialize_package(package_name, &path, "Directory already exists").into()
147            );
148        }
149
150        // Create the package directory.
151        std::fs::create_dir(&full_path)
152            .map_err(|e| crate::errors::failed_to_initialize_package(&package_name, &full_path, e))?;
153
154        // Change the current working directory to the package directory.
155        std::env::set_current_dir(&full_path)
156            .map_err(|e| crate::errors::failed_to_initialize_package(&package_name, &full_path, e))?;
157
158        // Create .gitignore
159        const GITIGNORE_TEMPLATE: &str = ".env\n*.avm\n*.prover\n*.verifier\nbuild/\n";
160        const GITIGNORE_FILENAME: &str = ".gitignore";
161
162        let gitignore_path = full_path.join(GITIGNORE_FILENAME);
163        std::fs::write(gitignore_path, GITIGNORE_TEMPLATE).map_err(crate::errors::io_error_gitignore_file)?;
164
165        // Create manifest
166        let manifest = Manifest {
167            program: package_name.clone(),
168            version: "0.1.0".to_string(),
169            description: String::new(),
170            license: "MIT".to_string(),
171            leo: env!("CARGO_PKG_VERSION").to_string(),
172            dependencies: None,
173            dev_dependencies: None,
174        };
175
176        let manifest_path = full_path.join(MANIFEST_FILENAME);
177        manifest.write_to_file(manifest_path)?;
178
179        // Create src/
180        let source_path = full_path.join(SOURCE_DIRECTORY);
181
182        std::fs::create_dir(&source_path)
183            .map_err(|e| crate::errors::failed_to_create_source_directory(source_path.display(), e))?;
184
185        let name_no_aleo = package_name.strip_suffix(".aleo").unwrap_or(&package_name);
186
187        if is_library {
188            // Create lib.leo with a placeholder function.
189            let lib_path = source_path.join("lib.leo");
190
191            std::fs::write(&lib_path, lib_template(name_no_aleo)).map_err(|e| {
192                crate::errors::util_file_io_error(format_args!("Failed to write `{}`", lib_path.display()), e)
193            })?;
194
195            // Create tests directory with a starter test file.
196            let tests_path = full_path.join(TESTS_DIRECTORY);
197
198            std::fs::create_dir(&tests_path)
199                .map_err(|e| crate::errors::failed_to_create_source_directory(tests_path.display(), e))?;
200
201            let test_file_path = tests_path.join(format!("test_{name_no_aleo}.leo"));
202
203            std::fs::write(&test_file_path, lib_test_template(name_no_aleo)).map_err(|e| {
204                crate::errors::util_file_io_error(format_args!("Failed to write `{}`", test_file_path.display()), e)
205            })?;
206        } else {
207            // Create main.leo
208            let main_path = source_path.join(MAIN_FILENAME);
209
210            std::fs::write(&main_path, main_template(name_no_aleo)).map_err(|e| {
211                crate::errors::util_file_io_error(format_args!("Failed to write `{}`", main_path.display()), e)
212            })?;
213
214            // Create tests directory
215            let tests_path = full_path.join(TESTS_DIRECTORY);
216
217            std::fs::create_dir(&tests_path)
218                .map_err(|e| crate::errors::failed_to_create_source_directory(tests_path.display(), e))?;
219
220            let test_file_path = tests_path.join(format!("test_{name_no_aleo}.leo"));
221
222            std::fs::write(&test_file_path, test_template(name_no_aleo)).map_err(|e| {
223                crate::errors::util_file_io_error(format_args!("Failed to write `{}`", test_file_path.display()), e)
224            })?;
225        }
226
227        Ok(full_path)
228    }
229
230    /// Examine the Leo package at `path` to create a `Package`, but don't find dependencies.
231    ///
232    /// This may be useful if you just need other information like the manifest file.
233    pub fn from_directory_no_graph<P: AsRef<Path>, Q: AsRef<Path>>(
234        path: P,
235        home_path: Q,
236        network: Option<NetworkName>,
237        endpoint: Option<&str>,
238        network_retries: u32,
239    ) -> Result<Self> {
240        Self::from_directory_impl(
241            path.as_ref(),
242            home_path.as_ref(),
243            /* build_graph */ false,
244            /* with_tests */ false,
245            /* no_cache */ false,
246            /* no_local */ false,
247            network,
248            endpoint,
249            network_retries,
250        )
251    }
252
253    /// Examine the Leo package at `path` to create a `Package`, including all its dependencies,
254    /// obtaining dependencies from the file system or network and topologically sorting them.
255    pub fn from_directory<P: AsRef<Path>, Q: AsRef<Path>>(
256        path: P,
257        home_path: Q,
258        no_cache: bool,
259        no_local: bool,
260        network: Option<NetworkName>,
261        endpoint: Option<&str>,
262        network_retries: u32,
263    ) -> Result<Self> {
264        Self::from_directory_impl(
265            path.as_ref(),
266            home_path.as_ref(),
267            /* build_graph */ true,
268            /* with_tests */ false,
269            no_cache,
270            no_local,
271            network,
272            endpoint,
273            network_retries,
274        )
275    }
276
277    /// Examine the Leo package at `path` to create a `Package`, including all its dependencies
278    /// and its tests, obtaining dependencies from the file system or network and topologically sorting them.
279    pub fn from_directory_with_tests<P: AsRef<Path>, Q: AsRef<Path>>(
280        path: P,
281        home_path: Q,
282        no_cache: bool,
283        no_local: bool,
284        network: Option<NetworkName>,
285        endpoint: Option<&str>,
286        network_retries: u32,
287    ) -> Result<Self> {
288        Self::from_directory_impl(
289            path.as_ref(),
290            home_path.as_ref(),
291            /* build_graph */ true,
292            /* with_tests */ true,
293            no_cache,
294            no_local,
295            network,
296            endpoint,
297            network_retries,
298        )
299    }
300
301    pub fn test_files(&self) -> impl Iterator<Item = PathBuf> {
302        let path = self.tests_directory();
303        // This allocation isn't ideal but it's not performance critical and
304        // easily resolves lifetime issues.
305        let data: Vec<PathBuf> = Self::files_with_extension(&path, "leo").collect();
306        data.into_iter()
307    }
308
309    fn files_with_extension(path: &Path, extension: &'static str) -> impl Iterator<Item = PathBuf> {
310        path.read_dir()
311            .ok()
312            .into_iter()
313            .flatten()
314            .flat_map(|maybe_filename| maybe_filename.ok())
315            .filter(|entry| entry.file_type().ok().map(|filetype| filetype.is_file()).unwrap_or(false))
316            .flat_map(move |entry| {
317                let path = entry.path();
318                if path.extension().is_some_and(|e| e == extension) { Some(path) } else { None }
319            })
320    }
321
322    #[allow(clippy::too_many_arguments)]
323    fn from_directory_impl(
324        path: &Path,
325        home_path: &Path,
326        build_graph: bool,
327        with_tests: bool,
328        no_cache: bool,
329        no_local: bool,
330        network: Option<NetworkName>,
331        endpoint: Option<&str>,
332        network_retries: u32,
333    ) -> Result<Self> {
334        let map_err = |path: &Path, err| {
335            crate::errors::util_file_io_error(format_args!("Trying to find path at {}", path.display()), err)
336        };
337
338        let path = path.canonicalize().map_err(|err| map_err(path, err))?;
339
340        let manifest = Manifest::read_from_file(path.join(MANIFEST_FILENAME))?;
341
342        let (compilation_units, digraph) = if build_graph {
343            let home_path = home_path.canonicalize().map_err(|err| map_err(home_path, err))?;
344
345            let mut map: IndexMap<Symbol, (Dependency, CompilationUnit)> = IndexMap::new();
346
347            let mut digraph = DiGraph::<Symbol>::new(Default::default());
348
349            // Pre-collect all declared dependencies from the manifest tree so that
350            // .aleo file import classification doesn't depend on processing order.
351            let declared_deps = collect_declared_deps(&path, &manifest, with_tests)?;
352
353            let first_dependency = Dependency {
354                name: manifest.program.clone(),
355                location: Location::Local,
356                path: Some(path.clone()),
357                edition: None,
358            };
359
360            let test_dependencies: Vec<Dependency> = if with_tests {
361                let tests_directory = path.join(TESTS_DIRECTORY);
362                let mut test_dependencies: Vec<Dependency> = Self::files_with_extension(&tests_directory, "leo")
363                    .map(|path| Dependency {
364                        // We just made sure it has a ".leo" extension.
365                        name: format!("{}.aleo", crate::filename_no_leo_extension(&path).unwrap()),
366                        edition: None,
367                        location: Location::Test,
368                        path: Some(path.to_path_buf()),
369                    })
370                    .collect();
371                if let Some(deps) = manifest.dev_dependencies.as_ref() {
372                    test_dependencies.extend(deps.iter().cloned());
373                }
374                test_dependencies
375            } else {
376                Vec::new()
377            };
378
379            for dependency in test_dependencies.into_iter().chain(std::iter::once(first_dependency.clone())) {
380                Self::graph_build(
381                    &home_path,
382                    network,
383                    endpoint,
384                    &first_dependency,
385                    dependency,
386                    &mut map,
387                    &mut digraph,
388                    no_cache,
389                    no_local,
390                    network_retries,
391                    &declared_deps,
392                )?;
393            }
394
395            let ordered_dependency_symbols =
396                digraph.post_order().map_err(|_| crate::errors::circular_dependency_error())?;
397
398            (
399                ordered_dependency_symbols.into_iter().map(|symbol| map.swap_remove(&symbol).unwrap().1).collect(),
400                digraph,
401            )
402        } else {
403            (Vec::new(), DiGraph::default())
404        };
405
406        Ok(Package { base_directory: path, compilation_units, manifest, dep_graph: digraph })
407    }
408
409    #[allow(clippy::too_many_arguments)]
410    fn graph_build(
411        home_path: &Path,
412        network: Option<NetworkName>,
413        endpoint: Option<&str>,
414        main_program: &Dependency,
415        new: Dependency,
416        map: &mut IndexMap<Symbol, (Dependency, CompilationUnit)>,
417        graph: &mut DiGraph<Symbol>,
418        no_cache: bool,
419        no_local: bool,
420        network_retries: u32,
421        declared_deps: &IndexMap<Symbol, Dependency>,
422    ) -> Result<()> {
423        let name_symbol = symbol(&new.name)?;
424
425        let unit = match map.entry(name_symbol) {
426            Entry::Occupied(occupied) => {
427                // We've already visited this dependency. Just make sure it's compatible with
428                // the one we already have.
429                let existing_dep = &occupied.get().0;
430                assert_eq!(new.name, existing_dep.name);
431                if new.location != existing_dep.location
432                    || new.path != existing_dep.path
433                    || new.edition != existing_dep.edition
434                {
435                    return Err(crate::errors::conflicting_dependency(existing_dep, new).into());
436                }
437                return Ok(());
438            }
439            Entry::Vacant(vacant) => {
440                let unit = match (new.path.as_ref(), new.location) {
441                    (Some(path), Location::Local) if !no_local => {
442                        // It's a local dependency.
443                        if path.extension().and_then(|p| p.to_str()) == Some("aleo") && path.is_file() {
444                            CompilationUnit::from_aleo_path(name_symbol, path, declared_deps)?
445                        } else {
446                            CompilationUnit::from_package_path(name_symbol, path)?
447                        }
448                    }
449                    (Some(path), Location::Test) => {
450                        // It's a test dependency - the path points to the source file,
451                        // not a package.
452                        CompilationUnit::from_test_path(path, main_program.clone())?
453                    }
454                    (_, Location::Network) | (Some(_), Location::Local) => {
455                        // It's a network dependency.
456                        let Some(endpoint) = endpoint else {
457                            return Err(anyhow!("An endpoint must be provided to fetch network dependencies.").into());
458                        };
459                        let Some(network) = network else {
460                            return Err(anyhow!("A network must be provided to fetch network dependencies.").into());
461                        };
462                        CompilationUnit::fetch(
463                            name_symbol,
464                            new.edition,
465                            home_path,
466                            network,
467                            endpoint,
468                            no_cache,
469                            network_retries,
470                        )?
471                    }
472                    (_, Location::Workspace) => {
473                        return Err(anyhow!(
474                            "Workspace dependency `{}` was not resolved before graph building. This is a compiler bug.",
475                            new.name
476                        )
477                        .into());
478                    }
479                    _ => return Err(anyhow!("Invalid dependency data for {} (path must be given).", new.name).into()),
480                };
481
482                vacant.insert((new, unit.clone()));
483
484                unit
485            }
486        };
487
488        graph.add_node(name_symbol);
489
490        for dependency in unit.dependencies.iter() {
491            let dependency_symbol = symbol(&dependency.name)?;
492            graph.add_edge(name_symbol, dependency_symbol);
493            Self::graph_build(
494                home_path,
495                network,
496                endpoint,
497                main_program,
498                dependency.clone(),
499                map,
500                graph,
501                no_cache,
502                no_local,
503                network_retries,
504                declared_deps,
505            )?;
506        }
507
508        Ok(())
509    }
510}
511
512fn main_template(name: &str) -> String {
513    format!(
514        r#"// The '{name}' program.
515program {name}.aleo {{
516    // This is the constructor for the program.
517    // The constructor allows you to manage program upgrades.
518    // It is called when the program is deployed or upgraded.
519    // It is currently configured to **prevent** upgrades.
520    // Other configurations include:
521    //  - @admin(address="aleo1...")
522    //  - @checksum(mapping="credits.aleo/fixme", key="0field")
523    //  - @custom
524    // For more information, please refer to the documentation: `https://docs.leo-lang.org/guides/upgradability`
525    @noupgrade
526    constructor() {{}}
527
528    fn main(public a: u32, b: u32) -> u32 {{
529        let c: u32 = a + b;
530        return c;
531    }}
532}}
533"#
534    )
535}
536
537fn test_template(name: &str) -> String {
538    format!(
539        r#"// The 'test_{name}' test program.
540import {name}.aleo;
541program test_{name}.aleo {{
542    @test
543    @should_fail
544    fn test_main_fails() {{
545        let result: u32 = {name}.aleo::main(2u32, 3u32);
546        assert_eq(result, 3u32);
547    }}
548
549    @noupgrade
550    constructor() {{}}
551}}
552"#
553    )
554}
555
556fn lib_template(name: &str) -> String {
557    format!(
558        r#"// The '{name}' library.
559
560// Returns the identity of x.
561fn example(x: u32) -> u32 {{
562    return x;
563}}
564"#
565    )
566}
567
568fn lib_test_template(name: &str) -> String {
569    format!(
570        r#"// The 'test_{name}' test program.
571program test_{name}.aleo {{
572    @test
573    fn test_example() {{
574        assert_eq({name}::example(42u32), 42u32);
575    }}
576
577    @noupgrade
578    constructor() {{}}
579}}
580"#
581    )
582}
583
584/// Walk the manifest tree and collect all declared dependencies.
585///
586/// This gives `parse_dependencies_from_aleo` full knowledge of which programs are
587/// declared as local dependencies, regardless of the order they appear in the manifest.
588/// Without this, `.aleo` file imports are classified against a snapshot of
589/// already-processed dependencies, requiring the user to list them in topological order.
590fn collect_declared_deps(
591    root_path: &Path,
592    manifest: &Manifest,
593    with_tests: bool,
594) -> Result<IndexMap<Symbol, Dependency>> {
595    let mut declared = IndexMap::new();
596    collect_declared_deps_recursive(root_path, manifest, with_tests, &mut declared)?;
597    Ok(declared)
598}
599
600fn collect_declared_deps_recursive(
601    base_path: &Path,
602    manifest: &Manifest,
603    include_dev: bool,
604    declared: &mut IndexMap<Symbol, Dependency>,
605) -> Result<()> {
606    let deps = manifest.dependencies.iter().flatten();
607    let dev: Vec<&Dependency> =
608        if include_dev { manifest.dev_dependencies.iter().flatten().collect() } else { Vec::new() };
609    for dep in deps.chain(dev) {
610        let dep = canonicalize_dependency_path_relative_to(base_path, dep.clone())?;
611        // Resolve workspace deps early - converts to Location::Local with an absolute path.
612        let dep = if dep.location == Location::Workspace { resolve_workspace_dependency(base_path, dep)? } else { dep };
613        let sym = symbol(&dep.name)?;
614        // Only recurse into newly discovered dependencies to avoid infinite
615        // recursion on circular manifests (cycles are caught later by
616        // `DiGraph::post_order`).
617        let Entry::Vacant(e) = declared.entry(sym) else {
618            continue;
619        };
620        e.insert(dep.clone());
621        if dep.location == Location::Local
622            && let Some(path) = &dep.path
623        {
624            let manifest_path = path.join(MANIFEST_FILENAME);
625            if path.is_dir() && manifest_path.exists() {
626                let child = Manifest::read_from_file(manifest_path)?;
627                // dev_dependencies are not transitive.
628                collect_declared_deps_recursive(path, &child, false, declared)?;
629            }
630        }
631    }
632    Ok(())
633}
634
635#[cfg(test)]
636mod tests {
637    use super::*;
638
639    fn dummy_package(base: &str) -> Package {
640        Package {
641            base_directory: PathBuf::from(base),
642            compilation_units: Vec::new(),
643            manifest: Manifest {
644                program: "demo.aleo".to_string(),
645                version: "0.1.0".to_string(),
646                description: String::new(),
647                license: "MIT".to_string(),
648                leo: "0.0.0".to_string(),
649                dependencies: None,
650                dev_dependencies: None,
651            },
652            dep_graph: DiGraph::default(),
653        }
654    }
655
656    #[test]
657    fn bare_unit_name_strips_aleo_suffix() {
658        assert_eq!(crate::bare_unit_name("token.aleo"), "token");
659        assert_eq!(crate::bare_unit_name("token"), "token");
660        assert_eq!(crate::bare_unit_name("credits.aleo"), "credits");
661    }
662
663    #[test]
664    fn unit_paths_are_keyed_by_bare_name() {
665        let pkg = dummy_package("/tmp/demo");
666        // The directory key is the bare compilation unit name, accepting input
667        // with or without the `.aleo` suffix.
668        assert_eq!(pkg.unit_build_directory("token.aleo"), PathBuf::from("/tmp/demo/build/token"));
669        assert_eq!(pkg.unit_build_directory("token"), PathBuf::from("/tmp/demo/build/token"));
670        assert_eq!(pkg.unit_bytecode_path("token.aleo"), PathBuf::from("/tmp/demo/build/token/token.aleo"));
671        assert_eq!(pkg.unit_abi_path("token"), PathBuf::from("/tmp/demo/build/token/abi.json"));
672        assert_eq!(pkg.unit_interfaces_directory("token"), PathBuf::from("/tmp/demo/build/token/interfaces"));
673        assert_eq!(pkg.unit_snapshots_directory("token"), PathBuf::from("/tmp/demo/build/token/snapshots"));
674    }
675
676    #[test]
677    fn libraries_are_keyed_like_programs() {
678        // A library is keyed by its name exactly like a program: a library
679        // `my_lib` declaring interfaces gets `build/my_lib/interfaces/`.
680        let pkg = dummy_package("/tmp/demo");
681        assert_eq!(pkg.unit_build_directory("my_lib"), PathBuf::from("/tmp/demo/build/my_lib"));
682        assert_eq!(pkg.unit_interfaces_directory("my_lib"), PathBuf::from("/tmp/demo/build/my_lib/interfaces"));
683    }
684
685    #[test]
686    fn build_directory_is_the_single_root() {
687        let pkg = dummy_package("/tmp/demo");
688        assert_eq!(pkg.build_directory(), PathBuf::from("/tmp/demo/build"));
689        // Every per-unit path is rooted at `build_directory()`, the single layout seam.
690        assert!(pkg.unit_bytecode_path("x").starts_with(pkg.build_directory()));
691        assert!(pkg.unit_interfaces_directory("credits.aleo").starts_with(pkg.build_directory()));
692    }
693}