chore: Merge vfox.rs into jdx/mise monorepo (#5590)
Integrate vfox.rs into the mise monorepo by moving its source into a new `crates/vfox` directory. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
53 files changed
+2947
-5
No files matched your search
@@ -1,4 +1,3 @@
|
||||
#paths = ["../vfox.rs"]
|
||||
#paths = ["../usage/lib"]
|
||||
|
||||
[env]
|
||||
|
||||
Generated
+5
-2
@@ -1295,6 +1295,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1312,6 +1313,7 @@ dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"env_filter",
|
||||
"jiff",
|
||||
"log",
|
||||
]
|
||||
|
||||
@@ -6498,11 +6500,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "vfox"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed8b4515b35f86ca1d3ea7c936e704ade91ec9f609000f3de82febdea27406da"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"env_logger",
|
||||
"homedir",
|
||||
"indexmap 2.10.0",
|
||||
"insta",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
"mlua",
|
||||
|
||||
+4
-2
@@ -1,3 +1,6 @@
|
||||
[workspace]
|
||||
members = ["crates/vfox"]
|
||||
|
||||
[package]
|
||||
name = "mise"
|
||||
version = "2025.7.4"
|
||||
@@ -149,8 +152,7 @@ url = "2"
|
||||
urlencoding = "2.1.3"
|
||||
usage-lib = { version = "2", features = ["clap", "docs"] }
|
||||
versions = { version = "6", features = ["serde"] }
|
||||
# vfox = { path = "../vfox.rs" }
|
||||
vfox = { version = "1", default-features = false }
|
||||
vfox = { path = "crates/vfox", default-features = false }
|
||||
walkdir = "2"
|
||||
which = "7"
|
||||
xx = { version = "2", features = ["glob"] }
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
[package]
|
||||
name = "vfox"
|
||||
version = "1.0.5"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
description = "Interface to vfox plugins"
|
||||
documentation = "https://docs.rs/vfox"
|
||||
homepage = "https://github.com/jdx/vfox.rs"
|
||||
repository = "https://github.com/jdx/vfox.rs"
|
||||
include = ["src", "lua", "Cargo.toml", "Cargo.lock", "README.md", "LICENSE"]
|
||||
|
||||
[lib]
|
||||
name = "vfox"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "vfox-cli"
|
||||
path = "src/bin.rs"
|
||||
|
||||
[dependencies]
|
||||
homedir = "0.3"
|
||||
indexmap = "2"
|
||||
itertools = "0.14"
|
||||
log = "0.4"
|
||||
mlua = { version = "0.10", features = [
|
||||
"async",
|
||||
"lua51",
|
||||
"macros",
|
||||
"serialize",
|
||||
"send",
|
||||
] }
|
||||
once_cell = "1"
|
||||
reqwest = { version = "0.12", features = [] } # TODO: replace with xx
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
thiserror = "2"
|
||||
tokio = { version = "1", features = ["macros", "fs"] }
|
||||
url = "2"
|
||||
xx = { version = "2", features = ["archive", "hash", "http"] }
|
||||
|
||||
# cli dependencies
|
||||
env_logger = { version = "0.11", optional = true }
|
||||
clap = { version = "4", features = ["derive"], optional = true }
|
||||
tempfile = "3"
|
||||
|
||||
[dev-dependencies]
|
||||
insta = "1"
|
||||
#pretty_assertions = "1.4.0"
|
||||
|
||||
[features]
|
||||
default = ["vendored-lua"]
|
||||
cli = ["clap", "tokio/full", "env_logger"]
|
||||
native-tls = ["xx/native-tls"]
|
||||
rustls = ["xx/rustls"]
|
||||
rustls-native-roots = ["xx/rustls-native-roots"]
|
||||
vendored-lua = ["mlua/vendored"]
|
||||
|
||||
# [workspace.metadata.release] - removed since this is now part of mise workspace
|
||||
# pre-release-hook = ["git", "cliff", "-o", "CHANGELOG.md", "--tag", "{{version}}"]
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Jeff Dickey
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1 @@
|
||||
A rust interface to [vfox](https://github.com/version-fox/vfox).
|
||||
@@ -0,0 +1,256 @@
|
||||
-- vim: ft=lua ts=2 sw=2
|
||||
|
||||
-- Syntactic Sugar {{{
|
||||
local function rine(val) -- Return (val) If it's Not Empty (non-zero-length)
|
||||
return (val and #val>0) and val
|
||||
end
|
||||
local function rit(a) -- Return (a) If it's Table
|
||||
return (type(a) == "table") and a
|
||||
end
|
||||
local noop = function() end
|
||||
local esc = function(s) return string.gsub(s, "([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%" .. "%1") end
|
||||
local str = tostring
|
||||
local char = string.char
|
||||
local opts = rit(htmlparser_opts) or {} -- needed for silent/noerr/noout/nonl directives, also needed to be defined before `require` in such case
|
||||
local prn = opts.silent and noop or function(l,f,...)
|
||||
local fd = (l=="i") and "stdout" or "stderr"
|
||||
local t = (" [%s] "):format(l:upper())
|
||||
io[fd]
|
||||
:write('[HTMLParser]'..t..f:format(...)
|
||||
..(opts.nonl or "\n")
|
||||
)
|
||||
end
|
||||
local err = opts.noerr and noop or function(f,...) prn("e",f,...) end
|
||||
local out = opts.noout and noop or function(f,...) prn("i",f,...) end
|
||||
local line = debug and function(lvl) return debug.getinfo(lvl or 2).currentline end or noop
|
||||
local dbg = opts.debug and function(f,...) prn("d",f:gsub("#LINE#",str(line(3))),...) end or noop
|
||||
-- }}}
|
||||
-- Requires {{{
|
||||
local ElementNode = require"htmlparser.ElementNode"
|
||||
local voidelements = require"htmlparser.voidelements"
|
||||
--}}}
|
||||
local HtmlParser = {}
|
||||
local function parse(text,limit) -- {{{
|
||||
local opts = rine(opts) -- use top-level opts-table (the one, defined before requiring the module), if exists
|
||||
or rit(htmlparser_opts) -- or defined after requiring (but before calling `parse`)
|
||||
or {} -- fallback otherwise
|
||||
opts.looplimit = opts.looplimit or htmlparser_looplimit
|
||||
|
||||
local text = str(text)
|
||||
local limit = limit or opts.looplimit or 1000
|
||||
local tpl = false
|
||||
|
||||
if not opts.keep_comments then -- Strip (or not) comments {{{
|
||||
text = text:gsub("<!%-%-.-%-%->","") -- Many chances commented code will have syntax errors, that'll lead to parser failures
|
||||
end -- }}}
|
||||
|
||||
local tpr={}
|
||||
|
||||
if not opts.keep_danger_placeholders then -- {{{ little speedup by cost of potential parsing breakages
|
||||
-- search unused "invalid" bytes {{{
|
||||
local busy,i={},0;
|
||||
repeat -- {{{
|
||||
local cc = char(i)
|
||||
if not(text:match(cc)) then -- {{{
|
||||
if not(tpr["<"]) or not(tpr[">"]) then -- {{{
|
||||
if not(busy[i]) then -- {{{
|
||||
if not(tpr["<"]) then -- {{{
|
||||
tpr["<"] = cc;
|
||||
elseif not(tpr[">"]) then
|
||||
tpr[">"] = cc;
|
||||
end -- }}}
|
||||
busy[i] = true
|
||||
dbg("c:{%s}||cc:{%d}||tpr[c]:{%s}",str(c),cc:byte(),str(tpr[c]))
|
||||
dbg("busy[i]:{%s},i:{%d}",str(busy[i]),i)
|
||||
dbg("[FindPH]:#LINE# Success! || i=%d",i)
|
||||
else -- if !busy
|
||||
dbg("[FindPH]:#LINE# Busy! || i=%d",i)
|
||||
end -- if !busy -- }}}
|
||||
dbg("c:{%s}||cc:{%d}||tpr[c]:{%s}",c,cc:byte(),str(tpr[c]))
|
||||
dbg("%s",str(busy[i]))
|
||||
else -- if < or >
|
||||
dbg("[FindPH]:#LINE# Done!",i)
|
||||
break
|
||||
end -- if < or > -- }}}
|
||||
else -- text!match(cc)
|
||||
dbg("[FindPH]:#LINE# Text contains this byte! || i=%d",i)
|
||||
end -- text!match(cc) -- }}}
|
||||
local skip=1
|
||||
if i==31 then
|
||||
skip=96 -- ASCII
|
||||
end
|
||||
i=i+skip
|
||||
until (i==255) -- }}}
|
||||
i=nil
|
||||
--- }}}
|
||||
|
||||
if not(tpr["<"]) or not(tpr[">"]) then
|
||||
err("Impossible to find at least two unused byte codes in this HTML-code. We need it to escape bracket-contained placeholders inside tags.")
|
||||
err("Consider enabling 'keep_danger_placeholders' option (to silence this error, if parser wasn't failed with current HTML-code) or manually replace few random bytes, to free up the codes.")
|
||||
else
|
||||
dbg("[FindPH]:#LINE# Found! || '<'=%d, '>'=%d",tpr["<"]:byte(),tpr[">"]:byte())
|
||||
end
|
||||
|
||||
-- dbg("tpr[>] || tpr[] || #busy%d")
|
||||
|
||||
-- g {{{
|
||||
local function g(id,...)
|
||||
local arg={...}
|
||||
local orig=arg[id]
|
||||
arg[id]=arg[id]:gsub("(.)",tpr)
|
||||
if arg[id] ~= orig then
|
||||
tpl=true
|
||||
dbg("[g]:#LINE# orig: %s", str(orig))
|
||||
dbg("[g]:#LINE# replaced: %s",str(arg[id]))
|
||||
end
|
||||
dbg("[g]:#LINE# called, id: %s, arg[id]: %s, args { "..(("{%s}, "):rep(#arg):gsub(", $","")).." }",id,arg[id],...)
|
||||
dbg("[g]:#LINE# concat(arg): %s",table.concat(arg))
|
||||
return table.concat(arg)
|
||||
end
|
||||
-- g }}}
|
||||
|
||||
-- tpl-placeholders and attributes {{{
|
||||
text=text
|
||||
:gsub(
|
||||
"(=[%s]-)".. -- only match attr.values, and not random strings between two random apostrophs
|
||||
"(%b'')",
|
||||
function(...)return g(2,...)end
|
||||
)
|
||||
:gsub(
|
||||
"(=[%s]-)".. -- same for "
|
||||
'(%b"")',
|
||||
function(...)return g(2,...)end
|
||||
) -- Escape "<"/">" inside attr.values (see issue #50)
|
||||
:gsub(
|
||||
"(<".. -- Match "<",
|
||||
(opts.tpl_skip_pattern or "[^!]").. -- with exclusion pattern (for example, to ignore comments, which aren't template placeholders, but can legally contain "<"/">" inside.
|
||||
")([^>]+)".. -- If matched, we want to escape '<'s if we meet them inside tag
|
||||
"(>)",
|
||||
function(...)return g(2,...)end
|
||||
)
|
||||
:gsub(
|
||||
"("..
|
||||
(tpr["<"] or "__FAILED__").. -- Here we search for "<", we escaped in previous gsub (and don't break things if we have no escaping replacement)
|
||||
")("..
|
||||
(opts.tpl_marker_pattern or "[^%w%s]").. -- Capture templating symbol
|
||||
")([%g%s]-)".. -- match placeholder's content
|
||||
"(%2)(>)".. -- placeholder's tail
|
||||
"([^>]*>)", -- remainings
|
||||
function(...)return g(5,...)end
|
||||
)
|
||||
-- }}}
|
||||
end -- }}}
|
||||
|
||||
local index = 0
|
||||
local root = ElementNode:new(index, str(text))
|
||||
local node, descend, tpos, opentags = root, true, 1, {}
|
||||
|
||||
while true do -- MainLoop {{{
|
||||
if index == limit then -- {{{
|
||||
err("Main loop reached loop limit (%d). Consider either increasing it or checking HTML-code for syntax errors", limit)
|
||||
break
|
||||
end -- }}}
|
||||
-- openstart/tpos Definitions {{{
|
||||
local openstart, name
|
||||
openstart, tpos, name = root._text:find(
|
||||
"<" .. -- an uncaptured starting "<"
|
||||
"([%w-]+)" .. -- name = the first word, directly following the "<"
|
||||
"[^>]*>", -- include, but not capture everything up to the next ">"
|
||||
tpos)
|
||||
dbg("[MainLoop]:#LINE# openstart=%s || tpos=%s || name=%s",str(openstart),str(tpos),str(name))
|
||||
-- }}}
|
||||
if not name then break end
|
||||
-- Some more vars {{{
|
||||
index = index + 1
|
||||
local tag = ElementNode:new(index, str(name), (node or {}), descend, openstart, tpos)
|
||||
node = tag
|
||||
local tagloop
|
||||
local tagst, apos = tag:gettext(), 1
|
||||
-- }}}
|
||||
while true do -- TagLoop {{{
|
||||
dbg("[TagLoop]:#LINE# tag.name=%s, tagloop=%s",str(tag.name),str(tagloop))
|
||||
if tagloop == limit then -- {{{
|
||||
err("Tag parsing loop reached loop limit (%d). Consider either increasing it or checking HTML-code for syntax errors", limit)
|
||||
break
|
||||
end -- }}}
|
||||
-- Attrs {{{
|
||||
local start, k, eq, quote, v, zsp
|
||||
start, apos, k, zsp, eq, zsp, quote = tagst:find(
|
||||
"%s+" .. -- some uncaptured space
|
||||
"([^%s=/>]+)" .. -- k = an unspaced string up to an optional "=" or the "/" or ">"
|
||||
"([%s]-)".. -- zero or more spaces
|
||||
"(=?)" .. -- eq = the optional; "=", else ""
|
||||
"([%s]-)".. -- zero or more spaces
|
||||
[=[(['"]?)]=], -- quote = an optional "'" or '"' following the "=", or ""
|
||||
apos)
|
||||
dbg("[TagLoop]:#LINE# start=%s || apos=%s || k=%s || zsp='%s' || eq='%s', quote=[%s]",str(start),str(apos),str(k),str(zsp),str(eq),str(quote))
|
||||
-- }}}
|
||||
if not k or k == "/>" or k == ">" then break end
|
||||
-- Pattern {{{
|
||||
if eq == "=" then
|
||||
local pattern = "=([^%s>]*)"
|
||||
if quote ~= "" then
|
||||
pattern = quote .. "([^" .. quote .. "]*)" .. quote
|
||||
end
|
||||
start, apos, v = tagst:find(pattern, apos)
|
||||
dbg("[TagLoop]:#LINE# start=%s || apos=%s || v=%s || pattern=%s",str(start),str(apos),str(v),str(pattern))
|
||||
end
|
||||
-- }}}
|
||||
v=v or ""
|
||||
if tpl then -- {{{
|
||||
for rk,rv in pairs(tpr) do
|
||||
v = v:gsub(rv,rk)
|
||||
dbg("[TagLoop]:#LINE# rv=%s || rk=%s",str(rv),str(rk))
|
||||
end
|
||||
end -- }}}
|
||||
|
||||
dbg("[TagLoop]:#LINE# k=%s || v=%s",str(k),str(v))
|
||||
tag:addattribute(k, v)
|
||||
tagloop = (tagloop or 0) + 1
|
||||
end
|
||||
-- }}}
|
||||
if voidelements[tag.name:lower()] then -- {{{
|
||||
descend = false
|
||||
tag:close()
|
||||
else
|
||||
descend = true
|
||||
opentags[tag.name] = opentags[tag.name] or {}
|
||||
table.insert(opentags[tag.name], tag)
|
||||
end
|
||||
-- }}}
|
||||
local closeend = tpos
|
||||
local closingloop
|
||||
while true do -- TagCloseLoop {{{
|
||||
-- Can't remember why did I add that, so comment it for now (and not remove), in case it will be needed again
|
||||
-- (although, it causes #59 and #60, so it will anyway be needed to rework)
|
||||
-- if voidelements[tag.name:lower()] then break end -- already closed
|
||||
if closingloop == limit then
|
||||
err("Tag closing loop reached loop limit (%d). Consider either increasing it or checking HTML-code for syntax errors", limit)
|
||||
break
|
||||
end
|
||||
|
||||
local closestart, closing, closename
|
||||
closestart, closeend, closing, closename = root._text:find("[^<]*<(/?)([%w-]+)", closeend)
|
||||
dbg("[TagCloseLoop]:#LINE# closestart=%s || closeend=%s || closing=%s || closename=%s",str(closestart),str(closeend),str(closing),str(closename))
|
||||
|
||||
if not closing or closing == "" then break end
|
||||
|
||||
tag = table.remove(opentags[closename] or {}) or tag -- kludges for the cases of closing void or non-opened tags
|
||||
closestart = root._text:find("<", closestart)
|
||||
dbg("[TagCloseLoop]:#LINE# closestart=%s",str(closestart))
|
||||
tag:close(closestart, closeend + 1)
|
||||
node = tag.parent
|
||||
descend = true
|
||||
closingloop = (closingloop or 0) + 1
|
||||
end -- }}}
|
||||
end -- }}}
|
||||
if tpl then -- {{{
|
||||
dbg("tpl")
|
||||
for k,v in pairs(tpr) do
|
||||
root._text = root._text:gsub(v,k)
|
||||
end
|
||||
end -- }}}
|
||||
return root
|
||||
end -- }}}
|
||||
HtmlParser.parse = parse
|
||||
return HtmlParser
|
||||
@@ -0,0 +1,283 @@
|
||||
-- vim: ft=lua ts=2
|
||||
local Set = {}
|
||||
Set.mt = {__index = Set}
|
||||
function Set:new(values)
|
||||
local instance = {}
|
||||
local isSet if getmetatable(values) == Set.mt then isSet = true end
|
||||
if type(values) == "table" then
|
||||
if not isSet and #values > 0 then
|
||||
for _,v in ipairs(values) do
|
||||
instance[v] = true
|
||||
end
|
||||
else
|
||||
for k in pairs(values) do
|
||||
instance[k] = true
|
||||
end
|
||||
end
|
||||
elseif values ~= nil then
|
||||
instance = {[values] = true}
|
||||
end
|
||||
return setmetatable(instance, Set.mt)
|
||||
end
|
||||
|
||||
function Set:add(e)
|
||||
if e ~= nil then self[e] = true end
|
||||
return self
|
||||
end
|
||||
|
||||
function Set:remove(e)
|
||||
if e ~= nil then self[e] = nil end
|
||||
return self
|
||||
end
|
||||
|
||||
function Set:tolist()
|
||||
local res = {}
|
||||
for k in pairs(self) do
|
||||
table.insert(res, k)
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
Set.mt.__add = function (a, b)
|
||||
local res, a, b = Set:new(), Set:new(a), Set:new(b)
|
||||
for k in pairs(a) do res[k] = true end
|
||||
for k in pairs(b) do res[k] = true end
|
||||
return res
|
||||
end
|
||||
|
||||
-- Subtraction
|
||||
Set.mt.__sub = function (a, b)
|
||||
local res, a, b = Set:new(), Set:new(a), Set:new(b)
|
||||
for k in pairs(a) do res[k] = true end
|
||||
for k in pairs(b) do res[k] = nil end
|
||||
return res
|
||||
end
|
||||
|
||||
-- Intersection
|
||||
Set.mt.__mul = function (a, b)
|
||||
local res, a, b = Set:new(), Set:new(a), Set:new(b)
|
||||
for k in pairs(a) do
|
||||
res[k] = b[k]
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
-- String representation
|
||||
Set.mt.__tostring = function (set)
|
||||
local s = "{"
|
||||
local sep = ""
|
||||
for k in pairs(set) do
|
||||
s = s .. sep .. tostring(k)
|
||||
sep = ", "
|
||||
end
|
||||
return s .. "}"
|
||||
end
|
||||
|
||||
|
||||
local ElementNode = {}
|
||||
ElementNode.mt = {__index = ElementNode}
|
||||
function ElementNode:new(index, nameortext, node, descend, openstart, openend)
|
||||
local instance = {
|
||||
index = index,
|
||||
name = nameortext,
|
||||
level = 0,
|
||||
parent = nil,
|
||||
root = nil,
|
||||
nodes = {},
|
||||
_openstart = openstart, _openend = openend,
|
||||
_closestart = openstart, _closeend = openend,
|
||||
attributes = {},
|
||||
id = nil,
|
||||
classes = {},
|
||||
deepernodes = Set:new(),
|
||||
deeperelements = {}, deeperattributes = {}, deeperids = {}, deeperclasses = {}
|
||||
}
|
||||
if not node then
|
||||
instance.name = "root"
|
||||
instance.root = instance
|
||||
instance._text = nameortext
|
||||
local length = string.len(nameortext)
|
||||
instance._openstart, instance._openend = 1, length
|
||||
instance._closestart, instance._closeend = 1, length
|
||||
elseif descend then
|
||||
instance.root = node.root
|
||||
instance.parent = node
|
||||
instance.level = node.level + 1
|
||||
table.insert(node.nodes, instance)
|
||||
else
|
||||
instance.root = node.root
|
||||
instance.parent = node.parent or node --XXX: adds some safety but needs more testing for heisenbugs in corner cases
|
||||
instance.level = node.level
|
||||
table.insert((node.parent and node.parent.nodes or node.nodes), instance) --XXX: see above about heisenbugs
|
||||
end
|
||||
return setmetatable(instance, ElementNode.mt)
|
||||
end
|
||||
|
||||
function ElementNode:gettext()
|
||||
return string.sub(self.root._text, self._openstart, self._closeend)
|
||||
end
|
||||
|
||||
function ElementNode:settext(c)
|
||||
self.root._text=c
|
||||
end
|
||||
|
||||
function ElementNode:textonly()
|
||||
return (self:gettext():gsub("<[^>]*>",""))
|
||||
end
|
||||
|
||||
function ElementNode:getcontent()
|
||||
return string.sub(self.root._text, self._openend + 1, self._closestart - 1)
|
||||
end
|
||||
|
||||
function ElementNode:addattribute(k, v)
|
||||
self.attributes[k] = v
|
||||
if string.lower(k) == "id" then
|
||||
self.id = v
|
||||
-- class attribute contains "space-separated tokens", each of which we'd like quick access to
|
||||
elseif string.lower(k) == "class" then
|
||||
for class in string.gmatch(v, "%S+") do
|
||||
table.insert(self.classes, class)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function insert(table, name, node)
|
||||
table[name] = table[name] or Set:new()
|
||||
table[name]:add(node)
|
||||
end
|
||||
|
||||
function ElementNode:close(closestart, closeend)
|
||||
if closestart and closeend then
|
||||
self._closestart, self._closeend = closestart, closeend
|
||||
end
|
||||
-- inform hihger level nodes about this element's existence in their branches
|
||||
local node = self
|
||||
while true do
|
||||
node = node.parent
|
||||
if not node then break end
|
||||
node.deepernodes:add(self)
|
||||
insert(node.deeperelements, string.lower(self.name), self)
|
||||
for k in pairs(self.attributes) do
|
||||
insert(node.deeperattributes, k, self)
|
||||
end
|
||||
if self.id then
|
||||
insert(node.deeperids, self.id, self)
|
||||
end
|
||||
for _,v in ipairs(self.classes) do
|
||||
insert(node.deeperclasses, v, self)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function escape(s)
|
||||
-- escape all ^, $, (, ), %, ., [, ], *, +, - , and ? with a % prefix
|
||||
return string.gsub(s, "([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%" .. "%1")
|
||||
end
|
||||
|
||||
local function select(self, s)
|
||||
if not s or type(s) ~= "string" or s == "" then return Set:new() end
|
||||
local sets = {[""] = self.deeperelements, ["["] = self.deeperattributes,
|
||||
["#"] = self.deeperids, ["."] = self.deeperclasses}
|
||||
local function match(t, w)
|
||||
local m, e, v
|
||||
if t == "[" then w, m, e, v = string.match(w,
|
||||
"([^=|%*~%$!%^]+)" .. -- w = 1 or more characters up to a possible "=", "|", "*", "~", "$", "!", or "^"
|
||||
"([|%*~%$!%^]?)" .. -- m = an optional "|", "*", "~", "$", "!", or "^", preceding the optional "="
|
||||
"(=?)" .. -- e = the optional "="
|
||||
"(.*)" -- v = anything following the "=", or else ""
|
||||
)
|
||||
end
|
||||
local matched = Set:new(sets[t][w])
|
||||
-- attribute value selectors
|
||||
if e == "=" then
|
||||
if #v < 2 then v = "'" .. v .. "'" end -- values should be quoted
|
||||
v = string.sub(v, 2, #v - 1) -- strip quotes
|
||||
if m == "!" then matched = Set:new(self.deepernodes) end -- include those without that attribute
|
||||
for node in pairs(matched) do
|
||||
local a = node.attributes[w]
|
||||
-- equals
|
||||
if m == "" and a ~= v then matched:remove(node)
|
||||
-- not equals
|
||||
elseif m == "!" and a == v then matched:remove(node)
|
||||
-- prefix
|
||||
elseif m =="|" and string.match(a, "^[^-]*") ~= v then matched:remove(node)
|
||||
-- contains
|
||||
elseif m =="*" and string.match(a, escape(v)) ~= v then matched:remove(node)
|
||||
-- word
|
||||
elseif m =="~" then matched:remove(node)
|
||||
for word in string.gmatch(a, "%S+") do
|
||||
if word == v then matched:add(node) break end
|
||||
end
|
||||
-- starts with
|
||||
elseif m =="^" and string.match(a, "^" .. escape(v)) ~= v then matched:remove(node)
|
||||
-- ends with
|
||||
elseif m =="$" and string.match(a, escape(v) .. "$") ~= v then matched:remove(node)
|
||||
end
|
||||
end -- for node
|
||||
end -- if v
|
||||
return matched
|
||||
end
|
||||
|
||||
local subjects, resultset, childrenonly = Set:new({self})
|
||||
for part in string.gmatch(s, "%S+") do
|
||||
repeat
|
||||
if part == ">" then childrenonly = true --[[goto nextpart]] break end
|
||||
resultset = Set:new()
|
||||
for subject in pairs(subjects) do
|
||||
local star = subject.deepernodes
|
||||
if childrenonly then star = Set:new(subject.nodes) end
|
||||
resultset = resultset + star
|
||||
end
|
||||
childrenonly = false
|
||||
if part == "*" then --[[goto nextpart]] break end
|
||||
local excludes, filter = Set:new()
|
||||
local start, pos = 0, 0
|
||||
while true do
|
||||
local switch, stype, name, eq, quote
|
||||
start, pos, switch, stype, name, eq, quote = string.find(part,
|
||||
"(%(?%)?)" .. -- switch = a possible ( or ) switching the filter on or off
|
||||
"([:%[#.]?)" .. -- stype = a possible :, [, #, or .
|
||||
"([%w-_\\]+)" .. -- name = 1 or more alfanumeric chars (+ hyphen, reverse slash and uderscore)
|
||||
"([|%*~%$!%^]?=?)" .. -- eq = a possible |=, *=, ~=, $=, !=, ^=, or =
|
||||
"(['\"]?)", -- quote = a ' or " delimiting a possible attribute value
|
||||
pos + 1
|
||||
)
|
||||
if not name then break end
|
||||
repeat
|
||||
if ":" == stype then
|
||||
filter = name
|
||||
--[[goto nextname]] break
|
||||
end
|
||||
if ")" == switch then
|
||||
filter = nil
|
||||
end
|
||||
if "[" == stype and "" ~= quote then
|
||||
local value
|
||||
start, pos, value = string.find(part, "(%b" .. quote .. quote .. ")]", pos)
|
||||
name = name .. eq .. value
|
||||
end
|
||||
local matched = match(stype, name)
|
||||
if filter == "not" then
|
||||
excludes = excludes + matched
|
||||
else
|
||||
resultset = resultset * matched
|
||||
end
|
||||
--::nextname::
|
||||
break
|
||||
until true
|
||||
end
|
||||
resultset = resultset - excludes
|
||||
subjects = Set:new(resultset)
|
||||
--::nextpart::
|
||||
break
|
||||
until true
|
||||
end
|
||||
resultset = resultset:tolist()
|
||||
table.sort(resultset, function (a, b) return a.index < b.index end)
|
||||
return resultset
|
||||
end
|
||||
|
||||
function ElementNode:select(s) return select(self, s) end
|
||||
ElementNode.mt.__call = select
|
||||
|
||||
return ElementNode
|
||||
@@ -0,0 +1,19 @@
|
||||
-- vim: ft=lua ts=2
|
||||
return {
|
||||
area = true,
|
||||
base = true,
|
||||
br = true,
|
||||
col = true,
|
||||
command = true,
|
||||
embed = true,
|
||||
hr = true,
|
||||
img = true,
|
||||
input = true,
|
||||
keygen = true,
|
||||
link = true,
|
||||
meta = true,
|
||||
param = true,
|
||||
source = true,
|
||||
track = true,
|
||||
wbr = true
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#[cfg(feature = "cli")]
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
mod cli;
|
||||
|
||||
#[allow(clippy::needless_return)]
|
||||
#[cfg(feature = "cli")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init_from_env(env_logger::Env::default().filter_or("VFOX_LOG", "info"));
|
||||
if let Err(err) = cli::run().await {
|
||||
error!("{err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "cli"))]
|
||||
fn main() {
|
||||
panic!("cli feature is not enabled");
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
use vfox::{Vfox, VfoxResult};
|
||||
|
||||
#[derive(clap::Args)]
|
||||
pub struct Available {}
|
||||
|
||||
impl Available {
|
||||
pub async fn run(&self) -> VfoxResult<()> {
|
||||
for (name, url) in Vfox::list_available_sdks() {
|
||||
println!("{name} {url}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
use vfox::{Vfox, VfoxResult};
|
||||
|
||||
#[derive(clap::Args)]
|
||||
pub struct EnvKeys {
|
||||
pub sdk: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
impl EnvKeys {
|
||||
pub async fn run(&self) -> VfoxResult<()> {
|
||||
let vfox = Vfox::new();
|
||||
let env_keys = vfox.env_keys(&self.sdk, &self.version).await?;
|
||||
for env_key in env_keys {
|
||||
println!("{}={}", env_key.key, env_key.value);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use std::path::PathBuf;
|
||||
use vfox::{Vfox, VfoxResult};
|
||||
|
||||
#[derive(clap::Args)]
|
||||
pub struct Install {
|
||||
pub sdk: String,
|
||||
pub version: String,
|
||||
#[clap(short, long)]
|
||||
pub output_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Install {
|
||||
pub async fn run(&self) -> VfoxResult<()> {
|
||||
let vfox = Vfox::new();
|
||||
let out = self
|
||||
.output_dir
|
||||
.clone()
|
||||
.unwrap_or_else(|| vfox.install_dir.join(&self.sdk).join(&self.version));
|
||||
info!(
|
||||
"Installing {} version {} to {out:?}",
|
||||
self.sdk, self.version
|
||||
);
|
||||
vfox.install(&self.sdk, &self.version, &out).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
use clap::Parser;
|
||||
use vfox::VfoxResult;
|
||||
|
||||
mod available;
|
||||
mod env_keys;
|
||||
mod install;
|
||||
mod plugins;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(version)]
|
||||
pub(crate) struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand)]
|
||||
enum Commands {
|
||||
Available(available::Available),
|
||||
EnvKeys(env_keys::EnvKeys),
|
||||
Install(install::Install),
|
||||
#[command(alias = "plugin")]
|
||||
Plugins(plugins::Plugins),
|
||||
}
|
||||
|
||||
impl Commands {
|
||||
pub async fn run(self) -> VfoxResult<()> {
|
||||
match self {
|
||||
Commands::Available(available) => available.run().await,
|
||||
Commands::EnvKeys(env_keys) => env_keys.run().await,
|
||||
Commands::Install(install) => install.run().await,
|
||||
Commands::Plugins(plugins) => plugins.run().await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run() -> VfoxResult<()> {
|
||||
Cli::parse().command.run().await
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
use vfox::Vfox;
|
||||
use vfox::VfoxResult;
|
||||
|
||||
#[derive(clap::Args)]
|
||||
#[command(alias = "ls")]
|
||||
pub struct List {}
|
||||
|
||||
impl List {
|
||||
pub async fn run(&self) -> VfoxResult<()> {
|
||||
let vfox = Vfox::new();
|
||||
let sdks = vfox.list_sdks()?;
|
||||
for sdk in sdks {
|
||||
println!("{sdk}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
use vfox::VfoxResult;
|
||||
|
||||
mod list;
|
||||
|
||||
#[derive(clap::Subcommand)]
|
||||
pub(crate) enum Commands {
|
||||
// Install(install::Install),
|
||||
List(list::List),
|
||||
}
|
||||
|
||||
#[derive(clap::Args)]
|
||||
pub(crate) struct Plugins {
|
||||
#[clap(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
impl Plugins {
|
||||
pub(crate) async fn run(&self) -> VfoxResult<()> {
|
||||
match &self.command {
|
||||
Commands::List(list) => list.run().await,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use std::env::consts::{ARCH, OS};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
pub plugin_dir: PathBuf,
|
||||
}
|
||||
|
||||
static CONFIG: Mutex<Option<Config>> = Mutex::new(None);
|
||||
|
||||
impl Config {
|
||||
pub fn get() -> Self {
|
||||
Self::_get().as_ref().unwrap().clone()
|
||||
}
|
||||
|
||||
fn _get() -> MutexGuard<'static, Option<Config>> {
|
||||
let mut config = CONFIG.lock().unwrap();
|
||||
if config.is_none() {
|
||||
let home = homedir::my_home()
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_else(|| PathBuf::from("/"));
|
||||
*config = Some(Config {
|
||||
plugin_dir: home.join(".version-fox/plugin"),
|
||||
});
|
||||
}
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
pub fn os() -> String {
|
||||
match OS {
|
||||
"macos" => "darwin".to_string(),
|
||||
os => os.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn arch() -> String {
|
||||
match ARCH {
|
||||
"aarch64" => "arm64".to_string(),
|
||||
"x86_64" => "amd64".to_string(),
|
||||
arch => arch.to_string(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
use mlua::{UserData, UserDataFields};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Context {
|
||||
pub args: Vec<String>,
|
||||
pub(crate) version: Option<String>,
|
||||
// pub(crate) runtime_version: String,
|
||||
}
|
||||
|
||||
impl UserData for Context {
|
||||
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("args", |_, t| Ok(t.args.clone()));
|
||||
fields.add_field_method_get("version", |_, t| Ok(t.version.clone()));
|
||||
// fields.add_field_method_get("runtimeVersion", |_, t| Ok(t.runtime_version.clone()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use mlua::Error as MLuaError;
|
||||
use thiserror::Error;
|
||||
use xx::XXError;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum VfoxError {
|
||||
#[error("{0}")]
|
||||
Error(String),
|
||||
#[error(transparent)]
|
||||
LuaError(#[from] MLuaError),
|
||||
#[error("serde_json")]
|
||||
SerdeJsonError(#[from] serde_json::Error),
|
||||
#[error(transparent)]
|
||||
XXError(#[from] XXError),
|
||||
#[error(transparent)]
|
||||
ReqwestError(#[from] reqwest::Error),
|
||||
#[error(transparent)]
|
||||
IoError(#[from] std::io::Error),
|
||||
#[error(transparent)]
|
||||
UrlParseError(#[from] url::ParseError),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, VfoxError>;
|
||||
|
||||
impl From<String> for VfoxError {
|
||||
fn from(s: String) -> Self {
|
||||
VfoxError::Error(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for VfoxError {
|
||||
fn from(s: &str) -> Self {
|
||||
VfoxError::Error(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! error {
|
||||
($($arg:tt)*) => {
|
||||
return Err(VfoxError::Error(format!($($arg)*)));
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
use mlua::prelude::LuaError;
|
||||
use mlua::{FromLua, Lua, Value};
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::Plugin;
|
||||
|
||||
impl Plugin {
|
||||
#[allow(clippy::needless_return)] // seems to be a clippy bug
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
pub async fn available(&self) -> Result<Vec<AvailableVersion>> {
|
||||
self.available_async().await
|
||||
}
|
||||
|
||||
pub async fn available_async(&self) -> Result<Vec<AvailableVersion>> {
|
||||
debug!("[vfox:{}] available_async", &self.name);
|
||||
let ctx = self.context(None)?;
|
||||
let available = self
|
||||
.eval_async(chunk! {
|
||||
require "hooks/available"
|
||||
return PLUGIN:Available($ctx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(available)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AvailableVersion {
|
||||
pub version: String,
|
||||
pub note: Option<String>,
|
||||
// pub addition: Option<Table>,
|
||||
}
|
||||
|
||||
impl FromLua for AvailableVersion {
|
||||
fn from_lua(value: Value, _: &Lua) -> std::result::Result<Self, LuaError> {
|
||||
match value {
|
||||
Value::Table(table) => {
|
||||
// TODO: try to default this to an empty table or something
|
||||
// let addition = table.get::<Option<Table>>("addition")?;
|
||||
Ok(AvailableVersion {
|
||||
version: table.get::<String>("version")?,
|
||||
note: table.get::<Option<String>>("note")?,
|
||||
// addition,
|
||||
})
|
||||
}
|
||||
_ => panic!("Expected table"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Plugin;
|
||||
|
||||
#[test]
|
||||
fn dummy() {
|
||||
let versions = run("dummy");
|
||||
assert_debug_snapshot!(versions, @r###"
|
||||
[
|
||||
"1.0.0",
|
||||
"1.0.1",
|
||||
]
|
||||
"###);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dummy_async() {
|
||||
let versions = run_async("dummy").await;
|
||||
assert_debug_snapshot!(versions, @r###"
|
||||
[
|
||||
"1.0.0",
|
||||
"1.0.1",
|
||||
]
|
||||
"###);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn nodejs_async() {
|
||||
let versions = run_async("nodejs").await;
|
||||
assert!(versions.contains(&"20.0.0".to_string()));
|
||||
}
|
||||
|
||||
fn run(plugin: &str) -> Vec<String> {
|
||||
let p = Plugin::test(plugin);
|
||||
let r = p.available().unwrap();
|
||||
r.iter().map(|v| v.version.clone()).collect()
|
||||
}
|
||||
|
||||
async fn run_async(plugin: &str) -> Vec<String> {
|
||||
let p = Plugin::test(plugin);
|
||||
let r = p.available_async().await.unwrap();
|
||||
r.iter().map(|v| v.version.clone()).collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use mlua::prelude::LuaError;
|
||||
use mlua::{FromLua, IntoLua, Lua, Value};
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::sdk_info::SdkInfo;
|
||||
use crate::Plugin;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EnvKey {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EnvKeysContext {
|
||||
pub args: Vec<String>,
|
||||
pub version: String,
|
||||
pub path: PathBuf,
|
||||
pub main: SdkInfo,
|
||||
pub sdk_info: BTreeMap<String, SdkInfo>,
|
||||
}
|
||||
|
||||
impl Plugin {
|
||||
pub async fn env_keys(&self, ctx: EnvKeysContext) -> Result<Vec<EnvKey>> {
|
||||
debug!("[vfox:{}] env_keys", &self.name);
|
||||
let env_keys = self
|
||||
.eval_async(chunk! {
|
||||
require "hooks/env_keys"
|
||||
return PLUGIN:EnvKeys($ctx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(env_keys)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoLua for EnvKeysContext {
|
||||
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
|
||||
let table = lua.create_table()?;
|
||||
table.set("version", self.version)?;
|
||||
table.set("path", self.path.to_string_lossy().to_string())?;
|
||||
table.set("sdkInfo", self.sdk_info)?;
|
||||
table.set("main", self.main)?;
|
||||
Ok(Value::Table(table))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromLua for EnvKey {
|
||||
fn from_lua(value: Value, _: &Lua) -> std::result::Result<Self, LuaError> {
|
||||
match value {
|
||||
Value::Table(table) => Ok(EnvKey {
|
||||
key: table.get::<String>("key")?,
|
||||
value: table.get::<String>("value")?,
|
||||
}),
|
||||
_ => panic!("Expected table"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use mlua::{IntoLua, Lua, LuaSerdeExt, Value};
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::hooks::env_keys::EnvKey;
|
||||
use crate::Plugin;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MiseEnvContext<T: serde::Serialize> {
|
||||
pub args: Vec<String>,
|
||||
pub options: T,
|
||||
}
|
||||
|
||||
impl Plugin {
|
||||
pub async fn mise_env<T: serde::Serialize>(
|
||||
&self,
|
||||
ctx: MiseEnvContext<T>,
|
||||
) -> Result<Vec<EnvKey>> {
|
||||
debug!("[vfox:{}] mise_env", &self.name);
|
||||
let env_keys = self
|
||||
.eval_async(chunk! {
|
||||
require "hooks/mise_env"
|
||||
return PLUGIN:MiseEnv($ctx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(env_keys)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: serde::Serialize> IntoLua for MiseEnvContext<T> {
|
||||
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
|
||||
let table = lua.create_table()?;
|
||||
table.set("options", lua.to_value(&self.options)?)?;
|
||||
Ok(Value::Table(table))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use mlua::{IntoLua, Lua, LuaSerdeExt, Value};
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::Plugin;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MisePathContext<T: serde::Serialize> {
|
||||
pub args: Vec<String>,
|
||||
pub options: T,
|
||||
}
|
||||
|
||||
impl Plugin {
|
||||
pub async fn mise_path<T: serde::Serialize>(
|
||||
&self,
|
||||
ctx: MisePathContext<T>,
|
||||
) -> Result<Vec<String>> {
|
||||
debug!("[vfox:{}] mise_path", &self.name);
|
||||
let path = self
|
||||
.eval_async(chunk! {
|
||||
require "hooks/mise_path"
|
||||
return PLUGIN:MisePath($ctx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: serde::Serialize> IntoLua for MisePathContext<T> {
|
||||
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
|
||||
let table = lua.create_table()?;
|
||||
table.set("options", lua.to_value(&self.options)?)?;
|
||||
Ok(Value::Table(table))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
pub mod available;
|
||||
pub mod env_keys;
|
||||
pub mod mise_env;
|
||||
pub mod mise_path;
|
||||
pub mod parse_legacy_file;
|
||||
pub mod post_install;
|
||||
pub mod pre_install;
|
||||
pub mod pre_use;
|
||||
@@ -0,0 +1,87 @@
|
||||
use mlua::prelude::LuaError;
|
||||
use mlua::{FromLua, IntoLua, Lua, MultiValue, Value};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::Plugin;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LegacyFileContext {
|
||||
pub args: Vec<String>,
|
||||
pub filepath: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParseLegacyFileResponse {
|
||||
pub version: Option<String>,
|
||||
}
|
||||
|
||||
impl Plugin {
|
||||
pub async fn parse_legacy_file(&self, legacy_file: &Path) -> Result<ParseLegacyFileResponse> {
|
||||
debug!("[vfox:{}] parse_legacy_file", &self.name);
|
||||
let ctx = LegacyFileContext {
|
||||
args: vec![],
|
||||
filepath: legacy_file.to_path_buf(),
|
||||
};
|
||||
let legacy_file_response = self
|
||||
.eval_async(chunk! {
|
||||
require "hooks/available"
|
||||
require "hooks/parse_legacy_file"
|
||||
return PLUGIN:ParseLegacyFile($ctx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(legacy_file_response)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoLua for LegacyFileContext {
|
||||
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
|
||||
let table = lua.create_table()?;
|
||||
table.set("args", self.args)?;
|
||||
table.set("filepath", self.filepath.to_string_lossy().to_string())?;
|
||||
table.set(
|
||||
"getInstalledVersions",
|
||||
lua.create_async_function(|lua, _input: MultiValue| async move {
|
||||
let plugin_dir = lua.named_registry_value::<PathBuf>("plugin_dir")?;
|
||||
Ok(Plugin::from_dir(plugin_dir.as_path())
|
||||
.map_err(|e| LuaError::RuntimeError(e.to_string()))?
|
||||
.available_async()
|
||||
.await
|
||||
.map_err(|e| LuaError::RuntimeError(e.to_string()))?
|
||||
.into_iter()
|
||||
.map(|v| v.version)
|
||||
.collect::<Vec<String>>())
|
||||
})?,
|
||||
)?;
|
||||
Ok(Value::Table(table))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromLua for ParseLegacyFileResponse {
|
||||
fn from_lua(value: Value, _: &Lua) -> std::result::Result<Self, LuaError> {
|
||||
match value {
|
||||
Value::Table(table) => Ok(ParseLegacyFileResponse {
|
||||
version: table.get::<Option<String>>("version")?,
|
||||
}),
|
||||
_ => panic!("Expected table"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Vfox;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_parse_legacy_file_nodejs() {
|
||||
let vfox = Vfox::test();
|
||||
let response = vfox
|
||||
.parse_legacy_file("nodejs", Path::new("test/data/.node-version"))
|
||||
.await
|
||||
.unwrap();
|
||||
let out = format!("{response:?}");
|
||||
assert_snapshot!(out, @r###"ParseLegacyFileResponse { version: Some("20.0.0") }"###);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use crate::error::Result;
|
||||
use crate::sdk_info::SdkInfo;
|
||||
use crate::Plugin;
|
||||
use mlua::{IntoLua, Lua, Value};
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
impl Plugin {
|
||||
pub async fn post_install(&self, ctx: PostInstallContext) -> Result<()> {
|
||||
debug!("[vfox:{}] post_install", &self.name);
|
||||
self.exec_async(chunk! {
|
||||
require "hooks/post_install"
|
||||
PLUGIN:PostInstall($ctx)
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PostInstallContext {
|
||||
pub root_path: PathBuf,
|
||||
pub runtime_version: String,
|
||||
pub sdk_info: BTreeMap<String, SdkInfo>,
|
||||
}
|
||||
|
||||
impl IntoLua for PostInstallContext {
|
||||
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
|
||||
let table = lua.create_table()?;
|
||||
table.set("rootPath", self.root_path.to_string_lossy().to_string())?;
|
||||
table.set("runtimeVersion", self.runtime_version)?;
|
||||
table.set("sdkInfo", self.sdk_info)?;
|
||||
Ok(Value::Table(table))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Plugin;
|
||||
use tokio::test;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
async fn dummy() {
|
||||
let p = Plugin::test("dummy");
|
||||
let ctx = PostInstallContext {
|
||||
root_path: PathBuf::from("root_path"),
|
||||
runtime_version: "runtime_version".to_string(),
|
||||
sdk_info: BTreeMap::new(),
|
||||
};
|
||||
p.post_install(ctx).await.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
use mlua::prelude::LuaError;
|
||||
use mlua::{FromLua, Lua, Value};
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::Plugin;
|
||||
|
||||
impl Plugin {
|
||||
pub async fn pre_install(&self, version: &str) -> Result<PreInstall> {
|
||||
debug!("[vfox:{}] pre_install", &self.name);
|
||||
let ctx = self.context(Some(version.to_string()))?;
|
||||
let pre_install = self
|
||||
.eval_async(chunk! {
|
||||
require "hooks/pre_install"
|
||||
return PLUGIN:PreInstall($ctx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(pre_install)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PreInstall {
|
||||
pub version: String,
|
||||
pub url: Option<String>,
|
||||
pub note: Option<String>,
|
||||
pub sha256: Option<String>,
|
||||
pub md5: Option<String>,
|
||||
pub sha1: Option<String>,
|
||||
pub sha512: Option<String>,
|
||||
// pub addition: Option<Table>,
|
||||
}
|
||||
|
||||
impl FromLua for PreInstall {
|
||||
fn from_lua(value: Value, _: &Lua) -> std::result::Result<Self, LuaError> {
|
||||
match value {
|
||||
Value::Table(table) => {
|
||||
if !table.contains_key("version")? {
|
||||
return Err(LuaError::FromLuaConversionError {
|
||||
from: "table",
|
||||
to: "PreInstall".into(),
|
||||
message: Some("no version returned from vfox plugin".to_string()),
|
||||
});
|
||||
}
|
||||
Ok(PreInstall {
|
||||
version: table.get::<String>("version")?,
|
||||
url: table.get::<Option<String>>("url")?,
|
||||
note: table.get::<Option<String>>("note")?,
|
||||
sha256: table.get::<Option<String>>("sha256")?,
|
||||
md5: table.get::<Option<String>>("md5")?,
|
||||
sha1: table.get::<Option<String>>("sha1")?,
|
||||
sha512: table.get::<Option<String>>("sha512")?,
|
||||
// addition,
|
||||
})
|
||||
}
|
||||
_ => panic!("Expected table"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::hooks::pre_install::PreInstall;
|
||||
use crate::runtime::Runtime;
|
||||
use crate::Plugin;
|
||||
use std::string::ToString;
|
||||
use tokio::test;
|
||||
|
||||
#[test]
|
||||
async fn dummy() {
|
||||
let pre_install = run("dummy", "1.0.1").await;
|
||||
assert_debug_snapshot!(pre_install);
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn nodejs() {
|
||||
Runtime::set_os("linux".to_string());
|
||||
Runtime::set_arch("x64".to_string());
|
||||
let pre_install = run("nodejs", "20.0.0").await;
|
||||
assert_debug_snapshot!(pre_install);
|
||||
|
||||
Runtime::set_os("macos".to_string());
|
||||
Runtime::set_arch("arm64".to_string());
|
||||
let pre_install = run("nodejs", "20.1.0").await;
|
||||
assert_debug_snapshot!(pre_install);
|
||||
|
||||
Runtime::set_os("windows".to_string());
|
||||
Runtime::set_arch("x64".to_string());
|
||||
let pre_install = run("nodejs", "20.3.0").await;
|
||||
assert_debug_snapshot!(pre_install);
|
||||
|
||||
Runtime::reset();
|
||||
}
|
||||
|
||||
async fn run(plugin: &str, v: &str) -> PreInstall {
|
||||
let p = Plugin::test(plugin);
|
||||
p.pre_install(v).await.unwrap()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
use indexmap::IndexMap;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::sdk_info::SdkInfo;
|
||||
use crate::{Plugin, Vfox};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct PreUseContext {
|
||||
pub installed_sdks: IndexMap<String, SdkInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PreUseResponse {
|
||||
pub version: Option<String>,
|
||||
}
|
||||
|
||||
impl Plugin {
|
||||
pub async fn pre_use(&self, _vfox: &Vfox, _legacy_file: &Path) -> Result<PreUseResponse> {
|
||||
debug!("[vfox:{}] pre_use", &self.name);
|
||||
// let ctx = PreUseContext {
|
||||
// installed_sdks: vfox.list_installed_versions(&self.name)?,
|
||||
// };
|
||||
|
||||
unimplemented!("pre_use hook is not implemented");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
source: src/hooks/pre_install.rs
|
||||
expression: pre_install
|
||||
---
|
||||
PreInstall {
|
||||
version: "1.0.1",
|
||||
url: None,
|
||||
note: None,
|
||||
sha256: None,
|
||||
md5: None,
|
||||
sha1: None,
|
||||
sha512: None,
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
source: src/hooks/pre_install.rs
|
||||
expression: pre_install
|
||||
---
|
||||
PreInstall {
|
||||
version: "20.1.0",
|
||||
url: Some(
|
||||
"https://nodejs.org/dist/v20.1.0/node-v20.1.0-macos-arm64.tar.gz",
|
||||
),
|
||||
note: None,
|
||||
sha256: None,
|
||||
md5: None,
|
||||
sha1: None,
|
||||
sha512: None,
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
source: src/hooks/pre_install.rs
|
||||
expression: pre_install
|
||||
---
|
||||
PreInstall {
|
||||
version: "20.3.0",
|
||||
url: Some(
|
||||
"https://nodejs.org/dist/v20.3.0/node-v20.3.0-win-x64.zip",
|
||||
),
|
||||
note: None,
|
||||
sha256: Some(
|
||||
"43be53f9f4d6fa19e27efdb724e10cbdf3c7abfaebe0d852af62fc80c6f465a2",
|
||||
),
|
||||
md5: None,
|
||||
sha1: None,
|
||||
sha512: None,
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
source: src/hooks/pre_install.rs
|
||||
expression: pre_install
|
||||
---
|
||||
PreInstall {
|
||||
version: "20.0.0",
|
||||
url: Some(
|
||||
"https://nodejs.org/dist/v20.0.0/node-v20.0.0-linux-x64.tar.gz",
|
||||
),
|
||||
note: None,
|
||||
sha256: Some(
|
||||
"95439a0a836fc858565c7b6a7b1e55d1741901e6f205485e496abbefe973cfce",
|
||||
),
|
||||
md5: None,
|
||||
sha1: None,
|
||||
sha512: None,
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
use reqwest::{Client, ClientBuilder};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
pub static CLIENT: LazyLock<Client> = LazyLock::new(|| {
|
||||
ClientBuilder::new()
|
||||
.user_agent(format!("vfox.rs/{}", env!("CARGO_PKG_VERSION")))
|
||||
.build()
|
||||
.expect("Failed to create reqwest client")
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
extern crate insta;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
#[macro_use]
|
||||
extern crate mlua;
|
||||
|
||||
pub use error::Result as VfoxResult;
|
||||
pub use error::VfoxError;
|
||||
pub use plugin::Plugin;
|
||||
pub use vfox::Vfox;
|
||||
|
||||
mod config;
|
||||
mod context;
|
||||
mod error;
|
||||
mod hooks;
|
||||
mod http;
|
||||
mod lua_mod;
|
||||
mod metadata;
|
||||
mod plugin;
|
||||
mod registry;
|
||||
mod runtime;
|
||||
mod sdk_info;
|
||||
mod vfox;
|
||||
@@ -0,0 +1,59 @@
|
||||
use crate::error::Result;
|
||||
use mlua::{ExternalResult, Lua, MultiValue, Table};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn mod_archiver(lua: &Lua) -> Result<()> {
|
||||
let package: Table = lua.globals().get("package")?;
|
||||
let loaded: Table = package.get("loaded")?;
|
||||
Ok(loaded.set(
|
||||
"archiver",
|
||||
lua.create_table_from(vec![(
|
||||
"decompress",
|
||||
lua.create_async_function(
|
||||
|_lua: mlua::Lua, input| async move { decompress(&_lua, input) },
|
||||
)?,
|
||||
)])?,
|
||||
)?)
|
||||
}
|
||||
|
||||
fn decompress(_lua: &Lua, input: MultiValue) -> mlua::Result<()> {
|
||||
let paths: Vec<mlua::Value> = input.into_iter().collect();
|
||||
let archive: PathBuf = PathBuf::from(paths[0].to_string()?);
|
||||
let destination: PathBuf = PathBuf::from(paths[1].to_string()?);
|
||||
let filename = archive.file_name().unwrap().to_str().unwrap();
|
||||
if filename.ends_with(".zip") {
|
||||
xx::archive::unzip(&archive, &destination).into_lua_err()?;
|
||||
} else if filename.ends_with(".tar.gz") {
|
||||
xx::archive::untar_gz(&archive, &destination).into_lua_err()?;
|
||||
} else if filename.ends_with(".tar.xz") {
|
||||
xx::archive::untar_xz(&archive, &destination).into_lua_err()?;
|
||||
} else if filename.ends_with(".tar.bz2") {
|
||||
xx::archive::untar_bz2(&archive, &destination).into_lua_err()?;
|
||||
} else {
|
||||
unimplemented!("Unsupported archive format {:?}", archive);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_zip() {
|
||||
let _ = std::fs::remove_dir_all("/tmp/test_zip_dst");
|
||||
let lua = Lua::new();
|
||||
mod_archiver(&lua).unwrap();
|
||||
lua.load(mlua::chunk! {
|
||||
local archiver = require("archiver")
|
||||
archiver.decompress("test/data/foo.zip", "/tmp/test_zip_dst")
|
||||
})
|
||||
.exec()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
std::fs::read_to_string("/tmp/test_zip_dst/foo/test.txt").unwrap(),
|
||||
"yep\n"
|
||||
);
|
||||
std::fs::remove_dir_all("/tmp/test_zip_dst").unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use mlua::prelude::*;
|
||||
use mlua::Table;
|
||||
|
||||
pub fn mod_env(lua: &Lua) -> LuaResult<()> {
|
||||
let package: Table = lua.globals().get("package")?;
|
||||
let loaded: Table = package.get("loaded")?;
|
||||
let env = lua.create_table_from(vec![("setenv", lua.create_function(setenv)?)])?;
|
||||
loaded.set("env", env.clone())?;
|
||||
loaded.set("vfox.env", env)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setenv(_lua: &Lua, (key, val): (String, String)) -> LuaResult<()> {
|
||||
std::env::set_var(key, val);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_env() {
|
||||
let lua = Lua::new();
|
||||
mod_env(&lua).unwrap();
|
||||
if cfg!(windows) {
|
||||
lua.load(mlua::chunk! {
|
||||
local env = require("env")
|
||||
env.setenv("TEST_ENV", "myvar")
|
||||
handle = io.popen("pwsh -Command \"echo $env:TEST_ENV\"")
|
||||
result = handle:read("*a")
|
||||
handle:close()
|
||||
assert(result == "myvar\n")
|
||||
})
|
||||
.exec()
|
||||
.unwrap();
|
||||
} else {
|
||||
lua.load(mlua::chunk! {
|
||||
local env = require("env")
|
||||
env.setenv("TEST_ENV", "myvar")
|
||||
handle = io.popen("echo $TEST_ENV")
|
||||
result = handle:read("*a")
|
||||
handle:close()
|
||||
assert(result == "myvar\n")
|
||||
})
|
||||
.exec()
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
use crate::error::Result;
|
||||
use mlua::{ExternalResult, Lua, MultiValue, Table};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::symlink as _symlink;
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::fs::symlink_dir;
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::fs::symlink_file;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn mod_file(lua: &Lua) -> Result<()> {
|
||||
let package: Table = lua.globals().get("package")?;
|
||||
let loaded: Table = package.get("loaded")?;
|
||||
Ok(loaded.set(
|
||||
"file",
|
||||
lua.create_table_from(vec![(
|
||||
"symlink",
|
||||
lua.create_async_function(|_lua: mlua::Lua, input| async move {
|
||||
symlink(&_lua, input).await
|
||||
})?,
|
||||
)])?,
|
||||
)?)
|
||||
}
|
||||
|
||||
async fn symlink(_lua: &Lua, input: MultiValue) -> mlua::Result<()> {
|
||||
let input: Vec<String> = input
|
||||
.into_iter()
|
||||
.map(|v| v.to_string())
|
||||
.collect::<mlua::Result<_>>()?;
|
||||
let src = Path::new(&input[0]);
|
||||
let dst = Path::new(&input[1]);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if src.is_dir() {
|
||||
symlink_dir(src, dst).into_lua_err()?;
|
||||
} else {
|
||||
symlink_file(src, dst).into_lua_err()?;
|
||||
}
|
||||
}
|
||||
#[cfg(unix)]
|
||||
_symlink(src, dst).into_lua_err()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn test_symlink() {
|
||||
let _ = fs::remove_file("/tmp/test_symlink_dst");
|
||||
let lua = Lua::new();
|
||||
mod_file(&lua).unwrap();
|
||||
lua.load(mlua::chunk! {
|
||||
local file = require("file")
|
||||
file.symlink("/tmp/test_symlink_src", "/tmp/test_symlink_dst")
|
||||
})
|
||||
.exec()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
fs::read_link("/tmp/test_symlink_dst").unwrap(),
|
||||
Path::new("/tmp/test_symlink_src")
|
||||
);
|
||||
fs::remove_file("/tmp/test_symlink_dst").unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use crate::error::Result;
|
||||
use mlua::Lua;
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::Path;
|
||||
|
||||
pub struct HookFunc {
|
||||
_name: &'static str,
|
||||
required: bool,
|
||||
filename: &'static str,
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
const HOOK_FUNCS: [HookFunc; 9] = [
|
||||
HookFunc { _name: "Available", required: false, filename: "available" },
|
||||
HookFunc { _name: "PreInstall", required: false, filename: "pre_install" },
|
||||
HookFunc { _name: "EnvKeys", required: false, filename: "env_keys" },
|
||||
HookFunc { _name: "PostInstall", required: false, filename: "post_install" },
|
||||
HookFunc { _name: "PreUse", required: false, filename: "pre_use" },
|
||||
HookFunc { _name: "ParseLegacyFile", required: false, filename: "parse_legacy_file" },
|
||||
HookFunc { _name: "PreUninstall", required: false, filename: "pre_uninstall" },
|
||||
|
||||
// mise
|
||||
HookFunc { _name: "MiseEnv", required: false, filename: "mise_env" },
|
||||
HookFunc { _name: "MisePath", required: false, filename: "mise_path" },
|
||||
];
|
||||
|
||||
pub fn mod_hooks(lua: &Lua, root: &Path) -> Result<BTreeSet<&'static str>> {
|
||||
let mut hooks = BTreeSet::new();
|
||||
for hook in &HOOK_FUNCS {
|
||||
let hook_path = root.join("hooks").join(format!("{}.lua", hook.filename));
|
||||
if hook_path.exists() {
|
||||
lua.load(hook_path).exec()?;
|
||||
hooks.insert(hook.filename);
|
||||
} else if hook.required {
|
||||
return Err(format!("Required hook '{}' not found", hook.filename).into());
|
||||
}
|
||||
}
|
||||
Ok(hooks)
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
use mlua::{Lua, Table};
|
||||
|
||||
pub fn mod_html(lua: &Lua) -> mlua::Result<()> {
|
||||
let package: Table = lua.globals().get("package")?;
|
||||
let loaded: Table = package.get("loaded")?;
|
||||
loaded.set(
|
||||
"htmlparser.voidelements",
|
||||
lua.load(include_str!("../../lua/htmlparser/voidelements.lua"))
|
||||
.eval::<Table>()?,
|
||||
)?;
|
||||
loaded.set(
|
||||
"htmlparser.ElementNode",
|
||||
lua.load(include_str!("../../lua/htmlparser/ElementNode.lua"))
|
||||
.eval::<Table>()?,
|
||||
)?;
|
||||
loaded.set(
|
||||
"htmlparser",
|
||||
lua.load(include_str!("../../lua/htmlparser.lua"))
|
||||
.eval::<Table>()?,
|
||||
)?;
|
||||
loaded.set(
|
||||
"html",
|
||||
lua.load(mlua::chunk! {
|
||||
local htmlparser = require("htmlparser")
|
||||
return {
|
||||
parse = function(s)
|
||||
Node = {
|
||||
find = function(self, tag)
|
||||
local nodes = self.node:select(tag)
|
||||
return Node.new(nodes)
|
||||
end,
|
||||
first = function(self)
|
||||
return Node.new({self.nodes[1]})
|
||||
end,
|
||||
eq = function(self, idx)
|
||||
local node = self.nodes[idx + 1]
|
||||
return Node.new({node})
|
||||
end,
|
||||
each = function(self, f)
|
||||
for i, node in ipairs(self.nodes) do
|
||||
f(i - 1, Node.new({node}))
|
||||
end
|
||||
end,
|
||||
text = function(self)
|
||||
if self.node == nil then
|
||||
return ""
|
||||
end
|
||||
return self.node:getcontent()
|
||||
end,
|
||||
attr = function(self, key)
|
||||
if self.node == nil then
|
||||
return ""
|
||||
end
|
||||
return self.node.attributes[key]
|
||||
end,
|
||||
}
|
||||
Node.new = function(nodes)
|
||||
return setmetatable({nodes = nodes, node = nodes[1]}, {__index = Node})
|
||||
end
|
||||
local root = htmlparser.parse(s, 100000)
|
||||
return Node.new({root})
|
||||
end
|
||||
}
|
||||
})
|
||||
.eval::<Table>()?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::lua_mod::http::mod_http;
|
||||
|
||||
#[test]
|
||||
fn test_html() {
|
||||
let lua = Lua::new();
|
||||
mod_html(&lua).unwrap();
|
||||
lua.load(mlua::chunk! {
|
||||
local html = require("html")
|
||||
local doc = html.parse("<html><body><div id='t2' name='123'>456</div><DIV foo='bar'>222</DIV></body></html>")
|
||||
local f = doc:find("div"):eq(0)
|
||||
local s = doc:find("div"):eq(1)
|
||||
assert(s:text() == "222")
|
||||
assert(f:text() == "456")
|
||||
|
||||
assert(s:attr("foo") == "bar")
|
||||
|
||||
doc:find("div"):each(function(i, e)
|
||||
if i == 0 then
|
||||
assert(e:text() == "456")
|
||||
else
|
||||
assert(e:text() == "222")
|
||||
end
|
||||
end)
|
||||
})
|
||||
.exec()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // TODO: make this actually work
|
||||
async fn test_html_go() {
|
||||
let lua = Lua::new();
|
||||
mod_html(&lua).unwrap();
|
||||
mod_http(&lua).unwrap();
|
||||
lua.load(mlua::chunk! {
|
||||
local http = require("http")
|
||||
local html = require("html")
|
||||
|
||||
table = {}
|
||||
|
||||
resp, err = http.get({
|
||||
url = "https://go.dev/dl/"
|
||||
})
|
||||
if err ~= nil or resp.status_code ~= 200 then
|
||||
error("parsing release info failed." .. err)
|
||||
end
|
||||
local doc = html.parse(resp.body)
|
||||
local listDoc = doc:find("div#archive")
|
||||
listDoc:find(".toggle"):each(function(i, selection)
|
||||
local versionStr = selection:attr("id")
|
||||
if versionStr ~= nil then
|
||||
selection:find("table.downloadtable tr"):each(function(ti, ts)
|
||||
local td = ts:find("td")
|
||||
local filename = td:eq(0):text()
|
||||
local kind = td:eq(1):text()
|
||||
local os = td:eq(2):text()
|
||||
local arch = td:eq(3):text()
|
||||
local checksum = td:eq(5):text()
|
||||
if kind == "Archive" and os == "Windows" and arch == "x86-64" then
|
||||
table.insert(result, {
|
||||
version = string.sub(versionStr, 3),
|
||||
url = "https://go.dev/dl/" .. filename,
|
||||
note = "",
|
||||
sha256 = checksum,
|
||||
})
|
||||
end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
print(table)
|
||||
// TODO: check results
|
||||
})
|
||||
.exec_async()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
use mlua::{ExternalResult, Lua, MultiValue, Result, Table};
|
||||
|
||||
use crate::http::CLIENT;
|
||||
|
||||
pub fn mod_http(lua: &Lua) -> Result<()> {
|
||||
let package: Table = lua.globals().get("package")?;
|
||||
let loaded: Table = package.get("loaded")?;
|
||||
loaded.set(
|
||||
"http",
|
||||
lua.create_table_from(vec![
|
||||
(
|
||||
"get",
|
||||
lua.create_async_function(|lua: mlua::Lua, input| async move {
|
||||
get(&lua, input).await
|
||||
})?,
|
||||
),
|
||||
(
|
||||
"head",
|
||||
lua.create_async_function(|lua: mlua::Lua, input| async move {
|
||||
head(&lua, input).await
|
||||
})?,
|
||||
),
|
||||
(
|
||||
"download_file",
|
||||
lua.create_async_function(|_lua: mlua::Lua, input| async move {
|
||||
download_file(&_lua, input).await
|
||||
})?,
|
||||
),
|
||||
])?,
|
||||
)
|
||||
}
|
||||
|
||||
async fn get(lua: &Lua, input: Table) -> Result<Table> {
|
||||
let url: String = input.get("url").into_lua_err()?;
|
||||
let resp = CLIENT.get(&url).send().await.into_lua_err()?;
|
||||
let t = lua.create_table()?;
|
||||
t.set("status_code", resp.status().as_u16())?;
|
||||
t.set("headers", get_headers(lua, resp.headers())?)?;
|
||||
t.set("body", resp.text().await.into_lua_err()?)?;
|
||||
Ok(t)
|
||||
}
|
||||
|
||||
async fn download_file(_lua: &Lua, input: MultiValue) -> Result<()> {
|
||||
let t: &Table = input.iter().next().unwrap().as_table().unwrap();
|
||||
let url: String = t.get("url").into_lua_err()?;
|
||||
let path: String = input.iter().nth(1).unwrap().to_string()?;
|
||||
let resp = CLIENT.get(&url).send().await.into_lua_err()?;
|
||||
resp.error_for_status_ref().into_lua_err()?;
|
||||
let mut file = tokio::fs::File::create(&path).await.into_lua_err()?;
|
||||
let bytes = resp.bytes().await.into_lua_err()?;
|
||||
tokio::io::AsyncWriteExt::write_all(&mut file, &bytes)
|
||||
.await
|
||||
.into_lua_err()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn head(lua: &Lua, input: Table) -> Result<Table> {
|
||||
let url: String = input.get("url").into_lua_err()?;
|
||||
let resp = CLIENT.head(&url).send().await.into_lua_err()?;
|
||||
let t = lua.create_table()?;
|
||||
t.set("status_code", resp.status().as_u16())?;
|
||||
t.set("headers", get_headers(lua, resp.headers())?)?;
|
||||
Ok(t)
|
||||
}
|
||||
|
||||
fn get_headers(lua: &Lua, headers: &reqwest::header::HeaderMap) -> Result<Table> {
|
||||
let t = lua.create_table()?;
|
||||
for (name, value) in headers.iter() {
|
||||
t.set(name.as_str(), value.to_str().into_lua_err()?)?;
|
||||
}
|
||||
Ok(t)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get() {
|
||||
let lua = Lua::new();
|
||||
mod_http(&lua).unwrap();
|
||||
lua.load(mlua::chunk! {
|
||||
local http = require("http")
|
||||
local resp = http.get({ url = "https://httpbin.org/get" })
|
||||
assert(resp.status_code == 200)
|
||||
assert(type(resp.body) == "string")
|
||||
})
|
||||
.exec_async()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_head() {
|
||||
let lua = Lua::new();
|
||||
mod_http(&lua).unwrap();
|
||||
lua.load(mlua::chunk! {
|
||||
local http = require("http")
|
||||
local resp = http.head({ url = "https://httpbin.org/get" })
|
||||
print(resp.headers)
|
||||
assert(resp.status_code == 200)
|
||||
assert(type(resp.headers) == "table")
|
||||
assert(resp.headers["content-type"] == "application/json")
|
||||
assert(resp.content_length == nil)
|
||||
})
|
||||
.exec_async()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // TODO: find out why this often fails in CI
|
||||
async fn test_download_file() {
|
||||
let lua = Lua::new();
|
||||
mod_http(&lua).unwrap();
|
||||
let path = "test/data/test_download_file.txt";
|
||||
lua.load(mlua::chunk! {
|
||||
local http = require("http")
|
||||
err = http.download_file({
|
||||
url = "https://vfox-plugins.lhan.me/index.json",
|
||||
headers = {}
|
||||
}, $path)
|
||||
assert(err == nil, [[must be nil]])
|
||||
})
|
||||
.exec_async()
|
||||
.await
|
||||
.unwrap();
|
||||
// TODO: figure out why this fails on gha
|
||||
assert!(fs::read_to_string(path).unwrap().contains("vfox-nodejs"));
|
||||
tokio::fs::remove_file(path).await.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
use mlua::{ExternalResult, Lua, LuaSerdeExt, Result, Table, Value};
|
||||
|
||||
pub fn mod_json(lua: &Lua) -> Result<()> {
|
||||
let package: Table = lua.globals().get("package")?;
|
||||
let loaded: Table = package.get("loaded")?;
|
||||
loaded.set(
|
||||
"json",
|
||||
lua.create_table_from(vec![
|
||||
("encode", lua.create_function(encode)?),
|
||||
("decode", lua.create_function(decode)?),
|
||||
])?,
|
||||
)
|
||||
}
|
||||
|
||||
fn encode(_lua: &Lua, value: Value) -> Result<String> {
|
||||
serde_json::to_string(&value).into_lua_err()
|
||||
}
|
||||
|
||||
fn decode(lua: &Lua, value: String) -> Result<Value> {
|
||||
let value: serde_json::Value = serde_json::from_str(&value).into_lua_err()?;
|
||||
lua.to_value(&value)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_encode() {
|
||||
let lua = Lua::new();
|
||||
mod_json(&lua).unwrap();
|
||||
lua.load(mlua::chunk! {
|
||||
local json = require("json")
|
||||
local obj = { "a", 1, "b", 2, "c", 3 }
|
||||
local jsonStr = json.encode(obj)
|
||||
assert(jsonStr == "[\"a\",1,\"b\",2,\"c\",3]")
|
||||
})
|
||||
.exec()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode() {
|
||||
let lua = Lua::new();
|
||||
mod_json(&lua).unwrap();
|
||||
lua.load(mlua::chunk! {
|
||||
local json = require("json")
|
||||
local obj = json.decode("[\"a\",1,\"b\",2,\"c\",3]")
|
||||
assert(obj[1] == "a")
|
||||
assert(obj[2] == 1)
|
||||
assert(obj[3] == "b")
|
||||
assert(obj[4] == 2)
|
||||
assert(obj[5] == "c")
|
||||
assert(obj[6] == 3)
|
||||
})
|
||||
.exec()
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
mod archiver;
|
||||
mod env;
|
||||
mod file;
|
||||
mod hooks;
|
||||
mod html;
|
||||
mod http;
|
||||
mod json;
|
||||
mod strings;
|
||||
|
||||
pub use archiver::mod_archiver as archiver;
|
||||
pub use env::mod_env as env;
|
||||
pub use file::mod_file as file;
|
||||
pub use hooks::mod_hooks as hooks;
|
||||
pub use html::mod_html as html;
|
||||
pub use http::mod_http as http;
|
||||
pub use json::mod_json as json;
|
||||
pub use strings::mod_strings as strings;
|
||||
@@ -0,0 +1,82 @@
|
||||
use mlua::prelude::*;
|
||||
use mlua::{Table, Value};
|
||||
|
||||
pub fn mod_strings(lua: &Lua) -> LuaResult<()> {
|
||||
let package: Table = lua.globals().get("package")?;
|
||||
let loaded: Table = package.get("loaded")?;
|
||||
let strings = lua.create_table_from(vec![
|
||||
("split", lua.create_function(split)?),
|
||||
("has_prefix", lua.create_function(has_prefix)?),
|
||||
("has_suffix", lua.create_function(has_suffix)?),
|
||||
("trim", lua.create_function(trim)?),
|
||||
("trim_space", lua.create_function(trim_space)?),
|
||||
("contains", lua.create_function(contains)?),
|
||||
("join", lua.create_function(join)?),
|
||||
])?;
|
||||
loaded.set("strings", strings.clone())?;
|
||||
loaded.set("vfox.strings", strings)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn split(_lua: &Lua, (s, sep): (String, String)) -> LuaResult<Vec<String>> {
|
||||
Ok(s.split(&sep).map(|s| s.to_string()).collect())
|
||||
}
|
||||
|
||||
fn has_prefix(_lua: &Lua, (s, prefix): (String, String)) -> LuaResult<bool> {
|
||||
Ok(s.starts_with(&prefix))
|
||||
}
|
||||
|
||||
fn has_suffix(_lua: &Lua, (s, suffix): (String, String)) -> LuaResult<bool> {
|
||||
Ok(s.ends_with(&suffix))
|
||||
}
|
||||
|
||||
fn trim(_lua: &Lua, (s, suffix): (String, String)) -> LuaResult<String> {
|
||||
Ok(s.trim_end_matches(&suffix).to_string())
|
||||
}
|
||||
|
||||
fn trim_space(_lua: &Lua, s: String) -> LuaResult<String> {
|
||||
Ok(s.trim().to_string())
|
||||
}
|
||||
|
||||
fn contains(_lua: &Lua, (s, substr): (String, String)) -> LuaResult<bool> {
|
||||
Ok(s.contains(&substr))
|
||||
}
|
||||
|
||||
fn join(_lua: &Lua, (arr, sep): (Vec<Value>, String)) -> LuaResult<String> {
|
||||
let mut res = String::new();
|
||||
for (i, v) in arr.iter().enumerate() {
|
||||
if i > 0 {
|
||||
res.push_str(&sep);
|
||||
}
|
||||
res.push_str(&v.to_string()?);
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_strings() {
|
||||
let lua = Lua::new();
|
||||
mod_strings(&lua).unwrap();
|
||||
lua.load(mlua::chunk! {
|
||||
local strings = require("strings")
|
||||
local str_parts = strings.split("hello world", " ")
|
||||
print(str_parts[1]) -- hello
|
||||
|
||||
assert(strings.has_prefix("hello world", "hello"), [[not strings.has_prefix("hello")]])
|
||||
assert(strings.has_suffix("hello world", "world"), [[not strings.has_suffix("world")]])
|
||||
assert(strings.trim("hello world", "world") == "hello ", "strings.trim()")
|
||||
assert(strings.contains("hello world", "hello ") == true, "strings.contains()")
|
||||
|
||||
// got = strings.trim_space(tt.input)
|
||||
//
|
||||
// local str = strings.join({"1",3,"4"},";")
|
||||
// assert(str == "1;3;4", "strings.join()")
|
||||
})
|
||||
.exec()
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use mlua::Table;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::error::VfoxError;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Metadata {
|
||||
pub name: String,
|
||||
pub legacy_filenames: Vec<String>,
|
||||
pub version: String,
|
||||
pub description: Option<String>,
|
||||
pub author: Option<String>,
|
||||
pub license: Option<String>,
|
||||
pub homepage: Option<String>,
|
||||
pub hooks: BTreeSet<&'static str>,
|
||||
}
|
||||
|
||||
impl TryFrom<Table> for Metadata {
|
||||
type Error = VfoxError;
|
||||
fn try_from(t: Table) -> Result<Self> {
|
||||
let legacy_filenames = t
|
||||
.get::<Option<Vec<String>>>("legacyFilenames")?
|
||||
.unwrap_or_default();
|
||||
Ok(Metadata {
|
||||
name: t.get("name")?,
|
||||
legacy_filenames,
|
||||
version: t.get("version")?,
|
||||
description: t.get("description")?,
|
||||
author: t.get("author")?,
|
||||
license: t.get("license")?,
|
||||
homepage: t.get("homepage")?,
|
||||
hooks: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt::Display;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use mlua::{AsChunk, FromLuaMulti, IntoLua, Lua, Table};
|
||||
use once_cell::sync::OnceCell;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::error::Result;
|
||||
use crate::metadata::Metadata;
|
||||
use crate::runtime::Runtime;
|
||||
use crate::sdk_info::SdkInfo;
|
||||
use crate::{config, error, lua_mod, VfoxError};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Plugin {
|
||||
pub name: String,
|
||||
pub dir: PathBuf,
|
||||
lua: Lua,
|
||||
metadata: OnceCell<Metadata>,
|
||||
}
|
||||
|
||||
impl Plugin {
|
||||
pub fn from_dir(dir: &Path) -> Result<Self> {
|
||||
if !dir.exists() {
|
||||
error!("Plugin directory not found: {:?}", dir);
|
||||
}
|
||||
let lua = Lua::new();
|
||||
lua.set_named_registry_value("plugin_dir", dir.to_path_buf())?;
|
||||
Ok(Self {
|
||||
name: dir.file_name().unwrap().to_string_lossy().to_string(),
|
||||
dir: dir.to_path_buf(),
|
||||
lua,
|
||||
metadata: OnceCell::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_name(name: &str) -> Result<Self> {
|
||||
let dir = Config::get().plugin_dir.join(name);
|
||||
Self::from_dir(&dir)
|
||||
}
|
||||
|
||||
pub fn list() -> Result<Vec<String>> {
|
||||
let config = Config::get();
|
||||
if !config.plugin_dir.exists() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let plugins = xx::file::ls(&config.plugin_dir)?;
|
||||
let plugins = plugins
|
||||
.iter()
|
||||
.filter_map(|p| {
|
||||
p.file_name()
|
||||
.and_then(|f| f.to_str())
|
||||
.map(|s| s.to_string())
|
||||
})
|
||||
.collect();
|
||||
Ok(plugins)
|
||||
}
|
||||
|
||||
pub fn get_metadata(&self) -> Result<Metadata> {
|
||||
Ok(self.load()?.clone())
|
||||
}
|
||||
|
||||
pub fn sdk_info(&self, version: String, install_dir: PathBuf) -> Result<SdkInfo> {
|
||||
Ok(SdkInfo::new(
|
||||
self.get_metadata()?.name.clone(),
|
||||
version,
|
||||
install_dir,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn test(name: &str) -> Self {
|
||||
let dir = PathBuf::from("plugins").join(name);
|
||||
Self::from_dir(&dir).unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn context(&self, version: Option<String>) -> Result<Context> {
|
||||
let ctx = Context {
|
||||
args: vec![],
|
||||
version,
|
||||
// version: "1.0.0".to_string(),
|
||||
// runtime_version: "xxx".to_string(),
|
||||
};
|
||||
Ok(ctx)
|
||||
}
|
||||
|
||||
pub(crate) async fn exec_async<'a>(&self, chunk: impl AsChunk<'a>) -> Result<()> {
|
||||
self.load()?;
|
||||
let chunk = self.lua.load(chunk);
|
||||
chunk.exec_async().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn eval_async<'a, R>(&self, chunk: impl AsChunk<'a>) -> Result<R>
|
||||
where
|
||||
R: FromLuaMulti,
|
||||
{
|
||||
self.load()?;
|
||||
let chunk = self.lua.load(chunk);
|
||||
let result = chunk.eval_async().await?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn load(&self) -> Result<&Metadata> {
|
||||
self.metadata.get_or_try_init(|| {
|
||||
debug!("Getting metadata for {self}");
|
||||
set_paths(
|
||||
&self.lua,
|
||||
&[
|
||||
self.dir.join("?.lua"), //xx
|
||||
self.dir.join("hooks/?.lua"),
|
||||
self.dir.join("lib/?.lua"),
|
||||
],
|
||||
)?;
|
||||
|
||||
lua_mod::archiver(&self.lua)?;
|
||||
lua_mod::file(&self.lua)?;
|
||||
lua_mod::html(&self.lua)?;
|
||||
lua_mod::http(&self.lua)?;
|
||||
lua_mod::json(&self.lua)?;
|
||||
lua_mod::strings(&self.lua)?;
|
||||
lua_mod::env(&self.lua)?;
|
||||
|
||||
let metadata = self.load_metadata()?;
|
||||
self.set_global("PLUGIN", metadata.clone())?;
|
||||
self.set_global("RUNTIME", Runtime::get(self.dir.clone()))?;
|
||||
self.set_global("OS_TYPE", config::os())?;
|
||||
self.set_global("ARCH_TYPE", config::arch())?;
|
||||
|
||||
let mut metadata: Metadata = metadata.try_into()?;
|
||||
|
||||
metadata.hooks = lua_mod::hooks(&self.lua, &self.dir)?;
|
||||
|
||||
Ok(metadata)
|
||||
})
|
||||
}
|
||||
|
||||
fn set_global<V>(&self, name: &str, value: V) -> Result<()>
|
||||
where
|
||||
V: IntoLua,
|
||||
{
|
||||
self.lua.globals().set(name, value)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_metadata(&self) -> Result<Table> {
|
||||
let metadata = self
|
||||
.lua
|
||||
.load(
|
||||
r#"
|
||||
require "metadata"
|
||||
return PLUGIN
|
||||
"#,
|
||||
)
|
||||
.eval()?;
|
||||
Ok(metadata)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_package(lua: &Lua) -> Result<Table> {
|
||||
let package = lua.globals().get::<Table>("package")?;
|
||||
Ok(package)
|
||||
}
|
||||
|
||||
fn set_paths(lua: &Lua, paths: &[PathBuf]) -> Result<()> {
|
||||
let paths = paths
|
||||
.iter()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(";");
|
||||
|
||||
get_package(lua)?.set("path", paths)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Display for Plugin {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Self> for Plugin {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.dir == other.dir
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Plugin {}
|
||||
|
||||
impl PartialOrd for Plugin {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Plugin {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.name.cmp(&other.name)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::str::FromStr;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use url::Url;
|
||||
|
||||
static SDKS: Lazy<BTreeMap<String, Url>> = Lazy::new(|| {
|
||||
[
|
||||
("nodejs", "https://github.com/version-fox/vfox-nodejs"),
|
||||
("cmake", "https://github.com/version-fox/vfox-cmake"),
|
||||
]
|
||||
.iter()
|
||||
.map(|(name, url)| (name.to_string(), Url::from_str(url).unwrap()))
|
||||
.collect()
|
||||
});
|
||||
|
||||
pub fn sdk_url(name: &str) -> Option<&Url> {
|
||||
SDKS.get(name)
|
||||
}
|
||||
|
||||
pub fn list_sdks() -> &'static BTreeMap<String, Url> {
|
||||
&SDKS
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
use crate::config::{arch, os};
|
||||
use mlua::{UserData, UserDataFields};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Runtime {
|
||||
pub(crate) os: String,
|
||||
pub(crate) arch: String,
|
||||
pub(crate) version: String,
|
||||
pub(crate) plugin_dir_path: PathBuf,
|
||||
}
|
||||
|
||||
static RUNTIME: Lazy<Mutex<Runtime>> = Lazy::new(|| {
|
||||
Mutex::new(Runtime {
|
||||
os: os(),
|
||||
arch: arch(),
|
||||
version: "0.6.0".to_string(), // https://github.com/version-fox/vfox/releases
|
||||
plugin_dir_path: PathBuf::new(),
|
||||
})
|
||||
});
|
||||
|
||||
impl Runtime {
|
||||
pub(crate) fn get(plugin_dir_path: PathBuf) -> Runtime {
|
||||
let mut runtime = RUNTIME.lock().unwrap().clone();
|
||||
runtime.plugin_dir_path = plugin_dir_path;
|
||||
runtime
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn set_os(os: String) {
|
||||
let mut runtime = RUNTIME.lock().unwrap();
|
||||
runtime.os = os;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn set_arch(arch: String) {
|
||||
let mut runtime = RUNTIME.lock().unwrap();
|
||||
runtime.arch = arch;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn reset() {
|
||||
let mut runtime = RUNTIME.lock().unwrap();
|
||||
runtime.os = os();
|
||||
runtime.arch = arch();
|
||||
}
|
||||
}
|
||||
|
||||
impl UserData for Runtime {
|
||||
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("osType", |_, t| Ok(t.os.clone()));
|
||||
fields.add_field_method_get("archType", |_, t| Ok(t.arch.clone()));
|
||||
fields.add_field_method_get("version", |_, t| Ok(t.version.clone()));
|
||||
fields.add_field_method_get("pluginDirPath", |_, t| Ok(t.plugin_dir_path.clone()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use mlua::{IntoLua, Lua, Value};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SdkInfo {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl SdkInfo {
|
||||
pub fn new(name: String, version: String, path: PathBuf) -> Self {
|
||||
Self {
|
||||
name,
|
||||
version,
|
||||
path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoLua for SdkInfo {
|
||||
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
|
||||
let table = lua.create_table()?;
|
||||
table.set("name", self.name)?;
|
||||
table.set("version", self.version)?;
|
||||
table.set("path", self.path.to_string_lossy().to_string())?;
|
||||
Ok(Value::Table(table))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: src/vfox.rs
|
||||
expression: output
|
||||
---
|
||||
[EnvKey { key: "PATH", value: "<INSTALL_DIR>/nodejs/20.0.0/bin" }]
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: src/vfox.rs
|
||||
expression: out
|
||||
snapshot_kind: text
|
||||
---
|
||||
Metadata { name: "nodejs", legacy_filenames: [".node-version", ".nvmrc"], version: "0.3.0", description: Some("Node.js runtime environment."), author: None, license: Some("Apache 2.0"), homepage: Some("https://github.com/version-fox/vfox-nodejs"), hooks: {"available", "env_keys", "parse_legacy_file", "pre_install", "pre_use"} }
|
||||
@@ -0,0 +1,426 @@
|
||||
use itertools::Itertools;
|
||||
use reqwest::Url;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::sync::mpsc;
|
||||
use tempfile::TempDir;
|
||||
use xx::file;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::hooks::available::AvailableVersion;
|
||||
use crate::hooks::env_keys::{EnvKey, EnvKeysContext};
|
||||
use crate::hooks::mise_env::MiseEnvContext;
|
||||
use crate::hooks::mise_path::MisePathContext;
|
||||
use crate::hooks::parse_legacy_file::ParseLegacyFileResponse;
|
||||
use crate::hooks::post_install::PostInstallContext;
|
||||
use crate::hooks::pre_install::PreInstall;
|
||||
use crate::http::CLIENT;
|
||||
use crate::metadata::Metadata;
|
||||
use crate::plugin::Plugin;
|
||||
use crate::registry;
|
||||
use crate::sdk_info::SdkInfo;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Vfox {
|
||||
pub runtime_version: String,
|
||||
pub install_dir: PathBuf,
|
||||
pub plugin_dir: PathBuf,
|
||||
pub cache_dir: PathBuf,
|
||||
pub download_dir: PathBuf,
|
||||
log_tx: Option<mpsc::Sender<String>>,
|
||||
}
|
||||
|
||||
impl Vfox {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn log_subscribe(&mut self) -> mpsc::Receiver<String> {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
self.log_tx = Some(tx);
|
||||
rx
|
||||
}
|
||||
|
||||
fn log_emit(&self, msg: String) {
|
||||
if let Some(tx) = &self.log_tx {
|
||||
let _ = tx.send(msg);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list_available_sdks() -> &'static BTreeMap<String, Url> {
|
||||
registry::list_sdks()
|
||||
}
|
||||
|
||||
pub async fn list_available_versions(&self, sdk: &str) -> Result<Vec<AvailableVersion>> {
|
||||
let sdk = self.get_sdk(sdk)?;
|
||||
sdk.available_async().await
|
||||
}
|
||||
|
||||
pub fn list_installed_versions(&self, sdk: &str) -> Result<Vec<SdkInfo>> {
|
||||
let path = self.install_dir.join(sdk);
|
||||
if !path.exists() {
|
||||
return Ok(Default::default());
|
||||
}
|
||||
let sdk = self.get_sdk(sdk)?;
|
||||
let versions = xx::file::ls(&path)?;
|
||||
versions
|
||||
.into_iter()
|
||||
.filter_map(|p| {
|
||||
p.file_name()
|
||||
.and_then(|f| f.to_str())
|
||||
.map(|s| s.to_string())
|
||||
})
|
||||
.sorted()
|
||||
.map(|version| {
|
||||
let path = path.join(&version);
|
||||
sdk.sdk_info(version, path)
|
||||
})
|
||||
.collect::<Result<_>>()
|
||||
}
|
||||
pub fn list_sdks(&self) -> Result<Vec<Plugin>> {
|
||||
if !self.plugin_dir.exists() {
|
||||
return Ok(Default::default());
|
||||
}
|
||||
let plugins = xx::file::ls(&self.plugin_dir)?;
|
||||
plugins
|
||||
.into_iter()
|
||||
.filter_map(|p| {
|
||||
p.file_name()
|
||||
.and_then(|f| f.to_str())
|
||||
.map(|s| s.to_string())
|
||||
})
|
||||
.sorted()
|
||||
.map(|name| self.get_sdk(&name))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_sdk(&self, name: &str) -> Result<Plugin> {
|
||||
Plugin::from_dir(&self.plugin_dir.join(name))
|
||||
}
|
||||
|
||||
pub fn install_plugin(&self, sdk: &str) -> Result<Plugin> {
|
||||
let plugin_dir = self.plugin_dir.join(sdk);
|
||||
if !plugin_dir.exists() {
|
||||
let url = registry::sdk_url(sdk).ok_or_else(|| format!("Unknown SDK: {sdk}"))?;
|
||||
return self.install_plugin_from_url(url);
|
||||
}
|
||||
Plugin::from_dir(&plugin_dir)
|
||||
}
|
||||
|
||||
pub fn install_plugin_from_url(&self, url: &Url) -> Result<Plugin> {
|
||||
let sdk = url
|
||||
.path_segments()
|
||||
.and_then(|mut s| {
|
||||
let filename = s.next_back().unwrap();
|
||||
filename
|
||||
.strip_prefix("vfox-")
|
||||
.map(|s| s.to_string())
|
||||
.or_else(|| Some(filename.to_string()))
|
||||
})
|
||||
.ok_or("No filename in URL")?;
|
||||
let plugin_dir = self.plugin_dir.join(&sdk);
|
||||
if !plugin_dir.exists() {
|
||||
debug!("Installing plugin {sdk}");
|
||||
xx::git::clone(url.as_ref(), &plugin_dir, &Default::default())?;
|
||||
}
|
||||
Plugin::from_dir(&plugin_dir)
|
||||
}
|
||||
|
||||
pub fn uninstall_plugin(&self, sdk: &str) -> Result<()> {
|
||||
let plugin_dir = self.plugin_dir.join(sdk);
|
||||
if plugin_dir.exists() {
|
||||
file::remove_dir_all(&plugin_dir)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn install<ID: AsRef<Path>>(
|
||||
&self,
|
||||
sdk: &str,
|
||||
version: &str,
|
||||
install_dir: ID,
|
||||
) -> Result<()> {
|
||||
self.install_plugin(sdk)?;
|
||||
let sdk = self.get_sdk(sdk)?;
|
||||
let pre_install = sdk.pre_install(version).await?;
|
||||
let install_dir = install_dir.as_ref();
|
||||
trace!("{pre_install:?}");
|
||||
if let Some(url) = pre_install.url.as_ref().map(|s| Url::from_str(s)) {
|
||||
let file = self.download(&url?, &sdk, version).await?;
|
||||
self.verify(&pre_install, &file)?;
|
||||
self.extract(&file, install_dir)?;
|
||||
}
|
||||
|
||||
if sdk.get_metadata()?.hooks.contains("post_install") {
|
||||
let sdk_info = sdk.sdk_info(version.to_string(), install_dir.to_path_buf())?;
|
||||
sdk.post_install(PostInstallContext {
|
||||
root_path: install_dir.to_path_buf(),
|
||||
runtime_version: self.runtime_version.clone(),
|
||||
sdk_info: BTreeMap::from([(sdk_info.name.clone(), sdk_info)]),
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn uninstall(&self, sdk: &str, version: &str) -> Result<()> {
|
||||
let path = self.install_dir.join(sdk).join(version);
|
||||
file::remove_dir_all(&path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn metadata(&self, sdk: &str) -> Result<Metadata> {
|
||||
self.get_sdk(sdk)?.get_metadata()
|
||||
}
|
||||
|
||||
pub async fn env_keys(&self, sdk: &str, version: &str) -> Result<Vec<EnvKey>> {
|
||||
debug!("Getting env keys for {sdk} version {version}");
|
||||
let sdk = self.get_sdk(sdk)?;
|
||||
let sdk_info = sdk.sdk_info(
|
||||
version.to_string(),
|
||||
self.install_dir.join(&sdk.name).join(version),
|
||||
)?;
|
||||
let ctx = EnvKeysContext {
|
||||
args: vec![],
|
||||
version: version.to_string(),
|
||||
path: sdk_info.path.clone(),
|
||||
sdk_info: BTreeMap::from([(sdk_info.name.clone(), sdk_info.clone())]),
|
||||
main: sdk_info,
|
||||
};
|
||||
sdk.env_keys(ctx).await
|
||||
}
|
||||
|
||||
pub async fn mise_env<T: serde::Serialize>(&self, sdk: &str, opts: T) -> Result<Vec<EnvKey>> {
|
||||
let plugin = self.get_sdk(sdk)?;
|
||||
let ctx = MiseEnvContext {
|
||||
args: vec![],
|
||||
options: opts,
|
||||
};
|
||||
plugin.mise_env(ctx).await
|
||||
}
|
||||
|
||||
pub async fn mise_path<T: serde::Serialize>(&self, sdk: &str, opts: T) -> Result<Vec<String>> {
|
||||
let plugin = self.get_sdk(sdk)?;
|
||||
let ctx = MisePathContext {
|
||||
args: vec![],
|
||||
options: opts,
|
||||
};
|
||||
plugin.mise_path(ctx).await
|
||||
}
|
||||
|
||||
pub async fn parse_legacy_file(
|
||||
&self,
|
||||
sdk: &str,
|
||||
file: &Path,
|
||||
) -> Result<ParseLegacyFileResponse> {
|
||||
let sdk = self.get_sdk(sdk)?;
|
||||
sdk.parse_legacy_file(file).await
|
||||
}
|
||||
|
||||
async fn download(&self, url: &Url, sdk: &Plugin, version: &str) -> Result<PathBuf> {
|
||||
self.log_emit(format!("Downloading {url}"));
|
||||
let filename = url
|
||||
.path_segments()
|
||||
.and_then(|mut s| s.next_back())
|
||||
.ok_or("No filename in URL")?;
|
||||
let path = self
|
||||
.download_dir
|
||||
.join(format!("{sdk}-{version}"))
|
||||
.join(filename);
|
||||
let resp = CLIENT.get(url.clone()).send().await?;
|
||||
resp.error_for_status_ref()?;
|
||||
file::mkdirp(path.parent().unwrap())?;
|
||||
let mut file = tokio::fs::File::create(&path).await?;
|
||||
let bytes = resp.bytes().await?;
|
||||
tokio::io::AsyncWriteExt::write_all(&mut file, &bytes).await?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn verify(&self, pre_install: &PreInstall, file: &Path) -> Result<()> {
|
||||
self.log_emit(format!("Verifying {file:?} checksum"));
|
||||
if let Some(sha256) = &pre_install.sha256 {
|
||||
xx::hash::ensure_checksum_sha256(file, sha256)?;
|
||||
}
|
||||
if let Some(sha512) = &pre_install.sha512 {
|
||||
xx::hash::ensure_checksum_sha512(file, sha512)?;
|
||||
}
|
||||
if let Some(_sha1) = &pre_install.sha1 {
|
||||
unimplemented!("sha1")
|
||||
}
|
||||
if let Some(_md5) = &pre_install.md5 {
|
||||
unimplemented!("md5")
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract(&self, file: &Path, install_dir: &Path) -> Result<()> {
|
||||
self.log_emit(format!("Extracting {file:?} to {install_dir:?}"));
|
||||
let filename = file.file_name().unwrap().to_string_lossy().to_string();
|
||||
let parent = install_dir.parent().unwrap();
|
||||
file::mkdirp(parent)?;
|
||||
let tmp = TempDir::with_prefix_in(&filename, parent)?;
|
||||
file::remove_dir_all(install_dir)?;
|
||||
let move_to_install = || {
|
||||
let subdirs = file::ls(tmp.path())?;
|
||||
if subdirs.len() == 1 && subdirs.first().unwrap().is_dir() {
|
||||
let subdir = subdirs.first().unwrap();
|
||||
file::mv(subdir, install_dir)?;
|
||||
} else {
|
||||
file::mv(tmp.path(), install_dir)?;
|
||||
}
|
||||
Result::Ok(())
|
||||
};
|
||||
if filename.ends_with(".tar.gz") {
|
||||
xx::archive::untar_gz(file, tmp.path())?;
|
||||
move_to_install()?;
|
||||
} else if filename.ends_with(".tar.xz") {
|
||||
xx::archive::untar_xz(file, tmp.path())?;
|
||||
move_to_install()?;
|
||||
} else if filename.ends_with(".tar.bz2") {
|
||||
xx::archive::untar_bz2(file, tmp.path())?;
|
||||
move_to_install()?;
|
||||
} else if filename.ends_with(".zip") {
|
||||
xx::archive::unzip(file, tmp.path())?;
|
||||
move_to_install()?;
|
||||
} else {
|
||||
file::mv(file, install_dir.join(&filename))?;
|
||||
#[cfg(unix)]
|
||||
file::make_executable(install_dir.join(&filename))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Vfox {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
runtime_version: "1.0.0".to_string(),
|
||||
plugin_dir: home().join(".version-fox/plugin"),
|
||||
cache_dir: home().join(".version-fox/cache"),
|
||||
download_dir: home().join(".version-fox/downloads"),
|
||||
install_dir: home().join(".version-fox/installs"),
|
||||
log_tx: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn home() -> PathBuf {
|
||||
homedir::my_home()
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_else(|| PathBuf::from("/"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
impl Vfox {
|
||||
pub fn test() -> Self {
|
||||
Self {
|
||||
runtime_version: "1.0.0".to_string(),
|
||||
plugin_dir: PathBuf::from("plugins"),
|
||||
cache_dir: PathBuf::from("test/cache"),
|
||||
download_dir: PathBuf::from("test/downloads"),
|
||||
install_dir: PathBuf::from("test/installs"),
|
||||
log_tx: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_env_keys() {
|
||||
let vfox = Vfox::test();
|
||||
vfox.install_plugin("nodejs").unwrap();
|
||||
let keys = vfox.env_keys("nodejs", "20.0.0").await.unwrap();
|
||||
let output = format!("{keys:?}").replace(
|
||||
&vfox.install_dir.to_string_lossy().to_string(),
|
||||
"<INSTALL_DIR>",
|
||||
);
|
||||
assert_snapshot!(output);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_install_plugin() {
|
||||
let vfox = Vfox::test();
|
||||
vfox.uninstall_plugin("nodejs").unwrap();
|
||||
assert!(!vfox.plugin_dir.join("nodejs").exists());
|
||||
vfox.install_plugin("nodejs").unwrap();
|
||||
assert!(vfox.plugin_dir.join("nodejs").exists());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_install() {
|
||||
let vfox = Vfox::test();
|
||||
let install_dir = vfox.install_dir.join("nodejs").join("20.0.0");
|
||||
vfox.install("nodejs", "20.0.0", &install_dir)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(vfox
|
||||
.install_dir
|
||||
.join("nodejs")
|
||||
.join("20.0.0")
|
||||
.join("bin")
|
||||
.join("node")
|
||||
.exists());
|
||||
vfox.uninstall_plugin("nodejs").unwrap();
|
||||
assert!(!vfox.plugin_dir.join("nodejs").exists());
|
||||
vfox.uninstall("nodejs", "20.0.0").unwrap();
|
||||
assert!(!vfox.install_dir.join("nodejs").join("20.0.0").exists());
|
||||
file::remove_dir_all(vfox.plugin_dir.join("nodejs")).unwrap();
|
||||
file::remove_dir_all(vfox.install_dir).unwrap();
|
||||
file::remove_dir_all(vfox.download_dir).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_install_cmake() {
|
||||
let vfox = Vfox::test();
|
||||
vfox.install_plugin("cmake").unwrap();
|
||||
let install_dir = vfox.install_dir.join("cmake").join("3.21.0");
|
||||
vfox.install("cmake", "3.21.0", &install_dir).await.unwrap();
|
||||
if cfg!(target_os = "linux") {
|
||||
assert!(vfox
|
||||
.install_dir
|
||||
.join("cmake")
|
||||
.join("3.21.0")
|
||||
.join("bin")
|
||||
.join("cmake")
|
||||
.exists());
|
||||
} else if cfg!(target_os = "macos") {
|
||||
assert!(vfox
|
||||
.install_dir
|
||||
.join("cmake")
|
||||
.join("3.21.0")
|
||||
.join("CMake.app")
|
||||
.join("Contents")
|
||||
.join("bin")
|
||||
.join("cmake")
|
||||
.exists());
|
||||
} else if cfg!(target_os = "windows") {
|
||||
assert!(vfox
|
||||
.install_dir
|
||||
.join("cmake")
|
||||
.join("3.21.0")
|
||||
.join("bin")
|
||||
.join("cmake.exe")
|
||||
.exists());
|
||||
}
|
||||
vfox.uninstall_plugin("cmake").unwrap();
|
||||
assert!(!vfox.plugin_dir.join("cmake").exists());
|
||||
vfox.uninstall("cmake", "3.21.0").unwrap();
|
||||
assert!(!vfox.install_dir.join("cmake").join("3.21.0").exists());
|
||||
file::remove_dir_all(vfox.plugin_dir.join("cmake")).unwrap();
|
||||
file::remove_dir_all(vfox.install_dir).unwrap();
|
||||
file::remove_dir_all(vfox.download_dir).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_metadata() {
|
||||
let vfox = Vfox::test();
|
||||
let metadata = vfox.metadata("nodejs").await.unwrap();
|
||||
let out = format!("{metadata:?}");
|
||||
assert_snapshot!(out);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
v20.0.0
|
||||
Binary file not shown.
Reference in new issue
Block a user