mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 05:28:31 +09:00
hopper: actually using remote mirror
This commit is contained in:
@@ -5,6 +5,9 @@
|
|||||||
|
|
||||||
const SYSTEM_PACKEAGE_DEF_DIR = "A:/tvdos/hopper"
|
const SYSTEM_PACKEAGE_DEF_DIR = "A:/tvdos/hopper"
|
||||||
const MANIFEST_EXT = "hop.per"
|
const MANIFEST_EXT = "hop.per"
|
||||||
|
const MIRROR_LIST_PATH = `${SYSTEM_PACKEAGE_DEF_DIR}/mirrors.list`
|
||||||
|
|
||||||
|
const net = require("A:/tvdos/include/net.mjs")
|
||||||
|
|
||||||
// SYNOPSIS
|
// SYNOPSIS
|
||||||
// hopper {search,se} [--provides, --requires, --description, --author] query
|
// hopper {search,se} [--provides, --requires, --description, --author] query
|
||||||
@@ -168,16 +171,54 @@ function parseRequires(s) {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HopperProvides entries are "<name>" or "<name> <version>". A bare name
|
||||||
|
// falls back to the package's own HopperPackageVersion — the same idea
|
||||||
|
// as RPM's `Provides: aalib = 1.2.0` (where the package's real name and
|
||||||
|
// version may differ from the virtual identity it exposes).
|
||||||
|
function parseProvides(s, fallbackVersion) {
|
||||||
|
const out = []
|
||||||
|
splitList(s || "").forEach(entry => {
|
||||||
|
const idx = entry.search(/\s+/)
|
||||||
|
if (idx < 0) {
|
||||||
|
out.push({ name: entry, version: fallbackVersion })
|
||||||
|
} else {
|
||||||
|
const v = entry.substring(idx + 1).trim()
|
||||||
|
out.push({ name: entry.substring(0, idx), version: v || fallbackVersion })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up the version a candidate exposes for `name`. If `name` matches
|
||||||
|
// the package's own name (or isn't declared in HopperProvides at all),
|
||||||
|
// returns the package's own version.
|
||||||
|
function providedVersionOf(candidate, name) {
|
||||||
|
if (candidate.provides) {
|
||||||
|
for (let i = 0; i < candidate.provides.length; i++) {
|
||||||
|
if (candidate.provides[i].name === name) return candidate.provides[i].version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return candidate.version
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Candidate index (installed + upstream)
|
// Candidate index (installed + upstream)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
function _manifestToCandidate(m, source) {
|
function _manifestToCandidate(m, source) {
|
||||||
|
const name = m.HopperPackageName || ""
|
||||||
|
const version = m.HopperPackageVersion || "0.0.0"
|
||||||
|
const provides = parseProvides(m.HopperProvides || "", version)
|
||||||
|
// Every package implicitly provides itself at its own version. Only
|
||||||
|
// synthesise this when the manifest didn't declare it explicitly.
|
||||||
|
if (name && !provides.some(p => p.name === name)) {
|
||||||
|
provides.unshift({ name: name, version: version })
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
name: m.HopperPackageName || "",
|
name: name,
|
||||||
version: m.HopperPackageVersion || "0.0.0",
|
version: version,
|
||||||
requires: parseRequires(m.HopperRequires || ""),
|
requires: parseRequires(m.HopperRequires || ""),
|
||||||
provides: splitList(m.HopperProvides || ""),
|
provides: provides,
|
||||||
source: source, // "installed" | "upstream"
|
source: source, // "installed" | "upstream"
|
||||||
manifest: m
|
manifest: m
|
||||||
}
|
}
|
||||||
@@ -200,18 +241,24 @@ function buildCandidateIndex() {
|
|||||||
return idx
|
return idx
|
||||||
}
|
}
|
||||||
|
|
||||||
// Anything that satisfies a requirement on `name`: package whose own name is
|
// Anything that satisfies a requirement on `name`: a package whose own
|
||||||
// `name`, OR whose HopperProvides includes `name`.
|
// HopperPackageName matches OR whose HopperProvides declares `name`.
|
||||||
|
// Each candidate now carries `provides` as {name, version} pairs; the
|
||||||
|
// package's own (name, version) is always present (see
|
||||||
|
// _manifestToCandidate), so a single pass over `provides` is enough.
|
||||||
function findProviders(idx, name) {
|
function findProviders(idx, name) {
|
||||||
const direct = idx.get(name) ? idx.get(name).slice() : []
|
const out = []
|
||||||
const indirect = []
|
const seen = new Set()
|
||||||
idx.forEach(candidates => {
|
idx.forEach(candidates => {
|
||||||
candidates.forEach(c => {
|
candidates.forEach(c => {
|
||||||
if (c.name === name) return // already in `direct`
|
if (seen.has(c)) return
|
||||||
if (c.provides.indexOf(name) >= 0) indirect.push(c)
|
if (c.provides.some(p => p.name === name)) {
|
||||||
|
out.push(c)
|
||||||
|
seen.add(c)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return direct.concat(indirect)
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort: installed first (no churn), then highest version, then upstream order.
|
// Sort: installed first (no churn), then highest version, then upstream order.
|
||||||
@@ -251,18 +298,22 @@ function resolveAll(idx, requirements) {
|
|||||||
function _resolve(reqName, constraint, trail) {
|
function _resolve(reqName, constraint, trail) {
|
||||||
const existing = chosen.get(reqName)
|
const existing = chosen.get(reqName)
|
||||||
if (existing !== undefined) {
|
if (existing !== undefined) {
|
||||||
return satisfies(existing.version, constraint)
|
const v = providedVersionOf(existing, reqName)
|
||||||
|
return satisfies(v, constraint)
|
||||||
? { ok: true }
|
? { ok: true }
|
||||||
: { ok: false, reason: `${reqName} pinned to ${existing.version}, but ${trail.join(" -> ")} requires ${constraint}` }
|
: { ok: false, reason: `${reqName} pinned to ${v}, but ${trail.join(" -> ")} requires ${constraint}` }
|
||||||
}
|
}
|
||||||
|
|
||||||
const providers = findProviders(idx, reqName)
|
const providers = findProviders(idx, reqName)
|
||||||
if (providers.length === 0) {
|
if (providers.length === 0) {
|
||||||
return { ok: false, reason: `no package provides "${reqName}" (required by ${trail.join(" -> ") || "<root>"})` }
|
return { ok: false, reason: `no package provides "${reqName}" (required by ${trail.join(" -> ") || "<root>"})` }
|
||||||
}
|
}
|
||||||
const matching = sortCandidates(providers.filter(c => satisfies(c.version, constraint)))
|
// Satisfaction checks the virtual version the candidate exposes
|
||||||
|
// for `reqName` (HopperProvides), not necessarily the package's
|
||||||
|
// own HopperPackageVersion.
|
||||||
|
const matching = sortCandidates(providers.filter(c => satisfies(providedVersionOf(c, reqName), constraint)))
|
||||||
if (matching.length === 0) {
|
if (matching.length === 0) {
|
||||||
const versions = providers.map(p => `${p.version}[${p.source}]`).join(", ")
|
const versions = providers.map(p => `${providedVersionOf(p, reqName)}[${p.source}]`).join(", ")
|
||||||
return { ok: false, reason: `no version of "${reqName}" satisfies ${constraint} (available: ${versions})` }
|
return { ok: false, reason: `no version of "${reqName}" satisfies ${constraint} (available: ${versions})` }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,102 +390,105 @@ function printPlan(actions, target) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Remote mirrors
|
||||||
|
// ============================================================
|
||||||
|
//
|
||||||
|
// `mirrors.list` lives next to the installed package manifests.
|
||||||
|
// Each non-empty, non-`#` line is the URL prefix of a Hopper mirror.
|
||||||
|
// The mirror MUST expose `<prefix>mirror_manifest` (key:value pairs
|
||||||
|
// describing the mirror) and `<prefix>filelist` (CSV with rows of
|
||||||
|
// `packagename,version,hoppermanifest-filename`).
|
||||||
|
//
|
||||||
|
// Trailing slash on the prefix is optional and will be added if missing.
|
||||||
|
|
||||||
|
function loadMirrorList() {
|
||||||
|
const f = files.open(MIRROR_LIST_PATH)
|
||||||
|
if (!f.exists || f.isDirectory) return []
|
||||||
|
return f.sread().split("\n")
|
||||||
|
.map(line => line.replace(/\r$/, "").trim())
|
||||||
|
.filter(line => line.length > 0 && line[0] !== "#")
|
||||||
|
.map(line => line.endsWith("/") ? line : (line + "/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFileList(text) {
|
||||||
|
const out = []
|
||||||
|
text.split("\n").forEach(raw => {
|
||||||
|
const line = raw.replace(/\r$/, "").trim()
|
||||||
|
if (line.length === 0 || line[0] === "#") return
|
||||||
|
const parts = line.split(",")
|
||||||
|
if (parts.length < 3) return
|
||||||
|
out.push({
|
||||||
|
name: parts[0].trim(),
|
||||||
|
version: parts[1].trim(),
|
||||||
|
file: parts[2].trim(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchManifestsFromMirror(prefix) {
|
||||||
|
const mfText = net.fetchText(prefix + "mirror_manifest")
|
||||||
|
if (mfText === null) {
|
||||||
|
printerrln(` ! could not reach mirror: ${prefix}`)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const mirror = parseManifest(mfText)
|
||||||
|
const mirrorName = mirror.HopperMirrorName || prefix
|
||||||
|
|
||||||
|
const flText = net.fetchText(prefix + "filelist")
|
||||||
|
if (flText === null) {
|
||||||
|
printerrln(` ! mirror "${mirrorName}" has no filelist`)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const out = []
|
||||||
|
parseFileList(flText).forEach(entry => {
|
||||||
|
const manifestText = net.fetchText(prefix + entry.file)
|
||||||
|
if (manifestText === null) {
|
||||||
|
printerrln(` ! mirror "${mirrorName}" missing ${entry.file}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const m = parseManifest(manifestText)
|
||||||
|
m._mirrorName = mirrorName
|
||||||
|
m._mirrorPrefix = prefix
|
||||||
|
m._manifestUrl = prefix + entry.file
|
||||||
|
out.push(m)
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-invocation memoisation. Search and install both pull the same
|
||||||
|
// data; we only want to hit the network once per `hopper ...` call.
|
||||||
|
let _remoteCache = null
|
||||||
|
|
||||||
|
function fetchRemoteCandidates() {
|
||||||
|
if (_remoteCache !== null) return _remoteCache
|
||||||
|
|
||||||
|
const mirrors = loadMirrorList()
|
||||||
|
if (mirrors.length === 0) {
|
||||||
|
_remoteCache = []
|
||||||
|
return _remoteCache
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!net.isAvailable()) {
|
||||||
|
printerrln("Warning: no HTTP modem attached; remote mirrors will be skipped.")
|
||||||
|
_remoteCache = []
|
||||||
|
return _remoteCache
|
||||||
|
}
|
||||||
|
|
||||||
|
const out = []
|
||||||
|
mirrors.forEach(prefix => {
|
||||||
|
fetchManifestsFromMirror(prefix).forEach(m => out.push(m))
|
||||||
|
})
|
||||||
|
_remoteCache = out
|
||||||
|
return _remoteCache
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Search
|
// Search
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
// 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 = [
|
|
||||||
// doomster: single version, needs libgl 1.*
|
|
||||||
{
|
|
||||||
HopperPackageName: "doomster", HopperPackageVersion: "0.9.3",
|
|
||||||
ProperName: "Doomster", ProperAuthor: "id Sortware",
|
|
||||||
ProperDescription: "First-person shooter game for TSVM",
|
|
||||||
HopperProvides: "doomster;", HopperRequires: "tvdos 1.*;libgl 1.*"
|
|
||||||
},
|
|
||||||
|
|
||||||
// libfft: three versions
|
|
||||||
{
|
|
||||||
HopperPackageName: "libfft", HopperPackageVersion: "0.1.0",
|
|
||||||
ProperName: "LibFFT", ProperAuthor: "Soraya Vaughn",
|
|
||||||
ProperDescription: "Fast Fourier Transform library for TSVM",
|
|
||||||
HopperProvides: "libfft;", HopperRequires: "tvdos 1.*"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
HopperPackageName: "libfft", HopperPackageVersion: "0.2.0",
|
|
||||||
ProperName: "LibFFT", ProperAuthor: "Soraya Vaughn",
|
|
||||||
ProperDescription: "Fast Fourier Transform library for TSVM",
|
|
||||||
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",
|
|
||||||
HopperProvides: "chatlite;", HopperRequires: "tvdos 1.*;wintex 1.*"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
HopperPackageName: "chatlite", HopperPackageVersion: "3.0.0",
|
|
||||||
ProperName: "ChatLite", ProperAuthor: "TerraNetworks Co.",
|
|
||||||
ProperDescription: "Lightweight IRC-style chat client",
|
|
||||||
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",
|
|
||||||
HopperProvides: "snakey;", 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 || "")
|
||||||
@@ -480,10 +534,16 @@ function cmdSearch(args) {
|
|||||||
else sysHits.forEach(m => printSearchResult(m, "installed"))
|
else sysHits.forEach(m => printSearchResult(m, "installed"))
|
||||||
|
|
||||||
println("")
|
println("")
|
||||||
println("Searching remote repository ...")
|
println("Searching remote mirrors ...")
|
||||||
const netHits = FAKE_REMOTE_PACKAGES.filter(m => matchesQuery(m, field, query))
|
const remote = fetchRemoteCandidates()
|
||||||
|
if (remote.length === 0) {
|
||||||
|
println(" (no mirrors configured or reachable)")
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const netHits = remote.filter(m => matchesQuery(m, field, query))
|
||||||
if (netHits.length === 0) println(" (no matches)")
|
if (netHits.length === 0) println(" (no matches)")
|
||||||
else netHits.forEach(m => printSearchResult(m, "remote"))
|
else netHits.forEach(m => printSearchResult(m, m._mirrorName || "remote"))
|
||||||
|
}
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|||||||
17
assets/disk0/tvdos/hopper/mirrors.list
Normal file
17
assets/disk0/tvdos/hopper/mirrors.list
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Hopper Mirror List
|
||||||
|
#
|
||||||
|
# One mirror per non-empty, non-comment line.
|
||||||
|
# Each entry is the remote URL prefix from which Hopper can fetch
|
||||||
|
# <prefix>mirror_manifest
|
||||||
|
# <prefix>filelist
|
||||||
|
# <prefix><package>.hop.per (one per row of filelist)
|
||||||
|
#
|
||||||
|
# `mirror_manifest` declares HopperMirrorName, HopperMirrorMaintainer
|
||||||
|
# and HopperMirrorRemotePrefix; `filelist` is CSV of
|
||||||
|
# packagename,version,hoppermanifest-filename
|
||||||
|
#
|
||||||
|
# Lines starting with `#` and empty lines are ignored.
|
||||||
|
# A trailing slash on the prefix is optional; Hopper will add one
|
||||||
|
# if missing.
|
||||||
|
|
||||||
|
https://raw.githubusercontent.com/curioustorvald/hopper-mirror/refs/heads/master/
|
||||||
@@ -22,7 +22,7 @@ import java.net.URL
|
|||||||
*/
|
*/
|
||||||
class HttpModem(private val vm: VM, private val artificialDelayBlockSize: Int = 1024, private val artificialDelayWaitTime: Int = -1) : BlockTransferInterface(false, true) {
|
class HttpModem(private val vm: VM, private val artificialDelayBlockSize: Int = 1024, private val artificialDelayWaitTime: Int = -1) : BlockTransferInterface(false, true) {
|
||||||
|
|
||||||
private val DBGPRN = true
|
private val DBGPRN = falsehopp
|
||||||
|
|
||||||
private fun printdbg(msg: Any) {
|
private fun printdbg(msg: Any) {
|
||||||
if (DBGPRN) println("[WgetModem] $msg")
|
if (DBGPRN) println("[WgetModem] $msg")
|
||||||
|
|||||||
Reference in New Issue
Block a user