ludic remove <module> and ludic get --json (R9)

remove is the inverse of add: the require line leaves package.ludic; the lock keeps exactly what the
remaining requires still reach, read from the store's copy of each locked package.ludic (no
network), so a package another still requires stays locked and what only the removed one brought in
leaves with it; each leaving package loses the ludic_modules/ symlink add made, never the shared
store entry. Refused (exit 1) when package.ludic does not require the module; source still
importing a removed package is a warning. With no store copy to read, only the named module leaves.

get --json diffs the lock before and after in memory and prints {added, removed, changed,
unchanged} on stdout (an entry as the lock records it: name, version, hash, kind, provides; a change
as name, from, to, from_hash, to_hash), the resolver's lines on stderr. Cases added to test-pkg,
not run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-30 00:24:49 +03:00
parent 047bdf4189
commit 320ce42626
10 changed files with 341 additions and 10 deletions

View file

@ -12,6 +12,13 @@ function pt_write(path: pointer, body: pointer) -> void {
write_file(path, body)
}
# a file's text, "" when it is not there
function pt_read(path: pointer) -> pointer {
let t = read_file(path)
if t == null { return "" }
return t
}
# commit the current tree of `dir` and tag it `tag`
function pt_commit_tag(dir: pointer, tag: pointer) -> void {
shell(`git -C {dir} add -A`)
@ -170,6 +177,38 @@ function cmd_test_pkg() -> int {
ok("ludic vendor copies the resolved packages into ./vendor")
} else { bad2("ludic vendor failed", capture_line(`tail -1 {work}/vendor.out`)) }
# ---- for editors (R9): get --json says what changed in the lock; remove undoes add ----
let proj5 = `{work}/proj5`
pt_write(`{proj5}/package.ludic`, `package "app"` + nl() + `version "0.0.0"` + nl() + `require "example.test/greeter" "1.0.0"` + nl())
pt_write(`{proj5}/src/app.ludic`, "program App {\n import \"example.test/greeter/greet.ludic\"\n}\n")
shell(`( cd {proj5} && {envp} {root}/bin/ludic get --json > {work}/gj.json 2> {work}/gj.err )`)
let gj = pt_read(`{work}/gj.json`)
if s_contains(gj, `"name": "example.test/greeter", "version": "1.0.0"`) and s_contains(gj, `"name": "example.test/util", "version": "1.0.0"`) and s_contains(gj, `"unchanged": 0`) and shq(`python3 -m json.tool {work}/gj.json > /dev/null`) and not s_contains(gj, "resolving") {
ok("ludic get --json: a fresh lock is two added packages, one JSON object on stdout")
} else { bad2("ludic get --json (fresh)", gj) }
pt_write(`{proj5}/package.ludic`, `package "app"` + nl() + `version "0.0.0"` + nl() + `require "example.test/greeter" "1.0.0"` + nl() + `require "example.test/util" "1.2.0"` + nl())
let gj2 = capture(`( cd {proj5} && {envp} {root}/bin/ludic get --json 2>/dev/null )`)
if s_contains(gj2, `"added": []`) and s_contains(gj2, `"name": "example.test/util", "from": "1.0.0", "to": "1.2.0"`) and s_contains(gj2, `"unchanged": 1`) {
ok("ludic get --json: a raised minimum is a change from one version to the other")
} else { bad2("ludic get --json (changed)", gj2) }
if not shq(`( cd {proj5} && {root}/bin/ludic remove example.test/nothere > {work}/rm0.out 2>&1 )`) and shq(`grep -q 'does not require' {work}/rm0.out`) {
ok("ludic remove refuses a module package.ludic does not require")
} else { bad2("ludic remove of an unrequired module", capture_line(`cat {work}/rm0.out`)) }
let store_n = capture_line(`ls {store} | wc -l`)
shell(`( cd {proj5} && LUDIC_STORE={store} {root}/bin/ludic remove example.test/util > {work}/rm1.out 2>&1 )`)
let lk1 = pt_read(`{proj5}/package.lock.ludic`)
if not s_contains(pt_read(`{proj5}/package.ludic`), "example.test/util") and s_contains(lk1, "example.test/util") and file_exists(`{proj5}/ludic_modules/example.test/util`) {
ok("ludic remove keeps what another package still requires locked and linked")
} else { bad2("ludic remove dropped a package still required", capture_line(`cat {work}/rm1.out`)) }
shell(`( cd {proj5} && LUDIC_STORE={store} {root}/bin/ludic remove example.test/greeter > {work}/rm2.out 2>&1 )`)
let lk2 = pt_read(`{proj5}/package.lock.ludic`)
if not s_contains(lk2, "example.test/") and not file_exists(`{proj5}/ludic_modules`) and capture_line(`ls {store} | wc -l`) == store_n {
ok("ludic remove drops the require, what only it locked and its links, and leaves the shared store alone")
} else { bad2("ludic remove of the last require", capture_line(`cat {work}/rm2.out`)) }
if shq(`grep -q 'still imported by' {work}/rm2.out`) and shq(`grep -q 'src/app.ludic' {work}/rm2.out`) {
ok("ludic remove warns about source still importing what it removed")
} else { bad("ludic remove said nothing about src/app.ludic importing the package") }
# ---- package-declarable engine-system + namespace (issue #62) -------------
# A source package registers a compile-time engine system (@EngineSystem) and a
# Foo.* namespace (@Namespace) with no compiler edit; a consumer game imports it