Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<name>`. 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.
Expand Down
15 changes: 15 additions & 0 deletions src/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down
43 changes: 43 additions & 0 deletions tests/cli_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion tests/git_lock_reproducibility.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion tests/git_targeted_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 6 additions & 1 deletion tests/path_lock_portability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
61 changes: 61 additions & 0 deletions tests/wavec_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down