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:
parent
047bdf4189
commit
320ce42626
10 changed files with 341 additions and 10 deletions
|
|
@ -144,6 +144,7 @@ cached in a content-addressed store:
|
||||||
```bash
|
```bash
|
||||||
ludic add git.workshopsoft.io/user/pkg # resolve, fetch, link into ludic_modules/
|
ludic add git.workshopsoft.io/user/pkg # resolve, fetch, link into ludic_modules/
|
||||||
ludic get # install from package.ludic, write the lock
|
ludic get # install from package.ludic, write the lock
|
||||||
|
ludic remove git.workshopsoft.io/user/pkg # the inverse of add
|
||||||
ludic verify # check locked packages against the store
|
ludic verify # check locked packages against the store
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
10
changes/pkg-remove-get-json.md
Normal file
10
changes/pkg-remove-get-json.md
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
bump: minor
|
||||||
|
type: feat
|
||||||
|
**`ludic remove <module>` and `ludic get --json`.** `remove` is the inverse of `add`: the `require`
|
||||||
|
leaves `package.ludic`, the lock keeps what the remaining requires still reach (read from the store's
|
||||||
|
copies of each package's manifest, no network), and each package leaving the lock loses the
|
||||||
|
`ludic_modules/` link `add` made - never the shared store entry. A module that is not required is
|
||||||
|
refused; source still importing a removed package is a warning. `ludic get --json` prints what the run
|
||||||
|
changed in the lock as one JSON object on stdout - `{"added", "removed", "changed", "unchanged"}`, an
|
||||||
|
entry as the lock records it (`name`, `version`, `hash`, `kind`, `provides`), a change as `{name, from,
|
||||||
|
to, from_hash, to_hash}` - with the resolver's own lines on stderr.
|
||||||
|
|
@ -19,7 +19,8 @@ This is the v1 implementation of the direction decided in issue #63.
|
||||||
|
|
||||||
```
|
```
|
||||||
ludic add <module>[@version] add a dependency to package.ludic, then resolve + fetch + link
|
ludic add <module>[@version] add a dependency to package.ludic, then resolve + fetch + link
|
||||||
ludic get resolve every dependency in package.ludic, link them, write the lock
|
ludic remove <module> the inverse of add: drop the require, what only it locked, its links
|
||||||
|
ludic get [--json] resolve every dependency in package.ludic, link them, write the lock
|
||||||
ludic update [module] bump a dependency (or all) to its latest published version, then relock
|
ludic update [module] bump a dependency (or all) to its latest published version, then relock
|
||||||
ludic verify check every locked package against the store by content hash
|
ludic verify check every locked package against the store by content hash
|
||||||
ludic vendor copy the resolved packages into ./vendor for hermetic/offline builds
|
ludic vendor copy the resolved packages into ./vendor for hermetic/offline builds
|
||||||
|
|
@ -29,6 +30,23 @@ ludic vendor copy the resolved packages into ./vendor for herm
|
||||||
minimum. All the install commands print the resolved build list and write
|
minimum. All the install commands print the resolved build list and write
|
||||||
`package.lock.ludic`.
|
`package.lock.ludic`.
|
||||||
|
|
||||||
|
`ludic get --json` prints what the run changed in the lock as one JSON object on stdout (the
|
||||||
|
resolver's lines go to stderr): `{"added": [...], "removed": [...], "changed": [...], "unchanged": n}`,
|
||||||
|
an added or removed entry being the lock's line - `{"name", "version", "hash", "kind", "provides"}` -
|
||||||
|
and a changed one `{"name", "from", "to", "from_hash", "to_hash"}` (the versions, and the content
|
||||||
|
hashes beside them). The lock is compared before and after, in memory.
|
||||||
|
|
||||||
|
`ludic remove <module>` takes the `require` line out of `package.ludic` and re-reads the dependency
|
||||||
|
graph from the store's own copy of each locked package's manifest - no network - so the lock keeps
|
||||||
|
exactly what the remaining requires still reach: a package another one still requires stays locked
|
||||||
|
(at the version it had; `ludic get` settles versions), and what only the removed one brought in
|
||||||
|
leaves the lock with it. Each package leaving the lock loses its `ludic_modules/` link - the link
|
||||||
|
`add` made, never the store entry it points at, which other projects share; anything there that is
|
||||||
|
not such a link is left and reported, as is a copy under `vendor/`. A module `package.ludic` does
|
||||||
|
not require is refused (exit 1). Source that still imports a removed package is a warning, not a
|
||||||
|
failure - the caller may be about to delete it. If the store has no copy of a locked package, only
|
||||||
|
the named module leaves the lock, and it says so.
|
||||||
|
|
||||||
## The manifest — `package.ludic`
|
## The manifest — `package.ludic`
|
||||||
|
|
||||||
A line-oriented manifest. `#` starts a comment; strings are double-quoted.
|
A line-oriented manifest. `#` starts a comment; strings are double-quoted.
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ program LudicDev {
|
||||||
import "assets.ludic"
|
import "assets.ludic"
|
||||||
import "release.ludic"
|
import "release.ludic"
|
||||||
import "pkg.ludic"
|
import "pkg.ludic"
|
||||||
|
import "pkg_lock.ludic"
|
||||||
import "pkg_test.ludic"
|
import "pkg_test.ludic"
|
||||||
|
|
||||||
function usage() -> void {
|
function usage() -> void {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ program Ludic {
|
||||||
import "split.ludic"
|
import "split.ludic"
|
||||||
import "project.ludic"
|
import "project.ludic"
|
||||||
import "pkg.ludic"
|
import "pkg.ludic"
|
||||||
|
import "pkg_lock.ludic"
|
||||||
|
import "pkg_remove.ludic"
|
||||||
import "assets.ludic"
|
import "assets.ludic"
|
||||||
import "packignore.ludic"
|
import "packignore.ludic"
|
||||||
import "pack.ludic"
|
import "pack.ludic"
|
||||||
|
|
@ -59,7 +61,9 @@ program Ludic {
|
||||||
print("")
|
print("")
|
||||||
print("packages:")
|
print("packages:")
|
||||||
print(" add <module>[@version] add a dependency, then resolve + fetch + link")
|
print(" add <module>[@version] add a dependency, then resolve + fetch + link")
|
||||||
print(" get resolve every package.ludic dependency and write the lock")
|
print(" remove <module> drop a dependency: its require, what only it locked, its ludic_modules/ link")
|
||||||
|
print(" get [--json] resolve every package.ludic dependency and write the lock")
|
||||||
|
print(" (--json: what changed in the lock, as a JSON object on stdout)")
|
||||||
print(" update [module] bump a dependency (or all) to its latest published version")
|
print(" update [module] bump a dependency (or all) to its latest published version")
|
||||||
print(" verify check every locked package against the store by content hash")
|
print(" verify check every locked package against the store by content hash")
|
||||||
print(" vendor copy the resolved packages into ./vendor for offline builds")
|
print(" vendor copy the resolved packages into ./vendor for offline builds")
|
||||||
|
|
@ -124,6 +128,7 @@ program Ludic {
|
||||||
|
|
||||||
if (cmd == "add") { return cmd_pkg_add() }
|
if (cmd == "add") { return cmd_pkg_add() }
|
||||||
if (cmd == "get") { return cmd_pkg_get() }
|
if (cmd == "get") { return cmd_pkg_get() }
|
||||||
|
if (cmd == "remove") { return cmd_pkg_remove() }
|
||||||
if (cmd == "update") { return cmd_pkg_update() }
|
if (cmd == "update") { return cmd_pkg_update() }
|
||||||
if (cmd == "verify") { return cmd_pkg_verify() }
|
if (cmd == "verify") { return cmd_pkg_verify() }
|
||||||
if (cmd == "vendor") { return cmd_pkg_vendor() }
|
if (cmd == "vendor") { return cmd_pkg_vendor() }
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# pkg.ludic — the Ludic package manager (issue #63), the `ludic add`/`get`/
|
# pkg.ludic — the Ludic package manager (issue #63), the `ludic add`/`get`/
|
||||||
# `update`/`verify`/`vendor` commands.
|
# `update`/`verify`/`vendor` commands (`remove` and `get --json`: pkg_remove.ludic, pkg_lock.ludic).
|
||||||
#
|
#
|
||||||
# It realises the v1 direction decided in the RFC:
|
# It realises the v1 direction decided in the RFC:
|
||||||
#
|
#
|
||||||
|
|
@ -484,7 +484,7 @@ function write_lock(sels: []Manifest) -> bool {
|
||||||
function do_install(root: Manifest) -> int {
|
function do_install(root: Manifest) -> int {
|
||||||
let sels = resolve(root)
|
let sels = resolve(root)
|
||||||
if len(sels) == 0 {
|
if len(sels) == 0 {
|
||||||
print("no dependencies to resolve")
|
pkg_say("no dependencies to resolve")
|
||||||
write_lock(sels)
|
write_lock(sels)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
@ -505,11 +505,11 @@ function do_install(root: Manifest) -> int {
|
||||||
if slen(h) == 0 { err(`ludic: failed to snapshot {m.module}@{m.ver}\n`); return 1 }
|
if slen(h) == 0 { err(`ludic: failed to snapshot {m.module}@{m.ver}\n`); return 1 }
|
||||||
m.hash = `sha256:{h}`
|
m.hash = `sha256:{h}`
|
||||||
link_module(m.module, h)
|
link_module(m.module, h)
|
||||||
print(` {m.module} {m.ver} ({m.kind}) sha256:{sslice(h, 0, 12)}…`)
|
pkg_say(` {m.module} {m.ver} ({m.kind}) sha256:{sslice(h, 0, 12)}…`)
|
||||||
i += 1
|
i += 1
|
||||||
}
|
}
|
||||||
if not write_lock(sels) { err("ludic: cannot write package.lock.ludic\n"); return 1 }
|
if not write_lock(sels) { err("ludic: cannot write package.lock.ludic\n"); return 1 }
|
||||||
print(`resolved {string(len(sels))} package(s) — see package.lock.ludic; linked under ludic_modules/`)
|
pkg_say(`resolved {string(len(sels))} package(s) — see package.lock.ludic; linked under ludic_modules/`)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -596,12 +596,19 @@ function cmd_pkg_add() -> int {
|
||||||
return do_install(read_root_manifest())
|
return do_install(read_root_manifest())
|
||||||
}
|
}
|
||||||
|
|
||||||
# ludic get — resolve + fetch + link every dependency in package.ludic, write lock
|
# ludic get [--json] — resolve + fetch + link every dependency in package.ludic, write lock. --json
|
||||||
|
# says what changed in the lock as one JSON object on stdout (lock_diff_json), the rest on stderr.
|
||||||
function cmd_pkg_get() -> int {
|
function cmd_pkg_get() -> int {
|
||||||
let txt = read_file("package.ludic")
|
let txt = read_file("package.ludic")
|
||||||
if txt == null { err("ludic: no package.ludic in the current directory (ludic add <module> to start one)\n"); return 1 }
|
if txt == null { err("ludic: no package.ludic in the current directory (ludic add <module> to start one)\n"); return 1 }
|
||||||
print("resolving dependencies (MVS)…")
|
let json = pkg_has_flag("--json")
|
||||||
return do_install(parse_manifest(txt))
|
let before = read_lock_or_empty()
|
||||||
|
g_pkg_err = json
|
||||||
|
pkg_say("resolving dependencies (MVS)…")
|
||||||
|
let rc = do_install(parse_manifest(txt))
|
||||||
|
if rc != 0 or not json { return rc }
|
||||||
|
out(lock_diff_json(before, read_lock_or_empty()))
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# ludic update [module] — bump a dep (or all) to its latest published version, relock
|
# ludic update [module] — bump a dep (or all) to its latest published version, relock
|
||||||
|
|
|
||||||
116
tools/ludic-cli/pkg_lock.ludic
Normal file
116
tools/ludic-cli/pkg_lock.ludic
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
# pkg_lock.ludic — the package manager's answers for editors and tools: `ludic get --json` (what a
|
||||||
|
# get changed in package.lock.ludic, as one JSON object on stdout) and `ludic remove <module>`, the
|
||||||
|
# inverse of `ludic add`. Neither touches the network: the diff is of the lock before and after, and
|
||||||
|
# a removal re-reads the dependency graph from the store's own copies of each package.ludic.
|
||||||
|
|
||||||
|
# the human lines of add / get / update: on stdout, or on stderr under --json
|
||||||
|
var g_pkg_err: bool = false
|
||||||
|
function pkg_say(line: pointer) -> void {
|
||||||
|
if g_pkg_err { err(line + nl()) } else { print(line) }
|
||||||
|
}
|
||||||
|
function pkg_has_flag(flag: pointer) -> bool {
|
||||||
|
var ai = 2
|
||||||
|
while ai < arg_count() {
|
||||||
|
if arg(ai) == flag { return true }
|
||||||
|
ai += 1
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- JSON out --------------------------------------------------------------------
|
||||||
|
|
||||||
|
function pkg_hex(d: int) -> pointer {
|
||||||
|
let b = bytes(2)
|
||||||
|
b[0] = '0' + d
|
||||||
|
if d > 9 { b[0] = 'a' + d - 10 }
|
||||||
|
b[1] = 0
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
function pkg_jesc(c: int) -> pointer {
|
||||||
|
if c == '"' { return "\\\"" }
|
||||||
|
if c == 92 { return "\\\\" }
|
||||||
|
if c == '\n' { return "\\n" }
|
||||||
|
if c == '\t' { return "\\t" }
|
||||||
|
if c == '\r' { return "\\r" }
|
||||||
|
return "\\u00" + pkg_hex(c >> 4) + pkg_hex(c & 15)
|
||||||
|
}
|
||||||
|
# `s` as a JSON string, quotes included
|
||||||
|
function pkg_jq(s: pointer) -> pointer {
|
||||||
|
var o = "\""
|
||||||
|
let n = slen(s)
|
||||||
|
var start = 0
|
||||||
|
var i = 0
|
||||||
|
while i < n {
|
||||||
|
let c = s[i] & 255
|
||||||
|
if c == '"' or c == 92 or c < 32 {
|
||||||
|
o = o + sslice(s, start, i) + pkg_jesc(c)
|
||||||
|
start = i + 1
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
return o + sslice(s, start, n) + "\""
|
||||||
|
}
|
||||||
|
# a lock line as the lock records it
|
||||||
|
function lock_entry_json(m: Manifest) -> pointer {
|
||||||
|
var o = `{{"name": {pkg_jq(m.module)}, "version": {pkg_jq(m.ver)}, "hash": {pkg_jq(m.hash)}, "kind": {pkg_jq(m.kind)}, "provides": [`
|
||||||
|
var p = 0
|
||||||
|
while p < len(m.provides) {
|
||||||
|
if p > 0 { o = o + ", " }
|
||||||
|
o = o + pkg_jq(m.provides[p])
|
||||||
|
p += 1
|
||||||
|
}
|
||||||
|
return o + "]}"
|
||||||
|
}
|
||||||
|
function lock_index(ms: []Manifest, module: pointer) -> int {
|
||||||
|
var i = 0
|
||||||
|
while i < len(ms) {
|
||||||
|
if ms[i].module == module { return i }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
function json_list(items: []pointer) -> pointer {
|
||||||
|
if len(items) == 0 { return "[]" }
|
||||||
|
var o = "["
|
||||||
|
var i = 0
|
||||||
|
while i < len(items) {
|
||||||
|
if i > 0 { o = o + "," }
|
||||||
|
o = o + nl() + " " + items[i]
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
return o + nl() + " ]"
|
||||||
|
}
|
||||||
|
# what changed between two locks: {"added", "removed", "changed", "unchanged"}. A module whose version
|
||||||
|
# or content hash moved is changed; `from` / `to` are its versions, the hashes beside them.
|
||||||
|
function lock_diff_json(before: []Manifest, after: []Manifest) -> pointer {
|
||||||
|
let added = new []pointer
|
||||||
|
let removed = new []pointer
|
||||||
|
let changed = new []pointer
|
||||||
|
var same = 0
|
||||||
|
var i = 0
|
||||||
|
while i < len(after) {
|
||||||
|
let m = after[i]
|
||||||
|
let at = lock_index(before, m.module)
|
||||||
|
if at < 0 { push(added, lock_entry_json(m)) }
|
||||||
|
else {
|
||||||
|
let o = before[at]
|
||||||
|
if o.ver == m.ver and o.hash == m.hash { same += 1 }
|
||||||
|
else { push(changed, `{{"name": {pkg_jq(m.module)}, "from": {pkg_jq(o.ver)}, "to": {pkg_jq(m.ver)}, "from_hash": {pkg_jq(o.hash)}, "to_hash": {pkg_jq(m.hash)}}}`) }
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
i = 0
|
||||||
|
while i < len(before) {
|
||||||
|
if lock_index(after, before[i].module) < 0 { push(removed, lock_entry_json(before[i])) }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
var o = "{" + nl() + ` "added": {json_list(added)},` + nl()
|
||||||
|
o = o + ` "removed": {json_list(removed)},` + nl()
|
||||||
|
o = o + ` "changed": {json_list(changed)},` + nl()
|
||||||
|
return o + ` "unchanged": {string(same)}` + nl() + "}" + nl()
|
||||||
|
}
|
||||||
|
function read_lock_or_empty() -> []Manifest {
|
||||||
|
let t = read_file("package.lock.ludic")
|
||||||
|
if t == null { return new []Manifest }
|
||||||
|
return parse_lock(t)
|
||||||
|
}
|
||||||
134
tools/ludic-cli/pkg_remove.ludic
Normal file
134
tools/ludic-cli/pkg_remove.ludic
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,13 @@ function pt_write(path: pointer, body: pointer) -> void {
|
||||||
write_file(path, body)
|
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`
|
# commit the current tree of `dir` and tag it `tag`
|
||||||
function pt_commit_tag(dir: pointer, tag: pointer) -> void {
|
function pt_commit_tag(dir: pointer, tag: pointer) -> void {
|
||||||
shell(`git -C {dir} add -A`)
|
shell(`git -C {dir} add -A`)
|
||||||
|
|
@ -170,6 +177,38 @@ function cmd_test_pkg() -> int {
|
||||||
ok("ludic vendor copies the resolved packages into ./vendor")
|
ok("ludic vendor copies the resolved packages into ./vendor")
|
||||||
} else { bad2("ludic vendor failed", capture_line(`tail -1 {work}/vendor.out`)) }
|
} 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) -------------
|
# ---- package-declarable engine-system + namespace (issue #62) -------------
|
||||||
# A source package registers a compile-time engine system (@EngineSystem) and a
|
# A source package registers a compile-time engine system (@EngineSystem) and a
|
||||||
# Foo.* namespace (@Namespace) with no compiler edit; a consumer game imports it
|
# Foo.* namespace (@Namespace) with no compiler edit; a consumer game imports it
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
# the built-in commands a hook can wrap (every user command that does work)
|
# the built-in commands a hook can wrap (every user command that does work)
|
||||||
function is_hookable(cmd: pointer) -> bool {
|
function is_hookable(cmd: pointer) -> bool {
|
||||||
return (cmd == "build") or (cmd == "run") or (cmd == "test") or (cmd == "deps") or (cmd == "schema") or (cmd == "migrate") or (cmd == "bundle") or (cmd == "pack") or (cmd == "clean") or (cmd == "fmt") or (cmd == "get") or (cmd == "add") or (cmd == "update") or (cmd == "verify") or (cmd == "vendor") or (cmd == "assets") or (cmd == "build-lib")
|
return (cmd == "build") or (cmd == "run") or (cmd == "test") or (cmd == "deps") or (cmd == "schema") or (cmd == "migrate") or (cmd == "bundle") or (cmd == "pack") or (cmd == "clean") or (cmd == "fmt") or (cmd == "get") or (cmd == "add") or (cmd == "remove") or (cmd == "update") or (cmd == "verify") or (cmd == "vendor") or (cmd == "assets") or (cmd == "build-lib")
|
||||||
}
|
}
|
||||||
|
|
||||||
# the directory this `ludic` binary lives in, with no trailing slash
|
# the directory this `ludic` binary lives in, with no trailing slash
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue