ludic/tools/ludic-cli/pkg_remove.ludic
Orkuncakilkaya 320ce42626 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>
2026-09-30 00:24:49 +03:00

134 lines
5.2 KiB
Text

# pkg_remove.ludic — `ludic remove <module>`, the inverse of `ludic add`: the `require` line leaves
# package.ludic, and the lock keeps exactly what the remaining requires still reach. The graph is
# read from the store's copies of each locked package.ludic, so nothing is fetched; what the removal
# leaves unreachable leaves the lock and its ludic_modules/ link (the link `add` made, never the store
# entry it points at, which other projects share). Source still importing a removed package is a
# warning: the caller may be about to delete it.
# package.ludic's text with every `require "<module>" ...` line dropped
function drop_require(txt: pointer, module: pointer) -> pointer {
var out = ""
let n = slen(txt)
var i = 0
while i < n {
let line = line_at(txt, i)
i = i + slen(line) + 1
let ts = tok_line(line)
if not (len(ts) >= 2 and ts[0] == "require" and ts[1] == module) { out = out + line + nl() }
}
return out
}
function requires(m: Manifest, module: pointer) -> bool {
var i = 0
while i < len(m.deps) {
if m.deps[i].module == module { return true }
i += 1
}
return false
}
var g_graph_unknown: pointer = "" # a locked package the store has no copy of, when there is one
# the locked entries the root's requires still reach, through the store's package.ludic files
function lock_reachable(root: Manifest, locked: []Manifest) -> []Manifest {
let seen = new []pointer
let todo = new []pointer
var d = 0
while d < len(root.deps) {
push(todo, root.deps[d].module)
d += 1
}
while len(todo) > 0 {
let m = List.pop(todo)
let at = lock_index(locked, m)
if find_mod(seen, m) < 0 and at >= 0 {
push(seen, m)
let dir = `{store_root()}{strip_prefix(locked[at].hash, "sha256:")}`
if not shq(`test -d "{dir}"`) { g_graph_unknown = m }
else {
let mt = read_file(`{dir}/package.ludic`)
if mt != null {
let dm = parse_manifest(mt)
var k = 0
while k < len(dm.deps) {
push(todo, dm.deps[k].module)
k += 1
}
}
}
}
}
let keep = new []Manifest
var i = 0
while i < len(locked) {
if find_mod(seen, locked[i].module) >= 0 { push(keep, locked[i]) }
i += 1
}
return keep
}
# take down the ludic_modules/ view `add` linked, and the directories it leaves empty
function unlink_module(module: pointer) -> void {
let link = `ludic_modules/{module}`
if shq(`test -L "{link}"`) {
shell(`rm -f "{link}"; d=$(dirname "{link}"); while [ "$d" != ludic_modules ] && [ "$d" != . ] && rmdir "$d" 2>/dev/null; do d=$(dirname "$d"); done; rmdir ludic_modules 2>/dev/null`)
} else if shq(`test -e "{link}"`) {
err(`ludic remove: warning: {link} is not a link 'ludic add' made; left as it is\n`)
}
if shq(`test -e "vendor/{module}"`) { err(`ludic remove: vendor/{module} is still there (run 'ludic vendor' again)\n`) }
}
# the project's own files that still import `module`
function warn_importers(module: pointer) -> void {
let hits = split_lines(capture(`grep -rlF --include='*.ludic' --exclude-dir=ludic_modules --exclude-dir=vendor --exclude-dir=build --exclude-dir=.git -e 'import "{module}/' -e 'import "{module}"' . | sed 's|^\\./||' | LC_ALL=C sort`))
if len(hits) == 0 { return }
err(`ludic remove: warning: {module} is still imported by:\n`)
var i = 0
while i < len(hits) {
err(` {hits[i]}\n`)
i += 1
}
}
# ludic remove <module> — drop a dependency: the require, what only it brought into the lock, the links
function cmd_pkg_remove() -> int {
if arg_count() < 3 { err("usage: ludic remove <module>\n"); return 1 }
let module = split_spec(arg(2))[0]
let txt = read_file("package.ludic")
if txt == null { err("ludic remove: no package.ludic in the current directory\n"); return 1 }
let locked = read_lock_or_empty()
if not requires(parse_manifest(txt), module) {
var why = ""
if lock_index(locked, module) >= 0 { why = " (the lock has it because another package requires it)" }
err(`ludic remove: package.ludic does not require {module}{why}\n`)
return 1
}
if not write_file("package.ludic", drop_require(txt, module)) { err("ludic remove: cannot write package.ludic\n"); return 1 }
print(`ludic remove: {module} is no longer required`)
if file_exists("package.lock.ludic") {
var keep = lock_reachable(read_root_manifest(), locked)
if g_graph_unknown != "" {
err(`ludic remove: the store has no copy of {g_graph_unknown}, so only {module} leaves the lock; 'ludic get' settles the rest\n`)
keep = new []Manifest
var j = 0
while j < len(locked) {
if not (locked[j].module == module) { push(keep, locked[j]) }
j += 1
}
}
if not write_lock(keep) { err("ludic remove: cannot write package.lock.ludic\n"); return 1 }
var i = 0
while i < len(locked) {
let m = locked[i]
if lock_index(keep, m.module) < 0 {
unlink_module(m.module)
print(` removed {m.module} {m.ver}`)
if not (m.module == module) { warn_importers(m.module) }
}
i += 1
}
if lock_index(keep, module) >= 0 { print(` {module} stays locked: another package still requires it`) }
}
warn_importers(module)
return 0
}