Files
HyperionOS/Src/spm/data/bin/spm
T
2026-08-15 22:38:35 -04:00

734 lines
23 KiB
Plaintext

--:Minify:--
local spm = function(...)
if syscall.geteuid()~=0 then print("Permission denied") return end
local inpArgs = { ... }
local args = {}
local cloptions = {
S = false,
y = false,
u = false,
R = false,
h = false,
r = false,
Q = false,
l = false,
o = false,
i = false,
e = false,
t = false,
s = false,
sysroot = "",
init = false,
help = false,
overwrite = false
}
local i = 1
local fs = require("fs")
local http = require("http")
local json = require("json")
local tar = require("tar")
local minify = require("minify")
local deflate = require("deflate")
local bit32 = require("bit32")
local httpcache = {}
local function get(url)
if httpcache[url] then return httpcache[url] end
local ret = http.get(url)
httpcache[url] = ret
return ret
end
while i <= #inpArgs do
local v = inpArgs[i]
if v:sub(1, 2) == "--" then
local opt, value = v:match("^%-%-(.-)=(.*)$")
if not opt then
opt = v:sub(3)
end
if cloptions[opt] == nil then
print("spm: unrecognized option '" .. v .. "'.")
print("try 'spm --help' for more information.")
return
end
if type(cloptions[opt]) == "boolean" then
cloptions[opt] = true
else
if value == nil then
i = i + 1
value = inpArgs[i]
end
if value == nil then
print("spm: option '--" .. opt .. "' requires an argument.")
return
end
cloptions[opt] = value
end
elseif v:sub(1, 1) == "-" then
for j = 2, #v do
local opt = v:sub(j, j)
if cloptions[opt] == nil then
print("spm: invalid option '-" .. opt .. "'.")
print("try 'spm --help' for more information.")
return
end
if type(cloptions[opt]) ~= "boolean" then
print("spm: option '-" .. opt .. "' requires the long form '--" .. opt .. "=value'.")
return
end
cloptions[opt] = true
end
else
table.insert(args, v)
end
i = i + 1
end
i=nil
if cloptions.help or cloptions.h then
if cloptions.S then
print("spm -S - Synchronize Packages")
print("")
print("Usage:")
print(" spm -S [options] <packages>")
print("")
print("Options:")
print(" -y Refresh package databases")
print(" -u Upgrade installed packages")
print(" -i Show remote package information")
print(" -s Download source")
print("")
print("Examples:")
print(" spm -Sy")
print(" spm -S package")
print(" spm -Syu")
return
elseif cloptions.Q then
print("spm -Q - Query Package Database")
print("")
print("Usage:")
print(" spm -Q [options] [package]")
print("")
print("Options:")
print(" -i Show installed package information")
print(" -l List files owned by a package")
print(" -o Find which package owns a file")
print(" -e List explicitly installed packages")
print(" -t List orphan packages")
print("")
print("Examples:")
print(" spm -Q")
print(" spm -Qi lua")
print(" spm -Ql lua")
print(" spm -Qo /bin/lua")
return
elseif cloptions.R then
print("spm -R - Remove Packages")
print("")
print("Usage:")
print(" spm -R [options] <packages>")
print("")
print("Currently no remove-specific options are implemented.")
print("")
print("Example:")
print(" spm -R package")
return
else
print("spm - HyperionOS Package Manager")
print("")
print("Usage:")
print(" spm <operation> [options] [targets]")
print("")
print("Operations:")
print(" -S Synchronize packages")
print(" -Q Query package database")
print(" -R Remove packages")
print("")
print("General Options:")
print(" -h, --help Show help")
print(" --init Initialize spm")
print(" --sysroot DIR Operate on alternate root")
print("")
print("For operation-specific help:")
print(" spm -Sh")
print(" spm -Qh")
print(" spm -Rh")
return
end
end
if cloptions.sysroot=="" then
syscall.chdir("/")
else
syscall.chdir(cloptions.sysroot)
end
if cloptions.init then
fs.mkdir("var/spm/cache/")
fs.mkdir("var/spm/db/")
fs.writeAllText("var/spm/db/repos.list","")
fs.mkdir("var/spm/db/installed/")
fs.mkdir("etc/spm/")
fs.writeAllText("etc/spm/sources.list","https://git.astronand.dev/Hyperion/HyperionOS/raw/branch/main/spm.json\nhttp://localhost:8000/spm.json")
end
if not fs.exists("var/spm/") or not fs.exists("etc/spm/") then
print("spm not initialized run \"spm --init\"")
return
end
local log = fs.open("var/log/spm.log", "w")
local function w(...)
local args = {...}
local output = ""
for i = 1, #args do output = output .. tostring(args[i]) .. "\t" end
output = output:sub(1, -2)
syscall.write(1, output.."\n")
log.write(output.."\n")
log.flush()
end
local repodb = string.split(fs.readAllText("var/spm/db/repos.list"), "\n")
local sourcelist = string.split(fs.readAllText("etc/spm/sources.list"), "\n")
local pkgdblist = fs.list("var/spm/db/installed/")
local pkgdb = {}
for _,v in ipairs(pkgdblist) do
local file = fs.readAllText("var/spm/db/installed/"..v)
pkgdb[v] = json.decode(file)
end
local function printpkgs(list)
local colWidth=0
local tmp = syscall.devctl(1, "size")
local sizeX, sizeY = tonumber(tmp:sub(1, tmp:find(";")-1)), tonumber(tmp:sub(tmp:find(";")+1))
for _, v in ipairs(list) do
if #v + 2 > colWidth then colWidth = #v + 2 end
end
local numCols = math.max(1, math.floor(sizeX / colWidth))
for i, v in ipairs(list) do
printInline(v.id)
printInline((" "):rep(colWidth - #v))
if i % numCols == 0 then print("") end
end
if #list % numCols ~= 0 then print("") end
end
local function checkRepo(repo)
local resp = get(repo)
if not resp then return false end
if resp.code ~= 200 then
return false
end
local repojson = json.decode(resp.body)
if not repojson then
return false
end
return true, repojson
end
local function getRepos(root)
local repos = {}
local visited = {}
local queue = table.values(root)
local head = 1
while head <= #queue do
local url = queue[head]
head = head + 1
if not visited[url] then
visited[url] = true
print("GET:"..tostring(head-1).." "..url)
local ok, repo = checkRepo(url)
if ok then
table.insert(repos, url)
if repo.refs then
for _, ref in ipairs(repo.refs) do
if not visited[ref] then
queue[#queue + 1] = ref
end
end
end
end
end
end
return repos
end
local function getPkg(name)
for _, url in ipairs(repodb) do
local ok, repo = checkRepo(url)
if not ok then
w("Failed to contact repository:")
w(url)
w("Consider running 'spm -Sy' to remove broken repositories.")
else
if repo.packages then
if repo.packages[name] then
local pkgurl = repo.packages[name]
local resp = get(pkgurl)
if not resp then
w("Repository: "..url)
w("Has broken package: "..name)
w("Ignoring")
else
if resp.code ~= 200 then
w("Repository: "..url)
w("Has broken package: "..name)
w("Ignoring")
else
local pkgjson = json.decode(resp.body)
if not pkgjson then
w("Repository: "..url)
w("Has broken package: "..name)
w("Ignoring")
else
return {
url=pkgurl,
tar=pkgjson.tarball,
hash=pkgjson.hash,
id=pkgjson.id,
deps=pkgjson.dependencies,
desc=pkgjson.description,
auth=pkgjson.authors,
ver=pkgjson.version
}
end
end
end
end
end
end
end
return false, "target not found: "..name
end
local function flatten(t)
local ret={}
for _,v in ipairs(t.content) do
if v.type == "dir" then
local f=flatten(v)
for i,c in pairs(f) do
ret[i]=c
end
elseif v.type == "file" then
ret[v.name]=v.content
end
end
return ret
end
local function ptar(t, pkg)
t=t.content[1]
if t.name~=pkg.id then w("Tar contains "..t.name.." not "..pkg.id) return false end
local ret={}
for i=1, #t.content do
if t.content[i].name == "data" then
local data=flatten(t.content[i])
local new={}
for p,v in pairs(data) do
new[p:sub(#pkg.id+7)]=v
end
ret.data=new
elseif t.content[i].name == "control" then
local data=flatten(t.content[i])
local new={}
for p,v in pairs(data) do
new[p:sub(#pkg.id+10)]=v
end
ret.control=new
end
end
return ret
end
local function mini(tabl)
local ret={}
for i,v in pairs(tabl) do
if v:sub(1,12)=="--:Minify:--" then
local ok, code = pcall(minify.minify,v)
if not ok then w("Failed to minify "..i..":\n"..code) ret[i]=v
else ret[i]=code end
else
ret[i]=v
end
end
return ret
end
local function md5(msg)
local bit = bit32
local function rol(x, n)
return bit.lrotate(x, n)
end
local function F(x, y, z)
return bit.bor(bit.band(x, y), bit.band(bit.bnot(x), z))
end
local function G(x, y, z)
return bit.bor(bit.band(x, z), bit.band(y, bit.bnot(z)))
end
local function H(x, y, z)
return bit.bxor(x, y, z)
end
local function I(x, y, z)
return bit.bxor(y, bit.bor(x, bit.bnot(z)))
end
local function u32le(s, i)
return s:byte(i)
+ s:byte(i + 1) * 0x100
+ s:byte(i + 2) * 0x10000
+ s:byte(i + 3) * 0x1000000
end
local function le32(x)
return string.char(
bit.band(x, 0xff),
bit.band(bit.rshift(x, 8), 0xff),
bit.band(bit.rshift(x, 16), 0xff),
bit.band(bit.rshift(x, 24), 0xff)
)
end
-- MD5 padding
local bitlen = #msg * 8
msg = msg .. "\128"
while #msg % 64 ~= 56 do
msg = msg .. "\0"
end
-- Append 64-bit little-endian length
local lo = bitlen % 0x100000000
local hi = math.floor(bitlen / 0x100000000)
msg = msg .. le32(lo) .. le32(hi)
local a0 = 0x67452301
local b0 = 0xefcdab89
local c0 = 0x98badcfe
local d0 = 0x10325476
local S = {
7,12,17,22, 7,12,17,22, 7,12,17,22, 7,12,17,22,
5, 9,14,20, 5, 9,14,20, 5, 9,14,20, 5, 9,14,20,
4,11,16,23, 4,11,16,23, 4,11,16,23, 4,11,16,23,
6,10,15,21, 6,10,15,21, 6,10,15,21, 6,10,15,21
}
local K = {
0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05,
0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039,
0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391
}
for chunk = 1, #msg, 64 do
local M = {}
for i = 0, 15 do
M[i] = u32le(msg, chunk + i * 4)
end
local A = a0
local B = b0
local C = c0
local D = d0
for i = 0, 63 do
local f
local g
if i < 16 then
f = F(B, C, D)
g = i
elseif i < 32 then
f = G(B, C, D)
g = (5 * i + 1) % 16
elseif i < 48 then
f = H(B, C, D)
g = (3 * i + 5) % 16
else
f = I(B, C, D)
g = (7 * i) % 16
end
local tmp = D
D = C
C = B
local x = A + f + K[i + 1] + M[g]
x = x % 0x100000000
B = (B + rol(x, S[i + 1])) % 0x100000000
A = tmp
end
a0 = (a0 + A) % 0x100000000
b0 = (b0 + B) % 0x100000000
c0 = (c0 + C) % 0x100000000
d0 = (d0 + D) % 0x100000000
end
local digest = le32(a0) .. le32(b0) .. le32(c0) .. le32(d0)
return (digest:gsub(".", function(c)
return string.format("%02x", c:byte())
end))
end
if cloptions.S then
if cloptions.y then
w("Updating repositories...")
repodb={}
for _,v in ipairs(getRepos(sourcelist)) do
repodb[#repodb+1]=v
end
w("Writing new data...")
local str = ""
for _,v in ipairs(repodb) do
str=str..v.."\n"
end
fs.writeAllText("var/spm/db/repos.list", str)
end
if cloptions.i then
-- info remote
end
local needed = {}
local found = {}
for i,v in ipairs(args) do
local pkg, err = getPkg(v)
if not pkg then w(err) syscall.exit(1) end
needed[#needed+1] = pkg
found[v]=true
end
if cloptions.u then
for i,v in ipairs(table.keys(pkgdb)) do
local pkg, err = getPkg(v)
if pkg and not found[v] then
needed[#needed+1] = pkg
found[v]=true
end
end
end
local head = 0
while head<#needed do
head=head+1
for i,v in ipairs(needed[head].deps) do
if not found[v] and not pkgdb[v] then
local pkg, err = getPkg(v)
if pkg then
needed[#needed+1] = pkg
found[v]=true
end
end
end
end
if #needed==0 then
syscall.exit()
end
w("")
w("Packages("..tostring(#needed).."):")
printpkgs(needed)
w("")
while true do
local text = userinput(0, "Proceed with download [Y/n]", nil, 1)
if text=="" then
break
elseif text:lower()=="y" then
break
elseif text:lower()=="n" then
syscall.exit()
end
end
local files = {}
for _,v in ipairs(needed) do
if not pkgdb[v.id] or not pkgdb[v.id].hash==v.hash or cloptions.u then
w("Downloading "..v.id)
local resp = get(v.tar)
if not resp then
w("Package "..v.id.." failed GET")
syscall.exit()
end
if resp.code ~= 200 then
w("Package "..v.id.." returned code "..tostring(resp.code))
syscall.exit()
end
local ok, repotar = pcall(tar.unpack, resp.body)
if not ok or not repotar then
w("Package "..v.id.." failed tar parsing")
syscall.exit()
end
local f=ptar(repotar, v)
if not f or not f.data then syscall.exit() end
if not cloptions.s then
f.data=mini(f.data)
end
f.hash=v.hash
files[v.id]=f
else
w("Package "..v.id.." hash already installed")
end
end
w("Checking for conflicts...")
local seen={}
for i,v in pairs(files) do
for f,c in pairs(v.data) do
if seen[f] then
w(i.." and "..seen[f].." have conflicting file "..f)
v.data[f]=nil
else
seen[f]=i
end
end
end
seen=nil
w("Checking for modified files...")
local block={}
for i, v in pairs(files) do
if pkgdb[i] then
if not cloptions.overwrite then
for f,h in pairs(v.data) do
if pkgdb[i].files[f]~=md5(fs.readAllText(f)) then
w(f.." was modified on disk, specify --overwrite to overwrite")
block[f]=true
end
end
end
else
if not cloptions.overwrite then
for f,h in pairs(v.data) do
if fs.exists(f) then
w(f.." already exists, specify --overwrite to overwrite or remove the file")
syscall.exit()
end
end
end
end
end
w("Adding delete calls...")
local delete={}
for i,v in pairs(files) do
if pkgdb[i] then
for f,c in pairs(pkgdb[i].files) do
if not v.data[f] then
if c~=md5(fs.readAllText(f)) then
w(f.." was modified on disk, specify --overwrite to overwrite")
else
delete[f]=true
end
end
end
end
end
w("Calculating useage...")
local net=0
for i,v in pairs(files) do
for f,c in pairs(v.data) do
if not block[f] then
net=net+#c
end
end
for f,c in pairs(delete) do
net=net-#fs.readAllText(f)
end
end
w("")
w("Net Upgrade Size: "..tostring(net))
w("")
while true do
local text = userinput(0, "Proceed with Installation [Y/n]", nil, 1)
if text=="" then
break
elseif text:lower()=="y" then
break
elseif text:lower()=="n" then
syscall.exit()
end
end
w("Updatating db...")
for i,v in pairs(files) do
local content={}
content.files={}
for f,c in pairs(v.data) do
content.files[f]=md5(c)
end
content.hash=v.hash
fs.writeAllText("var/spm/db/installed/"..i, json.encode(content))
end
w("Writing changes...")
for i,v in pairs(files) do
w("Writing "..i)
for f,c in pairs(v.data) do
if not block[f] then
fs.writeAllText(f,c)
end
end
end
w("Install complete")
log.close()
elseif cloptions.R then
-- remove tree
elseif cloptions.Q then
if cloptions.l then
-- list installed files
elseif cloptions.o then
-- get package file arg[1] is from
elseif cloptions.e then
-- list installed manually
elseif cloptions.i then
-- list installed info
elseif cloptions.t then
-- list orphans
end
end
end
local ok,err = xpcall(spm, debug.traceback, ...)
if not ok then
print("Fatal error in spm:")
print(err)
end