diff --git a/README.md b/README.md index 885b615..67810ea 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,21 @@ A dependency entry must use exactly one of `path` or `git`. Git dependencies may Fetched Git dependencies are stored under `.vex/deps/`. Every fetched dependency must contain a `vex.ws` file at its root. Dependency manifests are resolved recursively, and a package name must identify one source and version requirement across the graph. +Every dependency is a library package: its manifest must set `lib = true` and +its canonical entry is `src/lib.wave`. Wave source imports the package name, +not the entry filename: + +```wave +import("local_math"); +import("local_math::vector"); +import("local_math")::{sum, Point}; +``` + +The first form resolves `local_math/src/lib.wave`; the second resolves +`local_math/src/vector.wave`. Vex passes exact mappings for every direct and +transitive dependency to `wavec`, and only `pub` declarations are visible to +consumers. + On the first `vex fetch`, build, run, or check, Vex resolves each Git selector to an exact commit and records the complete transitive graph in `vex.lock`. Later commands reuse those commits without updating branches or tags. Run `vex update` explicitly to refresh every Git dependency and rewrite the lockfile. Pass one or more package names to update only those packages, including transitive dependencies. Unrelated packages keep their exact locked commits and are not fetched. If an updated package changes its dependencies, Vex recalculates that part of the graph while preserving unrelated locked packages. diff --git a/src/resolver.rs b/src/resolver.rs index 04b10e0..6d9e65a 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -338,6 +338,21 @@ impl Resolver<'_> { package_manifest.name )); } + if !package_manifest.lib { + return Err(format!( + "dependency `{}` is not a library package\nhelp: set `lib = true` in `{}` and provide `src/lib.wave`", + dependency.name, + manifest_path.display() + )); + } + let library_entry = resolved_path.join(package_manifest.default_entry_path()); + if !library_entry.is_file() { + return Err(format!( + "dependency `{}` has no canonical library entry `{}`\nhelp: library packages must expose `src/lib.wave`", + dependency.name, + library_entry.display() + )); + } if let Some(required) = dependency.version.as_deref() { if package_manifest.version != required { return Err(format!( diff --git a/tests/cli_contract.rs b/tests/cli_contract.rs index 6aef863..9eb2cde 100644 --- a/tests/cli_contract.rs +++ b/tests/cli_contract.rs @@ -75,6 +75,49 @@ fn invalid_init_and_info_fail_without_mutating_the_directory() { assert!(String::from_utf8_lossy(&invalid_setup.stderr).contains("version value")); } +#[test] +fn dependencies_must_be_library_packages_with_src_lib_wave() { + let fixture = TestDir::new(); + let app = fixture.0.join("app"); + let dependency = fixture.0.join("add"); + fs::create_dir_all(&app).unwrap(); + fs::create_dir_all(&dependency).unwrap(); + fs::write( + app.join("vex.ws"), + "{ name = \"app\", version = 0.1.0, dependencies = [{ name = \"add\", path = \"../add\" }] }\n", + ) + .unwrap(); + fs::write( + dependency.join("vex.ws"), + "{ name = \"add\", version = 0.1.0, lib = false, dependencies = [] }\n", + ) + .unwrap(); + + let non_library = vex(&app, &["fetch"]); + assert_eq!(non_library.status.code(), Some(1)); + assert!(String::from_utf8_lossy(&non_library.stderr) + .contains("dependency `add` is not a library package")); + + fs::write( + dependency.join("vex.ws"), + "{ name = \"add\", version = 0.1.0, lib = true, dependencies = [] }\n", + ) + .unwrap(); + let missing_entry = vex(&app, &["fetch"]); + assert_eq!(missing_entry.status.code(), Some(1)); + assert!( + String::from_utf8_lossy(&missing_entry.stderr).contains("has no canonical library entry") + ); + + fs::create_dir_all(dependency.join("src")).unwrap(); + fs::write( + dependency.join("src/lib.wave"), + "pub fun sum(a: i32, b: i32) -> i32 { return a + b; }\n", + ) + .unwrap(); + assert_success(&vex(&app, &["fetch"]), "fetch canonical library"); +} + fn vex(path: &PathBuf, args: &[&str]) -> Output { Command::new(env!("CARGO_BIN_EXE_vex")) .args(args) diff --git a/tests/git_lock_reproducibility.rs b/tests/git_lock_reproducibility.rs index 724a403..34f9cd7 100644 --- a/tests/git_lock_reproducibility.rs +++ b/tests/git_lock_reproducibility.rs @@ -131,7 +131,9 @@ fn git_lock_keeps_transitive_graph_until_explicit_update() { } fn create_package(path: &Path, name: &str, dependencies: &[(&str, String, Option<&str>)]) { - fs::create_dir_all(path).expect("package directory must be created"); + fs::create_dir_all(path.join("src")).expect("package source directory must be created"); + fs::write(path.join("src/lib.wave"), "pub fun package_marker() {}\n") + .expect("library entry must be written"); let dependency_entries = dependencies .iter() .map(|(dependency, url, branch)| match branch { diff --git a/tests/git_targeted_update.rs b/tests/git_targeted_update.rs index 222b591..a9d3b3d 100644 --- a/tests/git_targeted_update.rs +++ b/tests/git_targeted_update.rs @@ -172,7 +172,9 @@ fn targeted_update_accepts_transitive_packages_and_recalculates_their_graph() { } fn create_package(path: &Path, name: &str, dependencies: &[(&str, String, Option<&str>)]) { - fs::create_dir_all(path).expect("package directory must be created"); + fs::create_dir_all(path.join("src")).expect("package source directory must be created"); + fs::write(path.join("src/lib.wave"), "pub fun package_marker() {}\n") + .expect("library entry must be written"); let dependency_entries = dependencies .iter() .map(|(dependency, url, branch)| match branch { diff --git a/tests/path_lock_portability.rs b/tests/path_lock_portability.rs index fba1fb2..bafaee6 100644 --- a/tests/path_lock_portability.rs +++ b/tests/path_lock_portability.rs @@ -61,7 +61,12 @@ fn path_lock_survives_moving_the_same_relative_package_tree() { fn create_tree(root: &Path) { fs::create_dir_all(root.join("app")).expect("app directory must be created"); - fs::create_dir_all(root.join("dep")).expect("dependency directory must be created"); + fs::create_dir_all(root.join("dep/src")).expect("dependency directory must be created"); + fs::write( + root.join("dep/src/lib.wave"), + "pub fun package_marker() {}\n", + ) + .expect("dependency library entry must be written"); fs::write( root.join("dep/vex.ws"), "{\n name = \"dep\",\n version = 0.1.0,\n lib = true,\n dependencies = []\n}\n", diff --git a/tests/wavec_contract.rs b/tests/wavec_contract.rs index 05dd01a..ba95572 100644 --- a/tests/wavec_contract.rs +++ b/tests/wavec_contract.rs @@ -105,6 +105,67 @@ fn vex_uses_path_and_override_wavec_and_rejects_unknown_schema() { ); } +#[test] +fn vex_passes_direct_and_transitive_library_mappings_to_wavec() { + let fixture = TestDir::new(); + let project = fixture.0.join("project"); + let add = fixture.0.join("add"); + let math = fixture.0.join("math"); + for package in [&project, &add, &math] { + fs::create_dir_all(package.join("src")).unwrap(); + } + + fs::write( + math.join("vex.ws"), + "{ name = \"math\", version = 0.1.0, lib = true, dependencies = [] }\n", + ) + .unwrap(); + fs::write( + math.join("src/lib.wave"), + "pub fun double(value: i32) -> i32 { return value * 2; }\n", + ) + .unwrap(); + fs::write( + add.join("vex.ws"), + "{ name = \"add\", version = 0.1.0, lib = true, dependencies = [{ name = \"math\", path = \"../math\" }] }\n", + ) + .unwrap(); + fs::write( + add.join("src/lib.wave"), + "import(\"math\");\npub fun sum(a: i32, b: i32) -> i32 { return a + b; }\n", + ) + .unwrap(); + fs::write( + project.join("vex.ws"), + "{ name = \"app\", version = 0.1.0, dependencies = [{ name = \"add\", path = \"../add\" }] }\n", + ) + .unwrap(); + fs::write( + project.join("src/main.wave"), + "import(\"add\")::{sum};\nfun main() { var value: i32 = sum(1, 2); }\n", + ) + .unwrap(); + + let fake = compile_fake_wavec(&fixture.0); + let log = fixture.0.join("dependency.log"); + let output = Command::new(env!("CARGO_BIN_EXE_vex")) + .arg("check") + .current_dir(&project) + .env("VEX_WAVEC", &fake) + .env("FAKE_WAVEC_LOG", &log) + .output() + .expect("Vex dependency check must start"); + assert_success(&output, "Vex dependency check"); + + let invocations = fs::read_to_string(log).unwrap(); + for package in ["add", "math"] { + let path = Path::new("..").join(package); + let expected = format!("--dep={package}={}", path.display()); + assert!(invocations.contains(&expected), "{invocations}"); + } + assert!(invocations.contains("src/main.wave"), "{invocations}"); +} + fn compile_fake_wavec(root: &Path) -> PathBuf { let source = root.join("fake_wavec.rs"); fs::write(