From d21f237211ab1d0b4b4adfa5d8d91ef00771adc4 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Mon, 24 Aug 2026 13:41:26 +0300 Subject: [PATCH] cli: add ability to set project root --- doc/architecture.md | 4 +- src/main.rs | 31 ++++++++-- tests/cli.rs | 137 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 6 deletions(-) diff --git a/doc/architecture.md b/doc/architecture.md index 0213c086..b70e8ffa 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -44,7 +44,9 @@ The `crate` keyword is used to construct absolute paths where the path root is t use crate::math::add; ``` -The compiler driver (`simc`) automatically maps the `crate` keyword to the directory containing the entry-point file. For external libraries linked via `--dep`, the driver also maps `crate` to the library's root, ensuring that `use crate::...` statements inside the library resolve correctly within that library's scope. +The compiler driver (`simc`) maps the `crate` keyword to the project root. By default, this is the directory containing the entry-point file. +For projects whose entry point is in a subdirectory, pass `--project-root ` to set the root explicitly. +For external libraries linked via `--dep`, the driver also maps `crate` to the library's root, ensuring that `use crate::...` statements inside the library resolve correctly within that library's scope. ### Strict Local Imports diff --git a/src/main.rs b/src/main.rs index 73bafd05..c10e0091 100644 --- a/src/main.rs +++ b/src/main.rs @@ -69,6 +69,15 @@ fn main() -> Result<(), Box> { .action(ArgAction::Append) .help("Link a dependency, optionally scoped to a specific module (e.g., --dep ./libs/merkle:math=./libs/math)"), ) + .arg( + Arg::new("project_root") + .long("project-root") + .value_name("PROJECT_ROOT") + .action(ArgAction::Set) + .help( + "Project root for resolving `crate::` imports (defaults to the entry file's directory)", + ), + ) .arg( Arg::new("wit_file") .long("wit") @@ -157,11 +166,23 @@ fn main() -> Result<(), Box> { .get_many::("dependencies") .unwrap_or_default(); - let canon_root = main_path - .as_path() - .parent() - .and_then(|p| CanonPath::canonicalize(p).ok()) - .ok_or("Failed to determine project root directory from entry file")?; + let canon_root = match matches.get_one::("project_root") { + Some(project_root) => CanonPath::canonicalize(Path::new(project_root))?, + None => main_path + .as_path() + .parent() + .and_then(|p| CanonPath::canonicalize(p).ok()) + .ok_or("Failed to determine project root directory from entry file")?, + }; + + if !main_path.starts_with(&canon_root) { + return Err(format!( + "Entry file '{}' is outside project root '{}'", + main_path.as_path().display(), + canon_root.as_path().display() + ) + .into()); + } let mut builder = DependencyMapBuilder::new(); diff --git a/tests/cli.rs b/tests/cli.rs index dd5035e8..486b48ff 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -54,6 +54,143 @@ fn cli_dependency_can_use_crate_root() { ); } +#[test] +fn cli_project_root_resolves_crate_from_nested_entry() { + let root = setup_project( + "nested_entry_project_root", + &[ + ( + "src/main.simf", + "simc \"*\";\nuse crate::utils::helper;\nfn main() { assert!(jet::eq_32(helper(), 42)); }\n", + ), + ( + "utils.simf", + "simc \"*\";\nuse support::values::answer;\npub fn helper() -> u32 { answer() }\n", + ), + ( + "src/utils.simf", + "simc \"*\";\npub fn wrong_helper() -> u32 { 0 }\n", + ), + ( + "deps/support/values.simf", + "simc \"*\";\npub fn answer() -> u32 { 42 }\n", + ), + ], + ); + + let output = Command::new(env!("CARGO_BIN_EXE_simc")) + .current_dir(&root) + .arg("src/main.simf") + .arg("--project-root") + .arg(".") + .arg("-Z") + .arg("imports") + .arg("--dep") + .arg("support=deps/support") + .output() + .expect("failed to run simc"); + + assert!( + output.status.success(), + "simc failed\nstatus: {:?}\nstdout:\n{}\nstderr:\n{}", + output.status.code(), + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); +} + +#[test] +fn cli_nested_entry_defaults_project_root_to_entry_parent() { + let root = setup_project( + "nested_entry_default_root", + &[ + ( + "src/main.simf", + "simc \"*\";\nuse crate::utils::helper;\nfn main() { assert!(jet::eq_32(helper(), 7)); }\n", + ), + ( + "src/utils.simf", + "simc \"*\";\npub fn helper() -> u32 { 7 }\n", + ), + ( + "utils.simf", + "simc \"*\";\npub fn wrong_helper() -> u32 { 0 }\n", + ), + ], + ); + + let output = Command::new(env!("CARGO_BIN_EXE_simc")) + .arg(root.join("src/main.simf")) + .arg("-Z") + .arg("imports") + .output() + .expect("failed to run simc"); + + assert!( + output.status.success(), + "entry-parent default failed\nstderr:\n{}", + String::from_utf8_lossy(&output.stderr), + ); +} + +#[test] +fn cli_project_root_must_contain_entry_file() { + let root = setup_project( + "project_root_containment", + &[ + ("project/main.simf", "simc \"*\";\nfn main() {}\n"), + ( + "other/placeholder.simf", + "simc \"*\";\nfn placeholder() {}\n", + ), + ], + ); + + let output = Command::new(env!("CARGO_BIN_EXE_simc")) + .arg(root.join("project/main.simf")) + .arg("--project-root") + .arg(root.join("other")) + .output() + .expect("failed to run simc"); + + assert!( + !output.status.success(), + "simc must reject an entry file outside the project root" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("outside project root"), + "expected a project-root containment error, got:\n{stderr}" + ); +} + +#[test] +fn cli_root_level_entry_keeps_entry_parent_default() { + let root = setup_project( + "root_level_entry_default", + &[ + ( + "main.simf", + "simc \"*\";\nuse crate::utils::helper;\nfn main() { helper(); }\n", + ), + ("utils.simf", "simc \"*\";\npub fn helper() {}\n"), + ], + ); + + let output = Command::new(env!("CARGO_BIN_EXE_simc")) + .arg(root.join("main.simf")) + .arg("-Z") + .arg("imports") + .output() + .expect("failed to run simc"); + + assert!( + output.status.success(), + "root-level entry failed\nstderr:\n{}", + String::from_utf8_lossy(&output.stderr), + ); +} + #[test] fn cli_import_program_rejected_without_unstable_flag() { let root = repo_path("functional-tests/valid-test-cases/external-library-uses-crate");