473 lines
13 KiB
Lua
473 lines
13 KiB
Lua
local defaultDBPath = "/ag2/testdb.json"
|
|
|
|
local fs = require("filesystem")
|
|
-- local db = require("solvitdb")
|
|
local db = {}
|
|
local json = require("json")
|
|
function db.create(dbpath)
|
|
local handle = fs.open(dbpath,"w")
|
|
handle:write("{}")
|
|
handle:close()
|
|
end
|
|
function db.readJSON(dbpath)
|
|
local handle = fs.open(dbpath,"r")
|
|
local content = ""
|
|
while true do
|
|
local s = handle:read(math.huge or math.maxinteger)
|
|
if not s then break end
|
|
content=content..s
|
|
end
|
|
handle:close()
|
|
return content
|
|
end
|
|
function db.get(dbpath,pack)
|
|
local dbc = json.decode(db.readJSON(dbpath))
|
|
return dbc[pack]
|
|
end
|
|
function db.set(dbpath,pack,info)
|
|
local dbc = json.decode(db.readJSON(dbpath))
|
|
dbc[pack]=info
|
|
local handle = fs.open(dbpath,"w")
|
|
handle:write(json.encode(dbc))
|
|
handle:close()
|
|
end
|
|
function db.remove(dbpath,pack)
|
|
local dbc = json.decode(db.readJSON(dbpath))
|
|
dbc[pack]=nil
|
|
local handle = fs.open(dbpath,"w")
|
|
handle:write(json.encode(dbc))
|
|
handle:close()
|
|
end
|
|
function db.list(dbpath,pack)
|
|
local dbc = json.decode(db.readJSON(dbpath))
|
|
local keys = {}
|
|
for i,_ in pairs(dbc) do
|
|
table.insert(keys,i)
|
|
end
|
|
return ipairs(keys)
|
|
end
|
|
|
|
local avs = {}
|
|
function avs.splitSingular(s)
|
|
local result = {}
|
|
for str in string.gmatch(s, "([^.]+)") do
|
|
table.insert(result,tonumber(str) or -1)
|
|
end
|
|
return result
|
|
end
|
|
|
|
function avs.parse(pack)
|
|
if not string.find(pack,"=") then
|
|
return {pack}
|
|
end
|
|
local idx=pack:find("=")
|
|
local name=pack:sub(1,idx-1)
|
|
local verstr=pack:sub(idx+1)
|
|
if string.find(verstr,"-") then
|
|
idx=verstr:find("-")
|
|
verstr={verstr:sub(1,idx-1),verstr:sub(idx+1)}
|
|
else
|
|
verstr={verstr}
|
|
end
|
|
for i=1,#verstr do
|
|
verstr[i]=avs.splitSingular(verstr[i])
|
|
end
|
|
if #verstr>1 then
|
|
for i=1,3 do
|
|
verstr[1][i]=math.max(verstr[1][i],0)
|
|
end
|
|
end
|
|
return {name,verstr}
|
|
end
|
|
|
|
function avs.serializeSingle(ver)
|
|
local ver2 = table.copy(ver)
|
|
for i=1,3 do
|
|
if ver2[i]==-1 then
|
|
ver2[i]="*"
|
|
else
|
|
ver2[i]=tostring(ver2[i])
|
|
end
|
|
end
|
|
return ver2[1].."."..ver2[2].."."..ver2[3]
|
|
end
|
|
|
|
function avs.serializeVersion(ver)
|
|
local singles = {}
|
|
for i=1,#ver do
|
|
table.insert(singles,avs.serializeSingle(ver[i]))
|
|
end
|
|
if singles[1]==singles[2] then
|
|
singles={singles[1]}
|
|
end
|
|
local out=""
|
|
for i=1,#singles do
|
|
out=out..singles[i]
|
|
if i~=#singles then
|
|
out=out.."-"
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
|
|
function avs.serializePack(pack)
|
|
if #pack==1 then
|
|
return pack[1]
|
|
end
|
|
return pack[1].."="..avs.serializeVersion(pack[2])
|
|
end
|
|
|
|
function avs.singleGreater(ver1,ver2)
|
|
for i=1,3 do
|
|
if ver1[i]~=ver2[i] then
|
|
if ver1[i]==-1 or ver1[i]>ver2[i] then
|
|
return true
|
|
end
|
|
if ver2[i]==-1 or ver2[i]>ver1[i] then
|
|
return false
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function avs.singleLesser(ver1,ver2)
|
|
return avs.singleGreater(ver2,ver1)
|
|
end
|
|
|
|
function avs.singleMin(ver1,ver2)
|
|
return avs.singleLesser(ver1,ver2) and ver1 or ver2
|
|
end
|
|
|
|
function avs.singleMax(ver1,ver2)
|
|
return avs.singleGreater(ver1,ver2) and ver1 or ver2
|
|
end
|
|
|
|
function avs.compatibleRange(vers)
|
|
for i=1,#vers do
|
|
if type(vers[i])=="string" then vers[i]=avs.parse(vers[i])[2] end
|
|
if #vers[i]==1 then vers[i]={vers[i][1],vers[i][1]} end
|
|
end
|
|
local range = vers[1]
|
|
for i=2,#vers do
|
|
range[1]=avs.singleMax(range[1],vers[i][1])
|
|
range[2]=avs.singleMin(range[2],vers[i][2])
|
|
end
|
|
if avs.singleGreater(range[1],range[2]) then
|
|
return nil
|
|
end
|
|
return range
|
|
end
|
|
|
|
function avs.matching(pack1,pack2)
|
|
if pack1[1]~=pack2[1] then return false end
|
|
local ver1 = pack1[2] or {{-1,-1,-1}}
|
|
local ver2 = pack2[2] or {{-1,-1,-1}}
|
|
if #ver1==1 then ver1={ver1[1],ver1[1]} end
|
|
if #ver2==1 then ver2={ver2[1],ver2[1]} end
|
|
return avs.compatibleRange({ver1,ver2})~=nil
|
|
end
|
|
|
|
local function packageInArray(pack,arr)
|
|
for i=1,#arr do
|
|
if avs.matching(avs.parse(arr[i]),pack) then
|
|
return true,arr[i]
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function packageNameInArray(pack,arr)
|
|
for i=1,#arr do
|
|
if arr[i][1]==pack[1] then
|
|
return true,arr[i]
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function removeFromArray(el,arr)
|
|
for i=1,#arr do
|
|
if arr[i]==el then
|
|
table.remove(arr,i)
|
|
return i
|
|
end
|
|
end
|
|
end
|
|
|
|
local function startTransaction(dbpath)
|
|
dbpath = dbpath or defaultDBPath
|
|
if not fs.exists(dbpath) then
|
|
db.create(dbpath)
|
|
end
|
|
|
|
local installIncomplete = false
|
|
local removeIncomplete = false
|
|
|
|
local packInfo = {}
|
|
local ins = {}
|
|
local rem = {}
|
|
local transaction = {}
|
|
function transaction.install(name)
|
|
table.insert(ins,avs.parse(name))
|
|
installIncomplete = true
|
|
end
|
|
function transaction.remove(name)
|
|
table.insert(rem,avs.parse(name))
|
|
removeIncomplete = true
|
|
end
|
|
function transaction.autoRemove()
|
|
end
|
|
function transaction.update(name)
|
|
end
|
|
function transaction.updateAll(name)
|
|
end
|
|
function transaction.addInfo(name,info)
|
|
if not info.type then info.type="package" end
|
|
packInfo[name]=info
|
|
-- print(require("serialize").table(packInfo))
|
|
end
|
|
|
|
local function getPackInfo(pack)
|
|
return packInfo[avs.serializePack(pack)]
|
|
end
|
|
local function findReverseDependencies(pack)
|
|
local out={}
|
|
for i,info in pairs(packInfo) do
|
|
if info.dependencies and packageInArray(pack,info.dependencies) then
|
|
table.insert(out,avs.parse(i))
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
local function finalizeInstall(settings)
|
|
-- find missing package information
|
|
local missing = {}
|
|
for i=1,#ins do
|
|
if getPackInfo(ins[i])==nil then
|
|
table.insert(missing,avs.serializePack(ins[i]))
|
|
end
|
|
end
|
|
if #missing>0 then
|
|
return false,missing
|
|
end
|
|
-- find dependencies
|
|
installIncomplete=false
|
|
local i=1
|
|
while i<=#ins do
|
|
local deps = getPackInfo(ins[i]).dependencies
|
|
if deps and #deps>=1 then
|
|
for j=1,#deps do
|
|
local dep = avs.parse(deps[j])
|
|
local inArr,arrPack = packageNameInArray(dep,ins)
|
|
if inArr then
|
|
if not avs.matching(dep,arrPack) then
|
|
local msg = ""
|
|
if settings.resolveConflict then
|
|
msg="Cannot resolve conflict: "..msg
|
|
end
|
|
local rev = findReverseDependencies(arrPack)
|
|
if #rev==0 then
|
|
msg=msg.."Package "..avs.serializePack(ins[i]).." depends on "..avs.serializePack(dep)..", but one or more packages depend on "..avs.serializePack(arrPack)
|
|
return false, msg
|
|
elseif #rev==1 then
|
|
msg=msg.."Package "..avs.serializePack(ins[i]).." depends on "..avs.serializePack(dep)..", but package "..avs.serializePack(rev[1]).." depends on "..avs.serializePack(arrPack)
|
|
return false, msg
|
|
else
|
|
msg=msg.."Package "..avs.serializePack(ins[i]).." depends on "..avs.serializePack(dep)..", but packages "
|
|
for i=1,#rev do
|
|
if i>1 and i~=#rev then
|
|
msg=msg..", "
|
|
elseif i==#rev then
|
|
msg=msg.." and "
|
|
end
|
|
msg=msg..avs.serializePack(rev[i])
|
|
end
|
|
msg=msg.." depend on "..avs.serializePack(arrPack)
|
|
return false, msg
|
|
end
|
|
end
|
|
elseif type(db.get(dbpath,dep[1]))=="nil" then
|
|
installIncomplete=true
|
|
table.insert(ins,j,dep)
|
|
end
|
|
end
|
|
i=i+#deps
|
|
end
|
|
i=i+1
|
|
end
|
|
end
|
|
local function finalizeRemove(settings)
|
|
removeIncomplete=false
|
|
-- filter to only have packages in the database
|
|
local i=1
|
|
while i<=#rem do
|
|
local dat = db.get(dbpath,rem[i][1])
|
|
if not dat then
|
|
table.remove(rem,i)
|
|
else
|
|
packInfo[avs.serializePack(rem[i])]=dat
|
|
i=i+1
|
|
end
|
|
end
|
|
-- get package info from database
|
|
for i=1,#rem do
|
|
if not getPackInfo(rem[i]) then
|
|
packInfo[avs.serializePack(rem[i])]=db.get(rem[i][1])
|
|
end
|
|
end
|
|
-- find if the main package is reverse dependant to another
|
|
i=1
|
|
while i<=#rem do
|
|
local dat = getPackInfo(rem[i])
|
|
if dat.reverseDependencies and #dat.reverseDependencies>0 then
|
|
for _,dep in ipairs(dat.reverseDependencies) do
|
|
if not packageNameInArray({dep},rem) then
|
|
if settings.cascade then
|
|
table.insert(rem,1,{dep})
|
|
i=i+1
|
|
else
|
|
return false, "Package "..rem[i][1].." is a dependency of "..dep
|
|
end
|
|
end
|
|
end
|
|
end
|
|
i=i+1
|
|
end
|
|
-- look for dependencies if settings.autoremove is on
|
|
if settings.autoremove then
|
|
i=1
|
|
while i<=#rem do
|
|
local deps = getPackInfo(rem[i]).dependencies
|
|
if deps and #deps>=1 then
|
|
for j=1,#deps do
|
|
local dep = avs.parse(deps[j])
|
|
local depdat = packInfo[deps[j]]
|
|
if not depdat then
|
|
depdat = db.get(dbpath,dep[1])
|
|
packInfo[deps[j]]=depdat
|
|
end
|
|
if (not packageNameInArray(dep,rem)) and type(db.get(dbpath,dep[1]))~="nil" and (#depdat.reverseDependencies==1 and depdat.reverseDependencies[1]==rem[i][1]) then
|
|
removeIncomplete=true
|
|
table.insert(rem,j,dep)
|
|
end
|
|
end
|
|
i=i+#deps
|
|
end
|
|
i=i+1
|
|
end
|
|
end
|
|
end
|
|
function transaction.finalize(settings)
|
|
settings = settings or {}
|
|
|
|
while installIncomplete or removeIncomplete do
|
|
while installIncomplete do
|
|
local out = {finalizeInstall(settings)}
|
|
if out[1]==false then return table.unpack(out) end
|
|
end
|
|
while removeIncomplete do
|
|
local out = {finalizeRemove(settings)}
|
|
if out[1]==false then return table.unpack(out) end
|
|
end
|
|
end
|
|
|
|
local install = {}
|
|
local remove = {}
|
|
for i=1,#ins do
|
|
table.insert(install,avs.serializePack(ins[i]))
|
|
end
|
|
for i=1,#rem do
|
|
table.insert(remove,avs.serializePack(rem[i]))
|
|
end
|
|
return true, {install=install,remove=remove}
|
|
|
|
-- return "true, {["install"] = {"dep1", "package1", "package2"}, ["remove"] = {"package3"}}" on success
|
|
-- return "false, {"dep1"}" when not enough data
|
|
-- return "false, "[verbose string]"" when conflict found
|
|
-- TODO: handle resolving different constant AVS conflict (package1->dep1=1.0.0 + package2->dep1=1.2.3 where both are uninstalled)
|
|
-- TODO: handle different constant AVS conflict (package1->dep1=1.0.0 + package2->dep1=1.2.3 where package2 and dep1 is on db)
|
|
-- TODO: handle same range AVS
|
|
-- TODO: handle different intercompatible 1.*.* range AVS
|
|
-- TODO: handle different incompatible 1.*.* range AVS
|
|
-- TODO: handle different intercompatible 1.*.*-2.*.* range AVS
|
|
-- TODO: handle different incompatible 1.*.*-2.*.* range AVS
|
|
-- TODO: handle conflicts from package info
|
|
-- TODO: handle reverse conflicts from another package's info
|
|
-- TODO: handle automatic conflict resolving
|
|
-- TODO: handle update of a single package with no dependencies
|
|
-- TODO: handle update of a single package with dependencies that don't need updating
|
|
-- TODO: handle update of a single package with dependencies that need updating
|
|
-- TODO: handle update of a single package that has a set dependency version changed
|
|
-- TODO: handle updating all packages in the database
|
|
-- TODO: handle installing optional packages
|
|
-- TODO: handle installing virtual packages and store this vpack info to database
|
|
-- TODO: handle removing virtual packages from database info
|
|
-- TODO: handle installing groups and store this group info to database
|
|
-- TODO: handle removing groups and store this group info to database
|
|
end
|
|
local function storeInstall()
|
|
-- directly set
|
|
for _,pack in ipairs(ins) do
|
|
if getPackInfo(pack) then
|
|
local info = table.copy(getPackInfo(pack))
|
|
if pack[2] then
|
|
info.version=avs.serializeVersion(pack[2])
|
|
else
|
|
info.version=info.latestVersion or info.version
|
|
end
|
|
db.set(dbpath,pack[1],info)
|
|
end
|
|
end
|
|
-- set reverse dependencies
|
|
for _,pack in pairs(ins) do
|
|
local i = avs.serializePack(pack)
|
|
local v = packInfo[i]
|
|
if v and v.dependencies then
|
|
for _,dep in ipairs(v.dependencies) do
|
|
local depname = avs.parse(dep)[1]
|
|
local dat = db.get(dbpath,depname)
|
|
if not dat then goto continue end
|
|
if type(dat.reverseDependencies)~="table" then
|
|
dat.reverseDependencies={}
|
|
end
|
|
table.insert(dat.reverseDependencies,pack[1])
|
|
db.set(dbpath,depname,dat)
|
|
::continue::
|
|
end
|
|
end
|
|
end
|
|
end
|
|
local function storeRemove()
|
|
-- directly remove
|
|
for _,pack in ipairs(rem) do
|
|
db.remove(dbpath,pack[1])
|
|
end
|
|
-- remove reverse dependencies
|
|
for _,pack in ipairs(rem) do
|
|
local pdat = getPackInfo(pack)
|
|
if not pdat.dependencies then goto continue end
|
|
for _,dep in ipairs(pdat.dependencies) do
|
|
local depname = avs.parse(dep)[1]
|
|
local dat = db.get(dbpath,depname)
|
|
if dat.reverseDependencies then
|
|
removeFromArray(pack[1],dat.reverseDependencies)
|
|
end
|
|
db.set(dbpath,depname,dat)
|
|
end
|
|
::continue::
|
|
end
|
|
end
|
|
function transaction.store()
|
|
if #ins>0 then
|
|
storeInstall()
|
|
end
|
|
if #rem>0 then
|
|
storeRemove()
|
|
end
|
|
end
|
|
return transaction
|
|
end
|
|
|
|
return { avs=avs,startTransaction=startTransaction }
|