mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-10 06:54:04 +09:00
more hopper stuffs
This commit is contained in:
29
assets/disk0/hopper/README.md
Normal file
29
assets/disk0/hopper/README.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
Hopper is a package manager for TVDOS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## For End Users
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## For Package Managers
|
||||||
|
|
||||||
|
A Hopper package is declared using the Hopper Manifest. Hopper Manifest has the following fields:
|
||||||
|
|
||||||
|
- **HopperManifestVersion.** The manifest version, always `1`
|
||||||
|
- **HopperPackageName.** Package name that Hopper understands
|
||||||
|
- **HopperPackageVersion.** The version. MUST STRICTLY follow Semantic Versioning 2.0.0
|
||||||
|
1. MAJOR version when you make incompatible API changes
|
||||||
|
2. MINOR version when you add functionality in a backward compatible manner
|
||||||
|
3. PATCH version when you make backward compatible bug fixes
|
||||||
|
- **HopperPackageMaintainer.** The maintainer of the package
|
||||||
|
- **HopperProvides.** (plural) What does your package provides
|
||||||
|
- **HopperRequires.** (plural) Dependencies
|
||||||
|
- **ProperName.** The displayed name of the package. Must be human-readable
|
||||||
|
- **ProperAuthor.** The displayed author of the package. Must be human-readable
|
||||||
|
- **ProperDescription.** Human-readable description of the package
|
||||||
|
- **Licence.** Licence of the package (e.g. `MIT`, `GPL-2.0-only`)
|
||||||
|
- **SupportMe.** (optional, plural) Any donation links
|
||||||
|
- **SystemPackagePath.** (for packages shipped with TVDOS only) path descriptor for the package file(s)
|
||||||
|
- **PackageFileList.** (for upstream packages, plural) HTTP(S) path for the files.
|
||||||
@@ -67,50 +67,374 @@ function findInstalledManifest(name) {
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSystemPackage(manifest) {
|
||||||
|
return !!(manifest.SystemPackagePath) // true if the field is truthy (not undefined, not empty string, not string '0', etc.)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yes/no prompt. Empty input falls back to `defaultYes`.
|
||||||
|
function confirm(prompt, defaultYes) {
|
||||||
|
const hint = defaultYes ? "[Y/n]" : "[y/N]"
|
||||||
|
print(`${prompt} ${hint} `)
|
||||||
|
const ans = (read() || "").trim().toLowerCase()
|
||||||
|
if (ans === "") return !!defaultYes
|
||||||
|
return ans === "y" || ans === "yes"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// SemVer (strict X.Y.Z) and constraint matching
|
||||||
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Versions are strict Semantic Versioning: three non-negative integer
|
||||||
|
// components MAJOR.MINOR.PATCH. No pre-release / build metadata.
|
||||||
|
//
|
||||||
|
// Constraint grammar (intentionally small, expandable later):
|
||||||
|
// * any version
|
||||||
|
// X.* major X, any minor/patch
|
||||||
|
// X.Y.* major X, minor Y, any patch
|
||||||
|
// X.Y.Z exact
|
||||||
|
// ^X.Y.Z >= X.Y.Z and < (X+1).0.0 (major-compatible)
|
||||||
|
// ~X.Y.Z >= X.Y.Z and < X.(Y+1).0 (minor-compatible)
|
||||||
|
// >=X.Y.Z / >X.Y.Z / <=X.Y.Z / <X.Y.Z / =X.Y.Z
|
||||||
|
//
|
||||||
|
// Multiple comma-separated constraints are AND-ed: "^1.2.0,<1.5.0".
|
||||||
|
|
||||||
|
function parseVersion(v) {
|
||||||
|
const m = String(v || "0.0.0").trim().match(/^(\d+)\.(\d+)\.(\d+)$/)
|
||||||
|
if (!m) return [0, 0, 0]
|
||||||
|
return [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)]
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareVersion(a, b) {
|
||||||
|
const A = parseVersion(a), B = parseVersion(b)
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
if (A[i] !== B[i]) return (A[i] < B[i]) ? -1 : 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function _matchSingleConstraint(version, c) {
|
||||||
|
c = c.trim()
|
||||||
|
if (c === "" || c === "*") return true
|
||||||
|
|
||||||
|
// Operator form: ^, ~, >=, <=, >, <, =
|
||||||
|
let opMatch = c.match(/^(\^|~|>=|<=|>|<|=)\s*(\d+\.\d+\.\d+)$/)
|
||||||
|
if (opMatch) {
|
||||||
|
const op = opMatch[1]
|
||||||
|
const target = opMatch[2]
|
||||||
|
const cmp = compareVersion(version, target)
|
||||||
|
const [tM, tm] = parseVersion(target)
|
||||||
|
switch (op) {
|
||||||
|
case "=": return cmp === 0
|
||||||
|
case ">": return cmp > 0
|
||||||
|
case ">=": return cmp >= 0
|
||||||
|
case "<": return cmp < 0
|
||||||
|
case "<=": return cmp <= 0
|
||||||
|
case "^": return cmp >= 0 && compareVersion(version, `${tM + 1}.0.0`) < 0
|
||||||
|
case "~": return cmp >= 0 && compareVersion(version, `${tM}.${tm + 1}.0`) < 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wildcard form: X.*, X.Y.*, X.x, X.Y.x, or exact X.Y.Z
|
||||||
|
const parts = c.split(".")
|
||||||
|
const vparts = parseVersion(version)
|
||||||
|
for (let i = 0; i < parts.length && i < 3; i++) {
|
||||||
|
if (parts[i] === "*" || parts[i] === "x" || parts[i] === "X") return true
|
||||||
|
const expected = parseInt(parts[i], 10)
|
||||||
|
if (isNaN(expected) || vparts[i] !== expected) return false
|
||||||
|
}
|
||||||
|
// All listed parts matched literally; remaining parts (if any) must be 0
|
||||||
|
for (let i = parts.length; i < 3; i++) {
|
||||||
|
if (vparts[i] !== 0) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function satisfies(version, constraint) {
|
||||||
|
if (!constraint) return true
|
||||||
|
return constraint.split(",").every(c => _matchSingleConstraint(version, c))
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRequires(s) {
|
||||||
|
const out = []
|
||||||
|
splitList(s || "").forEach(entry => {
|
||||||
|
// "<name>" or "<name> <constraint>"
|
||||||
|
const idx = entry.search(/\s+/)
|
||||||
|
if (idx < 0) {
|
||||||
|
out.push({ name: entry, constraint: "*" })
|
||||||
|
} else {
|
||||||
|
out.push({ name: entry.substring(0, idx), constraint: entry.substring(idx + 1).trim() })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Candidate index (installed + upstream)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
function _manifestToCandidate(m, source) {
|
||||||
|
return {
|
||||||
|
name: m.HopperPackageName || "",
|
||||||
|
version: m.HopperPackageVersion || "0.0.0",
|
||||||
|
requires: parseRequires(m.HopperRequires || ""),
|
||||||
|
provides: splitList(m.HopperProvides || ""),
|
||||||
|
source: source, // "installed" | "upstream"
|
||||||
|
manifest: m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns map: packageName -> array<Candidate>
|
||||||
|
function buildCandidateIndex() {
|
||||||
|
const idx = new Map()
|
||||||
|
function add(c) {
|
||||||
|
if (!idx.has(c.name)) idx.set(c.name, [])
|
||||||
|
// De-dupe (name+version+source)
|
||||||
|
const arr = idx.get(c.name)
|
||||||
|
if (arr.some(x => x.version === c.version && x.source === c.source)) return
|
||||||
|
arr.push(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
listInstalledManifests().forEach(m => add(_manifestToCandidate(m, "installed")))
|
||||||
|
fetchRemoteCandidates().forEach(m => add(_manifestToCandidate(m, "upstream")))
|
||||||
|
|
||||||
|
return idx
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anything that satisfies a requirement on `name`: package whose own name is
|
||||||
|
// `name`, OR whose HopperProvides includes `name`.
|
||||||
|
function findProviders(idx, name) {
|
||||||
|
const direct = idx.get(name) ? idx.get(name).slice() : []
|
||||||
|
const indirect = []
|
||||||
|
idx.forEach(candidates => {
|
||||||
|
candidates.forEach(c => {
|
||||||
|
if (c.name === name) return // already in `direct`
|
||||||
|
if (c.provides.indexOf(name) >= 0) indirect.push(c)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return direct.concat(indirect)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort: installed first (no churn), then highest version, then upstream order.
|
||||||
|
function sortCandidates(cands) {
|
||||||
|
return cands.slice().sort((a, b) => {
|
||||||
|
if (a.source !== b.source) return (a.source === "installed") ? -1 : 1
|
||||||
|
return -compareVersion(a.version, b.version)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Resolver (snapshot-based backtracking; precursor to a SAT solver)
|
||||||
|
// ============================================================
|
||||||
|
//
|
||||||
|
// State: chosen :: Map<packageName, Candidate>
|
||||||
|
// At every choice point we snapshot the whole map so that backtracking
|
||||||
|
// also undoes any transitive picks. The candidate ordering encodes the
|
||||||
|
// preference policy:
|
||||||
|
//
|
||||||
|
// 1. Keep installed if it satisfies the constraint.
|
||||||
|
// 2. Otherwise pick the newest upstream version that satisfies.
|
||||||
|
// 3. If newer versions cause downstream conflicts, walk older versions
|
||||||
|
// (downgrade) until either something fits or candidates are exhausted.
|
||||||
|
//
|
||||||
|
// The structure is intentionally close to DPLL: each "decision" is the
|
||||||
|
// candidate we assign to a variable, and "unit propagation" is the
|
||||||
|
// recursive resolve() call over each requirement. Replacing this with
|
||||||
|
// clause learning / a watched-literals scheme later would be local.
|
||||||
|
|
||||||
|
function resolveAll(idx, requirements) {
|
||||||
|
const chosen = new Map()
|
||||||
|
const issues = []
|
||||||
|
|
||||||
|
function snapshot() { return new Map(chosen) }
|
||||||
|
function restore(snap) { chosen.clear(); snap.forEach((v, k) => chosen.set(k, v)) }
|
||||||
|
|
||||||
|
function _resolve(reqName, constraint, trail) {
|
||||||
|
const existing = chosen.get(reqName)
|
||||||
|
if (existing !== undefined) {
|
||||||
|
return satisfies(existing.version, constraint)
|
||||||
|
? { ok: true }
|
||||||
|
: { ok: false, reason: `${reqName} pinned to ${existing.version}, but ${trail.join(" -> ")} requires ${constraint}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
const providers = findProviders(idx, reqName)
|
||||||
|
if (providers.length === 0) {
|
||||||
|
return { ok: false, reason: `no package provides "${reqName}" (required by ${trail.join(" -> ") || "<root>"})` }
|
||||||
|
}
|
||||||
|
const matching = sortCandidates(providers.filter(c => satisfies(c.version, constraint)))
|
||||||
|
if (matching.length === 0) {
|
||||||
|
const versions = providers.map(p => `${p.version}[${p.source}]`).join(", ")
|
||||||
|
return { ok: false, reason: `no version of "${reqName}" satisfies ${constraint} (available: ${versions})` }
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastReason = null
|
||||||
|
for (let i = 0; i < matching.length; i++) {
|
||||||
|
const cand = matching[i]
|
||||||
|
const snap = snapshot()
|
||||||
|
chosen.set(cand.name, cand)
|
||||||
|
|
||||||
|
let allOk = true
|
||||||
|
const subTrail = trail.concat([`${cand.name}@${cand.version}`])
|
||||||
|
for (let j = 0; j < cand.requires.length; j++) {
|
||||||
|
const req = cand.requires[j]
|
||||||
|
const r = _resolve(req.name, req.constraint, subTrail)
|
||||||
|
if (!r.ok) {
|
||||||
|
allOk = false
|
||||||
|
lastReason = r.reason
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (allOk) return { ok: true }
|
||||||
|
restore(snap)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: false, reason: lastReason || `no working candidate for "${reqName}"` }
|
||||||
|
}
|
||||||
|
|
||||||
|
requirements.forEach(req => {
|
||||||
|
const r = _resolve(req.name, req.constraint, [])
|
||||||
|
if (!r.ok) issues.push(r.reason)
|
||||||
|
})
|
||||||
|
|
||||||
|
return { chosen, issues }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare resolved assignment against currently-installed state.
|
||||||
|
function classifyPlan(idx, chosen) {
|
||||||
|
const installedByName = new Map()
|
||||||
|
listInstalledManifests().forEach(m => installedByName.set(m.HopperPackageName, m))
|
||||||
|
|
||||||
|
const actions = []
|
||||||
|
chosen.forEach((cand, name) => {
|
||||||
|
const inst = installedByName.get(name)
|
||||||
|
if (cand.source === "installed") {
|
||||||
|
actions.push({ action: "keep", name, version: cand.version })
|
||||||
|
}
|
||||||
|
else if (inst === undefined) {
|
||||||
|
actions.push({ action: "install", name, version: cand.version })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const cmp = compareVersion(cand.version, inst.HopperPackageVersion)
|
||||||
|
if (cmp > 0) actions.push({ action: "upgrade", name, from: inst.HopperPackageVersion, to: cand.version })
|
||||||
|
else if (cmp < 0) actions.push({ action: "downgrade", name, from: inst.HopperPackageVersion, to: cand.version })
|
||||||
|
else actions.push({ action: "reinstall", name, version: cand.version })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return actions
|
||||||
|
}
|
||||||
|
|
||||||
|
function printPlan(actions, target) {
|
||||||
|
const changing = actions.filter(a => a.action !== "keep")
|
||||||
|
if (changing.length === 0) {
|
||||||
|
println(`Nothing to do: ${target} is already installed and satisfied.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
println("Plan:")
|
||||||
|
changing.forEach(a => {
|
||||||
|
switch (a.action) {
|
||||||
|
case "install": println(` + install ${a.name} ${a.version}`); break
|
||||||
|
case "upgrade": println(` ^ upgrade ${a.name} ${a.from} -> ${a.to}`); break
|
||||||
|
case "downgrade": println(` v downgrade ${a.name} ${a.from} -> ${a.to}`); break
|
||||||
|
case "reinstall": println(` = reinstall ${a.name} ${a.version}`); break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Search
|
// Search
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
// Dummy "remote" repository -- pretends to be a network query result.
|
// Dummy "remote" repository -- pretends to be a network query result.
|
||||||
|
// Multiple entries per HopperPackageName represent multiple available
|
||||||
|
// versions; the resolver picks among them.
|
||||||
const FAKE_REMOTE_PACKAGES = [
|
const FAKE_REMOTE_PACKAGES = [
|
||||||
|
// doomster: single version, needs libgl 1.*
|
||||||
{
|
{
|
||||||
HopperPackageName: "doomster",
|
HopperPackageName: "doomster", HopperPackageVersion: "0.9.3",
|
||||||
HopperPackageVersion: "0.9.3",
|
ProperName: "Doomster", ProperAuthor: "id Sortware",
|
||||||
ProperName: "Doomster",
|
|
||||||
ProperAuthor: "id Sortware",
|
|
||||||
ProperDescription: "First-person shooter game for TSVM",
|
ProperDescription: "First-person shooter game for TSVM",
|
||||||
HopperProvides: "doomster;",
|
HopperProvides: "doomster;", HopperRequires: "tvdos 1.*;libgl 1.*"
|
||||||
HopperRequires: "tvdos 1.*;libgl 1.*"
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// libfft: three versions
|
||||||
{
|
{
|
||||||
HopperPackageName: "libfft",
|
HopperPackageName: "libfft", HopperPackageVersion: "0.1.0",
|
||||||
HopperPackageVersion: "0.1.0",
|
ProperName: "LibFFT", ProperAuthor: "Soraya Vaughn",
|
||||||
ProperName: "LibFFT",
|
|
||||||
ProperAuthor: "Soraya Vaughn",
|
|
||||||
ProperDescription: "Fast Fourier Transform library for TSVM",
|
ProperDescription: "Fast Fourier Transform library for TSVM",
|
||||||
HopperProvides: "libfft;",
|
HopperProvides: "libfft;", HopperRequires: "tvdos 1.*"
|
||||||
HopperRequires: "tvdos 1.*"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
HopperPackageName: "chatlite",
|
HopperPackageName: "libfft", HopperPackageVersion: "0.2.0",
|
||||||
HopperPackageVersion: "2.1.5",
|
ProperName: "LibFFT", ProperAuthor: "Soraya Vaughn",
|
||||||
ProperName: "ChatLite",
|
ProperDescription: "Fast Fourier Transform library for TSVM",
|
||||||
ProperAuthor: "TerraNetworks Co.",
|
HopperProvides: "libfft;", HopperRequires: "tvdos 1.*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
HopperPackageName: "libfft", HopperPackageVersion: "1.0.0",
|
||||||
|
ProperName: "LibFFT", ProperAuthor: "Soraya Vaughn",
|
||||||
|
ProperDescription: "Fast Fourier Transform library for TSVM",
|
||||||
|
HopperProvides: "libfft;", HopperRequires: "tvdos 1.*"
|
||||||
|
},
|
||||||
|
|
||||||
|
// chatlite: 2.1.5 fits installed wintex 1.*; 3.0.0 demands wintex 2.*
|
||||||
|
{
|
||||||
|
HopperPackageName: "chatlite", HopperPackageVersion: "2.1.5",
|
||||||
|
ProperName: "ChatLite", ProperAuthor: "TerraNetworks Co.",
|
||||||
ProperDescription: "Lightweight IRC-style chat client",
|
ProperDescription: "Lightweight IRC-style chat client",
|
||||||
HopperProvides: "chatlite;",
|
HopperProvides: "chatlite;", HopperRequires: "tvdos 1.*;wintex 1.*"
|
||||||
HopperRequires: "tvdos 1.*;wintex 1.*"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
HopperPackageName: "snakey",
|
HopperPackageName: "chatlite", HopperPackageVersion: "3.0.0",
|
||||||
HopperPackageVersion: "1.4.0",
|
ProperName: "ChatLite", ProperAuthor: "TerraNetworks Co.",
|
||||||
ProperName: "Snakey",
|
ProperDescription: "Lightweight IRC-style chat client",
|
||||||
ProperAuthor: "Iben Holst",
|
HopperProvides: "chatlite;", HopperRequires: "tvdos 1.*;wintex 2.*"
|
||||||
|
},
|
||||||
|
|
||||||
|
// snakey
|
||||||
|
{
|
||||||
|
HopperPackageName: "snakey", HopperPackageVersion: "1.4.0",
|
||||||
|
ProperName: "Snakey", ProperAuthor: "Iben Holst",
|
||||||
ProperDescription: "Classic snake game with TerranBASIC scripting",
|
ProperDescription: "Classic snake game with TerranBASIC scripting",
|
||||||
HopperProvides: "snakey;",
|
HopperProvides: "snakey;", HopperRequires: "tvdos 1.*;libterranbasic 1.*"
|
||||||
HopperRequires: "tvdos 1.*;libterranbasic 1.*"
|
},
|
||||||
}
|
|
||||||
|
// libgl future version (lets superchef pull in an upgrade)
|
||||||
|
{
|
||||||
|
HopperPackageName: "libgl", HopperPackageVersion: "2.0.0",
|
||||||
|
ProperName: "LibGL", ProperAuthor: "CuriousTorvald",
|
||||||
|
ProperDescription: "TVDOS Graphics Library, next-generation",
|
||||||
|
HopperProvides: "libgl;", HopperRequires: ""
|
||||||
|
},
|
||||||
|
|
||||||
|
// superchef: requires libgl 2.* -- triggers an upgrade of installed libgl
|
||||||
|
{
|
||||||
|
HopperPackageName: "superchef", HopperPackageVersion: "1.0.0",
|
||||||
|
ProperName: "SuperChef", ProperAuthor: "Pavlo Kvasnik",
|
||||||
|
ProperDescription: "Recipe-driven build automation",
|
||||||
|
HopperProvides: "superchef;", HopperRequires: "tvdos 1.*;libgl 2.*"
|
||||||
|
},
|
||||||
|
|
||||||
|
// phantomedit: needs something that does not exist -> unresolvable
|
||||||
|
{
|
||||||
|
HopperPackageName: "phantomedit", HopperPackageVersion: "0.1.0",
|
||||||
|
ProperName: "PhantomEdit", ProperAuthor: "anonymous",
|
||||||
|
ProperDescription: "Editor for non-existent files",
|
||||||
|
HopperProvides: "phantomedit;", HopperRequires: "libquantum 1.*"
|
||||||
|
},
|
||||||
|
|
||||||
|
// fake updated version of Microtone
|
||||||
|
{
|
||||||
|
HopperPackageName: "microtone", HopperPackageVersion: "1.3.0",
|
||||||
|
ProperName: "Microtone", ProperAuthor: "CuriousTorvald",
|
||||||
|
ProperDescription: "Fake updated version of Microtone",
|
||||||
|
HopperProvides: "microtone;", HopperRequires: "tvdos 1.*;libgl 2.*"
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Indirection point: in the future this should hit a real upstream.
|
||||||
|
function fetchRemoteCandidates() {
|
||||||
|
return FAKE_REMOTE_PACKAGES
|
||||||
|
}
|
||||||
|
|
||||||
function fieldCandidates(manifest, field) {
|
function fieldCandidates(manifest, field) {
|
||||||
switch (field) {
|
switch (field) {
|
||||||
case "provides": return splitList(manifest.HopperProvides || "")
|
case "provides": return splitList(manifest.HopperProvides || "")
|
||||||
@@ -181,14 +505,60 @@ function cmdInstall(args) {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
const verSuffix = version ? ` (v${version})` : ""
|
const targetConstraint = version || "*"
|
||||||
|
const verSuffix = (targetConstraint !== "*") ? ` (${targetConstraint})` : ""
|
||||||
println(`Resolving ${query}${verSuffix} ...`)
|
println(`Resolving ${query}${verSuffix} ...`)
|
||||||
println(`Fetching manifest from remote ...`)
|
|
||||||
println(`Resolving dependencies ...`)
|
const idx = buildCandidateIndex()
|
||||||
println(`Downloading package payload ...`)
|
|
||||||
println(`Verifying integrity ...`)
|
// Sanity check: target must exist in the index (installed or upstream).
|
||||||
println(`Writing manifest to ${SYSTEM_PACKEAGE_DEF_DIR}/${query}${MANIFEST_EXT} ...`)
|
if (findProviders(idx, query).length === 0) {
|
||||||
println(`Installed ${query}${verSuffix}.`)
|
printerrln(`Error: package "${query}" not found (not on upstream, not installed).`)
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed order matters: the target goes FIRST so its (possibly tight)
|
||||||
|
// constraints can drive upgrades of dependencies. The installed-set
|
||||||
|
// requirements follow at "*" so the resolver still has to keep them
|
||||||
|
// alive (preferring installed candidates when their version still fits,
|
||||||
|
// otherwise upgrading or downgrading them).
|
||||||
|
const seed = [{ name: query, constraint: targetConstraint }]
|
||||||
|
listInstalledManifests().forEach(m => {
|
||||||
|
if (m.HopperPackageName === query) return
|
||||||
|
seed.push({ name: m.HopperPackageName, constraint: "*" })
|
||||||
|
})
|
||||||
|
|
||||||
|
const { chosen, issues } = resolveAll(idx, seed)
|
||||||
|
if (issues.length > 0) {
|
||||||
|
printerrln("Resolution failed:")
|
||||||
|
issues.forEach(reason => printerrln(` - ${reason}`))
|
||||||
|
printerrln("")
|
||||||
|
printerrln("No solution found -- not installable.")
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
|
||||||
|
const plan = classifyPlan(idx, chosen)
|
||||||
|
printPlan(plan, query)
|
||||||
|
|
||||||
|
const changing = plan.filter(a => a.action !== "keep")
|
||||||
|
if (changing.length === 0) return 0
|
||||||
|
|
||||||
|
println("")
|
||||||
|
if (!confirm("Proceed with installation?", true)) {
|
||||||
|
println("Aborted.")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
println("Fetching manifests from remote ...")
|
||||||
|
println("Downloading package payloads ...")
|
||||||
|
println("Verifying integrity ...")
|
||||||
|
changing.forEach(a => {
|
||||||
|
if (a.action === "install" || a.action === "reinstall") {
|
||||||
|
println(` ${a.action} ${a.name} ${a.version}`)
|
||||||
|
} else {
|
||||||
|
println(` ${a.action} ${a.name} ${a.from} -> ${a.to}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
println("(dummy install: no files were actually created)")
|
println("(dummy install: no files were actually created)")
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -262,6 +632,11 @@ function cmdRemove(args) {
|
|||||||
println(` ${m._manifestPath}`)
|
println(` ${m._manifestPath}`)
|
||||||
|
|
||||||
println("")
|
println("")
|
||||||
|
if (!confirm("Proceed with removal?", false)) {
|
||||||
|
println("Aborted.")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
println("(dry-run: no files were actually deleted)")
|
println("(dry-run: no files were actually deleted)")
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user