mirror of
https://github.com/pesde-pkg/tooling.git
synced 2025-01-10 08:19:10 +00:00
feat(lib): include optional progress bar
Includes an optional progress bar which gets enabled when lib is called using the default convenience `__call` metamethod.
This commit is contained in:
parent
ead60c003e
commit
c69a7417a0
2 changed files with 160 additions and 7 deletions
|
@ -11,6 +11,7 @@ local net = require("@lune/net")
|
||||||
local process = require("@lune/process")
|
local process = require("@lune/process")
|
||||||
local stdio = require("@lune/stdio")
|
local stdio = require("@lune/stdio")
|
||||||
local serde = require("@lune/serde")
|
local serde = require("@lune/serde")
|
||||||
|
local task = require("@lune/task")
|
||||||
|
|
||||||
local pathfs = require("../lune_packages/pathfs")
|
local pathfs = require("../lune_packages/pathfs")
|
||||||
local dirs = require("../lune_packages/dirs")
|
local dirs = require("../lune_packages/dirs")
|
||||||
|
@ -23,6 +24,7 @@ type Option<T> = Option.Option<T>
|
||||||
|
|
||||||
local Github = require("./github")
|
local Github = require("./github")
|
||||||
local PlatformDescriptor = require("./platform/descriptor")
|
local PlatformDescriptor = require("./platform/descriptor")
|
||||||
|
local ProgressBar = require("./utils/progress")
|
||||||
local compression = require("./compression")
|
local compression = require("./compression")
|
||||||
local eq = require("./utils/eq")
|
local eq = require("./utils/eq")
|
||||||
local manifest = require("./manifest")
|
local manifest = require("./manifest")
|
||||||
|
@ -36,6 +38,8 @@ export type ToolId = {
|
||||||
-- TODO: Remove this in a breaking change
|
-- TODO: Remove this in a breaking change
|
||||||
export type GithubReleases = Github.GithubReleases
|
export type GithubReleases = Github.GithubReleases
|
||||||
|
|
||||||
|
local ERROR_PREFIX = `{stdio.color("red")}{stdio.style("bold")}error{stdio.color("reset")}:`
|
||||||
|
local _INFO_PREFIX = `{stdio.color("green")}{stdio.style("bold")}info{stdio.color("reset")}:`
|
||||||
local WARN_PREFIX = `{stdio.color("yellow")}{stdio.style("bold")}warn{stdio.color("reset")}:`
|
local WARN_PREFIX = `{stdio.color("yellow")}{stdio.style("bold")}warn{stdio.color("reset")}:`
|
||||||
|
|
||||||
local function warn(...)
|
local function warn(...)
|
||||||
|
@ -87,6 +91,13 @@ end
|
||||||
local LINK_INSTALL_DIR = (dirs.homeDir() or error("Couldn't get home dir :(")):join(".pesde"):join("bin")
|
local LINK_INSTALL_DIR = (dirs.homeDir() or error("Couldn't get home dir :(")):join(".pesde"):join("bin")
|
||||||
local TOOL_STORAGE_DIR = LINK_INSTALL_DIR:join("tool_storage")
|
local TOOL_STORAGE_DIR = LINK_INSTALL_DIR:join("tool_storage")
|
||||||
|
|
||||||
|
local bar = ProgressBar.new()
|
||||||
|
:withStage("init", "Initializing")
|
||||||
|
:withStage("fetch", "Fetching release")
|
||||||
|
:withStage("locate", "Identifying asset")
|
||||||
|
:withStage("download", "Downloading")
|
||||||
|
:withStage("install", "Installing")
|
||||||
|
|
||||||
function runTool(tool: ToolId | pathfs.Path): number
|
function runTool(tool: ToolId | pathfs.Path): number
|
||||||
-- FIXME: `process.spawn` has a bug where interactive features don't
|
-- FIXME: `process.spawn` has a bug where interactive features don't
|
||||||
-- forward properly
|
-- forward properly
|
||||||
|
@ -102,7 +113,36 @@ function runTool(tool: ToolId | pathfs.Path): number
|
||||||
}).code
|
}).code
|
||||||
end
|
end
|
||||||
|
|
||||||
function installTool(tool: ToolId, installPath: pathfs.Path)
|
local function makeCondInc(yes: boolean?)
|
||||||
|
return function()
|
||||||
|
if yes then
|
||||||
|
bar:nextStage()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function makeCondPause(yes: boolean?)
|
||||||
|
return function()
|
||||||
|
if yes then
|
||||||
|
bar:pause()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function makeCondResume(yes: boolean?)
|
||||||
|
return function()
|
||||||
|
if yes then
|
||||||
|
bar:resume()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function installTool(tool: ToolId, installPath: pathfs.Path, interactive: boolean?)
|
||||||
|
local next = makeCondInc(interactive)
|
||||||
|
local pause = makeCondPause(interactive)
|
||||||
|
local resume = makeCondResume(interactive)
|
||||||
|
next()
|
||||||
|
|
||||||
local toolAlias = toolAliasOrDefault(tool)
|
local toolAlias = toolAliasOrDefault(tool)
|
||||||
local client = Github.new(
|
local client = Github.new(
|
||||||
tool.repo,
|
tool.repo,
|
||||||
|
@ -113,6 +153,7 @@ function installTool(tool: ToolId, installPath: pathfs.Path)
|
||||||
}) :: Option<Github.Config>
|
}) :: Option<Github.Config>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
next()
|
||||||
local releases = client:queueTransactions({ "FetchReleases" })[1]:unwrap() :: GithubReleases
|
local releases = client:queueTransactions({ "FetchReleases" })[1]:unwrap() :: GithubReleases
|
||||||
local assets = tool.version:match({
|
local assets = tool.version:match({
|
||||||
Some = function(version: string)
|
Some = function(version: string)
|
||||||
|
@ -122,6 +163,7 @@ function installTool(tool: ToolId, installPath: pathfs.Path)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
pause()
|
||||||
return error(`No release found for version {version}`)
|
return error(`No release found for version {version}`)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
|
@ -130,6 +172,7 @@ function installTool(tool: ToolId, installPath: pathfs.Path)
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
next()
|
||||||
-- TODO: Use index type fn in solver v2
|
-- TODO: Use index type fn in solver v2
|
||||||
local matchingAsset: {
|
local matchingAsset: {
|
||||||
name: string,
|
name: string,
|
||||||
|
@ -156,8 +199,13 @@ function installTool(tool: ToolId, installPath: pathfs.Path)
|
||||||
|
|
||||||
local binaryPath: pathfs.Path
|
local binaryPath: pathfs.Path
|
||||||
if matchingAsset == nil then
|
if matchingAsset == nil then
|
||||||
|
pause()
|
||||||
warn("Pesde could not find a matching binary for your system")
|
warn("Pesde could not find a matching binary for your system")
|
||||||
warn("Will now attempt to download all binaries and find a matching one")
|
warn("Will now attempt to download all binaries and find a matching one")
|
||||||
|
resume()
|
||||||
|
|
||||||
|
next()
|
||||||
|
|
||||||
for _, asset in assets do
|
for _, asset in assets do
|
||||||
local decompressedPath = downloadAndDecompress(asset)
|
local decompressedPath = downloadAndDecompress(asset)
|
||||||
if decompressedPath:isSome() then
|
if decompressedPath:isSome() then
|
||||||
|
@ -177,10 +225,12 @@ function installTool(tool: ToolId, installPath: pathfs.Path)
|
||||||
local decompressedPath = downloadAndDecompress(matchingAsset):unwrap()
|
local decompressedPath = downloadAndDecompress(matchingAsset):unwrap()
|
||||||
binaryPath = decompressedPath:join(aliasPath)
|
binaryPath = decompressedPath:join(aliasPath)
|
||||||
if not pathfs.isFile(binaryPath) then
|
if not pathfs.isFile(binaryPath) then
|
||||||
|
pause()
|
||||||
error(`No matching binary found in {decompressedPath}`)
|
error(`No matching binary found in {decompressedPath}`)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
next()
|
||||||
-- Maintain multiple versions of a tool, and avoid downloading
|
-- Maintain multiple versions of a tool, and avoid downloading
|
||||||
-- the binary for a version again if it's already there
|
-- the binary for a version again if it's already there
|
||||||
local toolDir = Option.from(installPath:parent()):unwrap()
|
local toolDir = Option.from(installPath:parent()):unwrap()
|
||||||
|
@ -211,6 +261,11 @@ function installTool(tool: ToolId, installPath: pathfs.Path)
|
||||||
-- ...
|
-- ...
|
||||||
]]
|
]]
|
||||||
|
|
||||||
|
-- Stop the progress bar and clean it up from stdout
|
||||||
|
bar:stop()
|
||||||
|
task.cancel(bar.thread :: thread)
|
||||||
|
stdio.write("\x1b[2K\x1b[0G")
|
||||||
|
|
||||||
-- NOTE: This is equivalent to `0o755` or `rwxr-xr-x`
|
-- NOTE: This is equivalent to `0o755` or `rwxr-xr-x`
|
||||||
chmod(installPath, 0b10101010011)
|
chmod(installPath, 0b10101010011)
|
||||||
runTool(installPath)
|
runTool(installPath)
|
||||||
|
@ -218,7 +273,7 @@ end
|
||||||
|
|
||||||
type LibExports = {
|
type LibExports = {
|
||||||
runTool: (pathfs.Path | ToolId) -> number,
|
runTool: (pathfs.Path | ToolId) -> number,
|
||||||
installTool: (ToolId, pathfs.Path) -> never,
|
installTool: (ToolId, pathfs.Path, boolean?) -> never,
|
||||||
}
|
}
|
||||||
type LibExportsImpl = typeof(setmetatable(
|
type LibExportsImpl = typeof(setmetatable(
|
||||||
{} :: LibExports,
|
{} :: LibExports,
|
||||||
|
@ -232,10 +287,6 @@ return setmetatable(
|
||||||
} :: LibExports,
|
} :: LibExports,
|
||||||
{
|
{
|
||||||
__call = function(lib: LibExportsImpl, tool: string, pesdeRoot: string?): number
|
__call = function(lib: LibExportsImpl, tool: string, pesdeRoot: string?): number
|
||||||
-- TODO: Progress bar maybe? :D
|
|
||||||
|
|
||||||
local ERROR_PREFIX = `{stdio.color("red")}{stdio.style("bold")}error{stdio.color("reset")}:`
|
|
||||||
|
|
||||||
local repo, version = string.match(tool, "([^@]+)@?(.*)")
|
local repo, version = string.match(tool, "([^@]+)@?(.*)")
|
||||||
if repo == nil or version == nil then
|
if repo == nil or version == nil then
|
||||||
stdio.ewrite(`{ERROR_PREFIX} Invalid tool provided\n`)
|
stdio.ewrite(`{ERROR_PREFIX} Invalid tool provided\n`)
|
||||||
|
@ -280,6 +331,7 @@ return setmetatable(
|
||||||
return lib.runTool(toolInstallPath)
|
return lib.runTool(toolInstallPath)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
bar:start()
|
||||||
local ok, err = pcall(
|
local ok, err = pcall(
|
||||||
lib.installTool,
|
lib.installTool,
|
||||||
{
|
{
|
||||||
|
@ -287,7 +339,8 @@ return setmetatable(
|
||||||
repo = repo,
|
repo = repo,
|
||||||
version = Option.Some(versionOrDefault),
|
version = Option.Some(versionOrDefault),
|
||||||
} :: ToolId,
|
} :: ToolId,
|
||||||
toolInstallPath
|
toolInstallPath,
|
||||||
|
true
|
||||||
)
|
)
|
||||||
|
|
||||||
if not ok then
|
if not ok then
|
||||||
|
|
100
toolchainlib/src/utils/progress.luau
Normal file
100
toolchainlib/src/utils/progress.luau
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
--> Inspired by Rokit's progress bar: https://github.com/rojo-rbx/rokit/blob/a303faf/src/util/progress.rs
|
||||||
|
|
||||||
|
local task = require("@lune/task")
|
||||||
|
local stdio = require("@lune/stdio")
|
||||||
|
|
||||||
|
local Option = require("../../lune_packages/option")
|
||||||
|
type Option<T> = Option.Option<T>
|
||||||
|
|
||||||
|
-- FORMAT: {SPINNER} {MESSAGE} {BAR} {STAGE}
|
||||||
|
local SPINNERS = { "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" }
|
||||||
|
local BAR_COMPONENT = "▇"
|
||||||
|
local MAX_BAR_LENGTH = 30
|
||||||
|
|
||||||
|
local ProgressBar = {}
|
||||||
|
type ProgressBar = {
|
||||||
|
stages: { { tag: string, message: string } },
|
||||||
|
currentStageIndex: number,
|
||||||
|
finished: boolean,
|
||||||
|
thread: thread?,
|
||||||
|
}
|
||||||
|
export type ProgressBarImpl = typeof(setmetatable({} :: ProgressBar, { __index = ProgressBar }))
|
||||||
|
|
||||||
|
function ProgressBar.new(): ProgressBarImpl
|
||||||
|
return setmetatable(
|
||||||
|
{
|
||||||
|
stages = {},
|
||||||
|
currentStageIndex = 1,
|
||||||
|
finished = false,
|
||||||
|
} :: ProgressBar,
|
||||||
|
{
|
||||||
|
__index = ProgressBar,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ProgressBar.withStage(self: ProgressBarImpl, tag: string, msg: string): ProgressBarImpl
|
||||||
|
table.insert(self.stages, { tag = tag, message = msg })
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function ProgressBar.start(self: ProgressBarImpl): ()
|
||||||
|
local BAR_LENGTH = MAX_BAR_LENGTH // #self.stages
|
||||||
|
local TOTAL_BAR_LENGTH = BAR_LENGTH * #self.stages
|
||||||
|
local BAR = string.rep(BAR_COMPONENT, BAR_LENGTH)
|
||||||
|
local MAX_MESSAGE_LENGTH = 0
|
||||||
|
for _, stage in self.stages do
|
||||||
|
local len = #stage.message
|
||||||
|
if len > MAX_MESSAGE_LENGTH then
|
||||||
|
MAX_MESSAGE_LENGTH = len
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self.thread = task.spawn(function()
|
||||||
|
while not self.finished do
|
||||||
|
if self.currentStageIndex == #self.stages then
|
||||||
|
self:stop()
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, spinner in SPINNERS do
|
||||||
|
local stage = self.stages[self.currentStageIndex]
|
||||||
|
stdio.write(
|
||||||
|
`\x1b[2K\x1b[0G{spinner} {stage.message}{string.rep(" ", MAX_MESSAGE_LENGTH - #stage.message)} [{string.rep(
|
||||||
|
BAR,
|
||||||
|
self.currentStageIndex
|
||||||
|
)}{string.rep(" ", TOTAL_BAR_LENGTH - (BAR_LENGTH * self.currentStageIndex))}] {self.currentStageIndex} / {#self.stages}`
|
||||||
|
)
|
||||||
|
|
||||||
|
task.wait(0.1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
stdio.write("\n")
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ProgressBar.stop(self: ProgressBarImpl): ()
|
||||||
|
self.finished = true
|
||||||
|
end
|
||||||
|
|
||||||
|
function ProgressBar.pause(self: ProgressBarImpl): ()
|
||||||
|
local _ = self.thread and coroutine.yield(self.thread)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ProgressBar.resume(self: ProgressBarImpl): ()
|
||||||
|
local _ = self.thread and coroutine.resume(self.thread)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ProgressBar.nextStage(self: ProgressBarImpl): ()
|
||||||
|
local inc = self.currentStageIndex + 1
|
||||||
|
if inc > #self.stages then
|
||||||
|
-- TODO: Make this a result
|
||||||
|
self.finished = true
|
||||||
|
return error("Out of stage bounds")
|
||||||
|
end
|
||||||
|
|
||||||
|
self.currentStageIndex = inc
|
||||||
|
end
|
||||||
|
|
||||||
|
return ProgressBar
|
Loading…
Reference in a new issue