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