From 976aa4fbee8a070f362cab6f6ec84e9251a90cf9 Mon Sep 17 00:00:00 2001 From: William Boman Date: Fri, 8 Jul 2022 18:34:38 +0200 Subject: refactor: add mason-schemas and mason-core modules (#29) * refactor: add mason-schemas and move generated filetype map to mason-lspconfig * refactor: add mason-core module --- lua/mason-core/managers/github/client.lua | 117 ++++++++++++++++++++ lua/mason-core/managers/github/init.lua | 171 ++++++++++++++++++++++++++++++ 2 files changed, 288 insertions(+) create mode 100644 lua/mason-core/managers/github/client.lua create mode 100644 lua/mason-core/managers/github/init.lua (limited to 'lua/mason-core/managers/github') diff --git a/lua/mason-core/managers/github/client.lua b/lua/mason-core/managers/github/client.lua new file mode 100644 index 00000000..1bcede7a --- /dev/null +++ b/lua/mason-core/managers/github/client.lua @@ -0,0 +1,117 @@ +local _ = require "mason-core.functional" +local log = require "mason-core.log" +local fetch = require "mason-core.fetch" +local spawn = require "mason-core.spawn" + +local M = {} + +---@alias GitHubReleaseAsset {url: string, id: integer, name: string, browser_download_url: string, created_at: string, updated_at: string, size: integer, download_count: integer} +---@alias GitHubRelease {tag_name: string, prerelease: boolean, draft: boolean, assets:GitHubReleaseAsset[]} +---@alias GitHubTag {name: string} + +---@param path string +---@return Result @JSON decoded response. +local function api_call(path) + return spawn + .gh({ "api", path }) + :map(_.prop "stdout") + :recover_catching(function() + return fetch(("https://api.github.com/%s"):format(path)):get_or_throw() + end) + :map_catching(vim.json.decode) +end + +---@async +---@param repo string @The GitHub repo ("username/repo"). +---@return Result @of GitHubRelease[] +function M.fetch_releases(repo) + log.fmt_trace("Fetching GitHub releases for repo=%s", repo) + local path = ("repos/%s/releases"):format(repo) + return api_call(path):map_err(function() + return ("Failed to fetch releases for GitHub repository %s."):format(repo) + end) +end + +---@async +---@param repo string @The GitHub repo ("username/repo"). +---@param tag_name string @The tag_name of the release to fetch. +function M.fetch_release(repo, tag_name) + log.fmt_trace("Fetching GitHub release for repo=%s, tag_name=%s", repo, tag_name) + local path = ("repos/%s/releases/tags/%s"):format(repo, tag_name) + return api_call(path):map_err(function() + return ("Failed to fetch release %q for GitHub repository %s."):format(tag_name, repo) + end) +end + +---@param opts {include_prerelease: boolean, tag_name_pattern: string} +function M.release_predicate(opts) + local is_not_draft = _.prop_eq("draft", false) + local is_not_prerelease = _.prop_eq("prerelease", false) + local tag_name_matches = _.prop_satisfies(_.matches(opts.tag_name_pattern), "tag_name") + + return _.all_pass { + _.if_else(_.always(opts.include_prerelease), _.T, is_not_prerelease), + _.if_else(_.always(opts.tag_name_pattern), tag_name_matches, _.T), + is_not_draft, + } +end + +---@alias FetchLatestGithubReleaseOpts {tag_name_pattern:string|nil, include_prerelease: boolean} + +---@async +---@param repo string @The GitHub repo ("username/repo"). +---@param opts FetchLatestGithubReleaseOpts|nil +---@return Result @of GitHubRelease +function M.fetch_latest_release(repo, opts) + opts = opts or { + tag_name_pattern = nil, + include_prerelease = false, + } + return M.fetch_releases(repo):map_catching( + ---@param releases GitHubRelease[] + function(releases) + local is_stable_release = M.release_predicate(opts) + ---@type GitHubRelease|nil + local latest_release = _.find_first(is_stable_release, releases) + + if not latest_release then + log.fmt_info("Failed to find latest release. repo=%s, opts=%s", repo, opts) + error "Failed to find latest release." + end + + log.fmt_debug("Resolved latest version repo=%s, tag_name=%s", repo, latest_release.tag_name) + return latest_release + end + ) +end + +---@async +---@param repo string @The GitHub repo ("username/repo"). +---@return Result @of GitHubTag[] +function M.fetch_tags(repo) + local path = ("repos/%s/tags"):format(repo) + return api_call(path):map_err(function() + return ("Failed to fetch tags for GitHub repository %s."):format(repo) + end) +end + +---@async +---@param repo string @The GitHub repo ("username/repo"). +---@return Result @Result - The latest tag name. +function M.fetch_latest_tag(repo) + -- https://github.com/williamboman/vercel-github-api-latest-tag-proxy + return fetch(("https://latest-github-tag.redwill.se/api/latest-tag?repo=%s"):format(repo)) + :map_catching(vim.json.decode) + :map(_.prop "tag") +end + +---@alias GitHubRateLimit {limit: integer, remaining: integer, reset: integer, used: integer} +---@alias GitHubRateLimitResponse {resources: { core: GitHubRateLimit }} + +---@async +--@return Result @of GitHubRateLimitResponse +function M.fetch_rate_limit() + return api_call "rate_limit" +end + +return M diff --git a/lua/mason-core/managers/github/init.lua b/lua/mason-core/managers/github/init.lua new file mode 100644 index 00000000..55f3600f --- /dev/null +++ b/lua/mason-core/managers/github/init.lua @@ -0,0 +1,171 @@ +local installer = require "mason-core.installer" +local std = require "mason-core.managers.std" +local client = require "mason-core.managers.github.client" +local platform = require "mason-core.platform" +local Result = require "mason-core.result" +local _ = require "mason-core.functional" +local settings = require "mason.settings" + +local M = {} + +---@param repo string +---@param asset_file string +---@param release string +local function with_release_file_receipt(repo, asset_file, release) + return function() + local ctx = installer.context() + ctx.receipt:with_primary_source { + type = "github_release_file", + repo = repo, + file = asset_file, + release = release, + } + end +end + +---@param repo string +---@param tag string +local function with_tag_receipt(repo, tag) + return function() + local ctx = installer.context() + ctx.receipt:with_primary_source { + type = "github_tag", + repo = repo, + tag = tag, + } + end +end + +---@async +---@param opts {repo: string, version: Optional|nil, asset_file: string|fun(release: string):string} +function M.release_file(opts) + local ctx = installer.context() + local release = _.coalesce(opts.version, ctx.requested_version):or_else_get(function() + return client + .fetch_latest_release(opts.repo) + :map(_.prop "tag_name") + :get_or_throw "Failed to fetch latest release from GitHub API. Refer to :h mason-errors-github-api for more information." + end) + ---@type string + local asset_file + if type(opts.asset_file) == "function" then + asset_file = opts.asset_file(release) + else + asset_file = opts.asset_file + end + if not asset_file then + error( + ( + "Could not find which release file to download.\nMost likely the current operating system, architecture (%s), or libc (%s) is not supported." + ):format(platform.arch, platform.get_libc()), + 0 + ) + end + local download_url = settings.current.github.download_url_template:format(opts.repo, release, asset_file) + return { + release = release, + download_url = download_url, + asset_file = asset_file, + with_receipt = with_release_file_receipt(opts.repo, download_url, release), + } +end + +---@async +---@param opts {repo: string, version: Optional|nil} +function M.tag(opts) + local ctx = installer.context() + local tag = _.coalesce(opts.version, ctx.requested_version):or_else_get(function() + return client.fetch_latest_tag(opts.repo):get_or_throw "Failed to fetch latest tag from GitHub API." + end) + + return { + tag = tag, + with_receipt = with_tag_receipt(opts.repo, tag), + } +end + +---@param filename string +---@param processor async fun(opts: table) +local function release_file_processor(filename, processor) + ---@async + ---@param opts {repo: string, asset_file: string|fun(release: string):string} + return function(opts) + local release_file_source = M.release_file(opts) + std.download_file(release_file_source.download_url, filename) + processor(opts) + return release_file_source + end +end + +M.unzip_release_file = release_file_processor("archive.zip", function() + std.unzip("archive.zip", ".") +end) + +M.untarxz_release_file = release_file_processor("archive.tar.xz", function(opts) + std.untarxz("archive.tar.xz", { strip_components = opts.strip_components }) +end) + +M.untargz_release_file = release_file_processor("archive.tar.gz", function(opts) + std.untar("archive.tar.gz", { strip_components = opts.strip_components }) +end) + +---@async +---@param opts {repo: string, out_file:string, asset_file: string|fun(release: string):string} +function M.download_release_file(opts) + local release_file_source = M.release_file(opts) + std.download_file(release_file_source.download_url, assert(opts.out_file, "out_file is required")) + return release_file_source +end + +---@async +---@param opts {repo: string, out_file:string, asset_file: string|fun(release: string):string} +function M.gunzip_release_file(opts) + local release_file_source = M.release_file(opts) + local gzipped_file = ("%s.gz"):format(assert(opts.out_file, "out_file must be specified")) + std.download_file(release_file_source.download_url, gzipped_file) + std.gunzip(gzipped_file) + return release_file_source +end + +---@async +---@param receipt InstallReceipt +function M.check_outdated_primary_package_release(receipt) + local source = receipt.primary_source + if source.type ~= "github_release" and source.type ~= "github_release_file" then + return Result.failure "Receipt does not have a primary source of type (github_release|github_release_file)." + end + return client.fetch_latest_release(source.repo, { tag_name_pattern = source.tag_name_pattern }):map_catching( + ---@param latest_release GitHubRelease + function(latest_release) + if source.release ~= latest_release.tag_name then + return { + name = source.repo, + current_version = source.release, + latest_version = latest_release.tag_name, + } + end + error "Primary package is not outdated." + end + ) +end + +---@async +---@param receipt InstallReceipt +function M.check_outdated_primary_package_tag(receipt) + local source = receipt.primary_source + if source.type ~= "github_tag" then + return Result.failure "Receipt does not have a primary source of type github_tag." + end + return client.fetch_latest_tag(source.repo):map_catching(function(latest_tag) + if source.tag ~= latest_tag then + return { + name = source.repo, + current_version = source.tag, + latest_version = latest_tag, + } + end + error "Primary package is not outdated." + end) +end + +return M -- cgit v1.2.3-70-g09d2