miden-project 0.22.2

Interface for working with Miden projects
Documentation
use std::{boxed::Box, fs, path::Path, sync::Arc};

use miden_assembly_syntax::{
    Path as MasmPath,
    debuginfo::{DefaultSourceManager, SourceManager, SourceManagerExt},
    diagnostics::Report,
};
use miden_core::assert_matches;
use tempfile::TempDir;

use crate::{DependencyVersionScheme, Linkage, Project, TargetType, Workspace};

struct TestContext {
    pub source_manager: Arc<dyn SourceManager>,
}

impl Default for TestContext {
    fn default() -> Self {
        Self {
            source_manager: Arc::new(DefaultSourceManager::default()),
        }
    }
}

impl TestContext {
    pub fn load_workspace(&self, path: impl AsRef<Path>) -> Result<Box<Workspace>, Report> {
        let path = path.as_ref();
        let source_file = self.source_manager.load_file(path).map_err(Report::msg)?;
        Workspace::load(source_file, &self.source_manager)
    }
}

#[test]
fn can_load_protocol_example_project() -> Result<(), Report> {
    const MANIFEST_PATH: &str =
        concat!(env!("CARGO_MANIFEST_DIR"), "/examples/protocol/miden-project.toml");
    let context = TestContext::default();
    let workspace = context.load_workspace(MANIFEST_PATH)?;

    assert_eq!(workspace.members().len(), 3);

    let core_project = workspace
        .get_member_by_name("miden-utils")
        .expect("failed to locate 'miden-utils' project");
    assert!(Arc::ptr_eq(
        &core_project,
        &workspace
            .get_member_by_relative_path("utils")
            .expect("failed to locate 'miden-utils' project by relative path")
    ));

    let core_lib = core_project.library_target().unwrap();
    assert_eq!(core_lib.ty, TargetType::Library);
    assert_eq!(&**core_lib.name.inner(), "miden::utils");
    assert_eq!(&**core_lib.namespace.inner(), MasmPath::new("::miden::utils"));
    assert_eq!(core_project.executable_targets().len(), 0);

    let kernel_project = workspace
        .get_member_by_name("miden-tx")
        .expect("failed to locate 'miden-tx' project");

    let kernel_lib = kernel_project.library_target().unwrap();
    assert_eq!(kernel_lib.ty, TargetType::Kernel);
    assert_eq!(&**kernel_lib.name.inner(), "miden-tx");
    assert_eq!(&**kernel_lib.namespace.inner(), MasmPath::kernel_path());
    assert_eq!(kernel_project.executable_targets().len(), 2);

    assert_eq!(kernel_project.executable_targets()[0].ty, TargetType::Executable);
    assert_eq!(&**kernel_project.executable_targets()[0].name.inner(), "entry");
    assert_eq!(
        &**kernel_project.executable_targets()[0].namespace.inner(),
        MasmPath::exec_path()
    );

    assert_eq!(kernel_project.executable_targets()[1].ty, TargetType::Executable);
    assert_eq!(&**kernel_project.executable_targets()[1].name.inner(), "entry-alt");
    assert_eq!(
        &**kernel_project.executable_targets()[1].namespace.inner(),
        MasmPath::exec_path()
    );

    assert_eq!(kernel_project.dependencies().len(), 1);
    assert_eq!(&**kernel_project.dependencies()[0].name(), "miden-utils");
    assert_matches!(kernel_project.dependencies()[0].scheme(), DependencyVersionScheme::Workspace { member, .. } if member.path() == "utils");
    assert_eq!(kernel_project.dependencies()[0].linkage(), Linkage::Static);

    let userspace_project = workspace
        .get_member_by_name("miden-protocol")
        .expect("failed to locate 'miden-protocol' project");

    let userspace_lib = userspace_project.library_target().unwrap();
    assert_eq!(userspace_lib.ty, TargetType::Library);
    assert_eq!(&**userspace_lib.name.inner(), "miden::protocol");
    assert_eq!(&**userspace_lib.namespace.inner(), MasmPath::new("::miden::protocol"));
    assert_eq!(userspace_project.executable_targets().len(), 0);

    assert_eq!(userspace_project.dependencies().len(), 2);
    assert_eq!(&**userspace_project.dependencies()[0].name(), "miden-tx");
    assert_matches!(userspace_project.dependencies()[0].scheme(), DependencyVersionScheme::Workspace { member, .. } if member.path() == "kernel");
    assert_eq!(&**userspace_project.dependencies()[1].name(), "miden-utils");
    assert_matches!(userspace_project.dependencies()[1].scheme(), DependencyVersionScheme::Workspace { member, .. } if member.path() == "utils");
    assert_eq!(userspace_project.dependencies()[1].linkage(), Linkage::Dynamic);

    Ok(())
}

#[test]
fn workspace_dev_override_is_used_for_child_profile_inheritance() -> Result<(), Report> {
    let tempdir = TempDir::new().unwrap();
    let root = tempdir.path().join("workspace-profile");
    let app_dir = root.join("app");
    fs::create_dir_all(&app_dir).unwrap();

    fs::write(
        root.join("miden-project.toml"),
        r#"[workspace]
members = ["app"]

[workspace.package]
version = "0.1.0"

[profile.dev]
debug = false
"#,
    )
    .unwrap();

    let app_manifest_path = app_dir.join("miden-project.toml");
    fs::write(
        &app_manifest_path,
        r#"[package]
name = "app"
version = "0.1.0"

[profile.child]
inherits = "dev"
"#,
    )
    .unwrap();

    let context = TestContext::default();
    let Project::WorkspacePackage { package, workspace: _ } =
        Project::load(&app_manifest_path, &context.source_manager)?
    else {
        panic!("expected workspace package")
    };
    let child = package.profiles().iter().find(|p| p.name().as_ref() == "child").unwrap();

    assert!(!child.should_emit_debug_info());

    Ok(())
}

#[test]
fn workspace_package_version_can_be_inherited_with_dotted_key_syntax() -> Result<(), Report> {
    let tempdir = TempDir::new().unwrap();
    let root = tempdir.path().join("workspace-version");
    let app_dir = root.join("app");
    fs::create_dir_all(&app_dir).unwrap();

    fs::write(
        root.join("miden-project.toml"),
        r#"[workspace]
members = ["app"]

[workspace.package]
version = "0.1.0"
"#,
    )
    .unwrap();

    let app_manifest_path = app_dir.join("miden-project.toml");
    fs::write(
        &app_manifest_path,
        r#"[package]
name = "app"
version.workspace = true
"#,
    )
    .unwrap();

    let context = TestContext::default();
    let Project::WorkspacePackage { package, workspace: _ } =
        Project::load(&app_dir, &context.source_manager)?
    else {
        panic!("expected workspace package")
    };

    assert_eq!(format!("{}", package.version()), "0.1.0");

    Ok(())
}

#[test]
fn load_project_reference_keeps_non_member_workspace_paths_authoritative() -> Result<(), Report> {
    let tempdir = TempDir::new().unwrap();
    let root = tempdir.path().join("workspace");
    fs::create_dir_all(&root).unwrap();
    let root = root.canonicalize().unwrap();
    let member_dir = root.join("member");
    let vendor_dir = root.join("vendor").join("dep");
    fs::create_dir_all(&member_dir).unwrap();
    fs::create_dir_all(&vendor_dir).unwrap();

    fs::write(
        root.join("miden-project.toml"),
        r#"[workspace]
members = ["member"]
"#,
    )
    .unwrap();

    fs::write(
        member_dir.join("miden-project.toml"),
        r#"[package]
name = "dep"
version = "1.0.0"

[lib]
path = "lib.masm"
"#,
    )
    .unwrap();
    fs::write(member_dir.join("lib.masm"), "export.foo\nend\n").unwrap();

    let vendor_manifest = vendor_dir.join("miden-project.toml");
    fs::write(
        &vendor_manifest,
        r#"[package]
name = "dep"
version = "9.0.0"

[lib]
path = "lib.masm"
"#,
    )
    .unwrap();
    fs::write(vendor_dir.join("lib.masm"), "export.foo\nend\n").unwrap();

    let context = TestContext::default();
    let project = Project::load_project_reference("dep", &vendor_dir, &context.source_manager)?;

    assert!(!project.is_workspace_member());
    assert_eq!(project.manifest_path(), Some(vendor_manifest.as_path()));
    assert_eq!(format!("{}", project.package().version()), "9.0.0");

    Ok(())
}

#[test]
fn load_project_reference_resolves_workspace_manifest_file_inputs() -> Result<(), Report> {
    let tempdir = TempDir::new().unwrap();
    let root = tempdir.path().join("workspace");
    let dep_dir = root.join("dep");
    fs::create_dir_all(&dep_dir).unwrap();

    let workspace_manifest = root.join("miden-project.toml");
    fs::write(
        &workspace_manifest,
        r#"[workspace]
members = ["dep"]
"#,
    )
    .unwrap();

    let dep_manifest = dep_dir.join("miden-project.toml");
    fs::write(
        &dep_manifest,
        r#"[package]
name = "dep"
version = "1.2.3"

[lib]
path = "lib.masm"
"#,
    )
    .unwrap();
    fs::write(dep_dir.join("lib.masm"), "export.foo\nend\n").unwrap();
    let dep_manifest = dep_manifest.canonicalize().unwrap();

    let context = TestContext::default();
    let project =
        Project::load_project_reference("dep", &workspace_manifest, &context.source_manager)?;

    assert!(project.is_workspace_member());
    assert_eq!(project.manifest_path(), Some(dep_manifest.as_path()));
    assert_eq!(format!("{}", project.package().version()), "1.2.3");

    Ok(())
}

#[test]
fn workspace_rejects_duplicate_member_package_names() {
    let tempdir = TempDir::new().unwrap();
    let root = tempdir.path().join("workspace");
    fs::create_dir_all(&root).unwrap();
    let root = root.canonicalize().unwrap();
    let first_dir = root.join("first");
    let second_dir = root.join("second");
    fs::create_dir_all(&first_dir).unwrap();
    fs::create_dir_all(&second_dir).unwrap();

    fs::write(
        root.join("miden-project.toml"),
        r#"[workspace]
members = ["first", "second"]
"#,
    )
    .unwrap();

    fs::write(
        first_dir.join("miden-project.toml"),
        r#"[package]
name = "dep"
version = "1.0.0"

[lib]
path = "lib.masm"
"#,
    )
    .unwrap();
    fs::write(first_dir.join("lib.masm"), "export.foo\nend\n").unwrap();

    fs::write(
        second_dir.join("miden-project.toml"),
        r#"[package]
name = "dep"
version = "2.0.0"

[lib]
path = "lib.masm"
"#,
    )
    .unwrap();
    fs::write(second_dir.join("lib.masm"), "export.foo\nend\n").unwrap();

    let context = TestContext::default();
    let error = context
        .load_workspace(root.join("miden-project.toml"))
        .expect_err("duplicate member package names should be rejected");

    assert!(format!("{error}").contains("duplicate"), "{error}");
}