UNPKG

setup-cpp

Version:

Install all the tools required for building and testing C++/C projects.

217 lines (195 loc) 6.37 kB
import { readFile } from "fs/promises" import semverSatisfies from "semver/functions/satisfies.js" import { semverCoercedRangeIfInvalid } from "../setup/version.ts" /** * The list of assets * @key tag The tag of the release * @value assets The names of the assets of the release */ export type Assets = Record<string, string[] | undefined> /** * Load the list of assets from a json file */ export async function loadAssetList(path: string): Promise<Assets> { const data = await readFile(path, "utf-8") return JSON.parse(data) } /** * The options to match the asset */ export type MatchAssetOpts = { /** * The version to match */ version: string /** * The keywords that must be in the asset name. * If the element is a string, the keyword must be in the asset name. * If the element is an array, one of the keywords must be in the asset name. * @default [] */ keywords?: (string | string[])[] /** * Optional keywords that are not required to be in the asset name * but increase the score of the asset if they are present * * if the element is a string, the keyword must be in the asset name * if the element is an array, one of the keywords must be in the asset name * @default [] */ optionalKeywords?: (string | string[])[] /** * Custom version compare function * @param candidate The candidate version * @param coeredVersion The coerced version to compare against * @returns true if the candidate version satisfies the version * * @default semverSatisfies */ versionSatisfies?: (candidate: string, coeredVersion: string) => boolean /** * Custom tag filter and map function * @param tag The tag to filter and map * @returns The mapped tag or undefined if the tag should be * excluded from the search * @default undefined */ filterMapTag?: (tag: string) => string | undefined /** * Custom asset name filter function * @param asset The asset name to filter * @returns true if the asset should be included in the search * @default undefined */ filterName?: (asset: string) => boolean } /** * Match the asset that matches the version and given keywords * @param assets The list of assets * @param opts The options to match the asset * @returns The tag and name of the asset that matches the version and keywords */ export function matchAsset( assets: Assets, opts: MatchAssetOpts, ): { tag: string; name: string } | undefined { // get the list of versions const origTags = Object.keys(assets) // filter/map the tags const versionMap: Map<string, string> = new Map() if (opts.filterMapTag === undefined) { for (const origTag of origTags) { versionMap.set(origTag, origTag) } } else { for (const origTag of origTags) { const mappedTag = opts.filterMapTag(origTag) if (mappedTag !== undefined) { versionMap.set(mappedTag, origTag) } } } if (versionMap.size === 0) { return undefined } // Assume the version is a semver version if a custom version compare function is not given const versionSatisfies: (c: string, v: string) => boolean = opts.versionSatisfies ?? semverSatisfies // If not a valid semver version, coerce it to a semver version range const versionRange = semverCoercedRangeIfInvalid(opts.version) // find the first tag that starts with the version // loop over the versions starting with the latest const candidateTags: string[] = [] for (const [version, origTag] of versionMap.entries()) { if (versionSatisfies(version, versionRange)) { candidateTags.push(origTag) } } if (candidateTags.length === 0) { return undefined } // Loop over the candidate tags and return the first one that has assets for (const candidateTag of candidateTags) { // get the list of assets let assetNames = assets[candidateTag] if (assetNames === undefined) { continue } // filter the assets if (opts.filterName !== undefined) { assetNames = assetNames.filter(opts.filterName) } if (assetNames.length === 0) { continue } // check if this version contains the keywords and optional keywords in the asset name const match = matchAssetName(candidateTag, assetNames, opts) if (match !== undefined) { return match } } return undefined } function matchAssetName(tag: string, assetNames: string[], opts: MatchAssetOpts) { // if no keywords are given, return the first asset if ( (opts.keywords === undefined || opts.keywords.length === 0) && (opts.optionalKeywords === undefined || opts.optionalKeywords.length === 0) ) { return { tag, name: assetNames[0] } } // check if the asset contains all the keywords let candidates: string[] = [] if ( opts.keywords !== undefined && opts.keywords.length !== 0 ) { for (const name of assetNames) { if ( opts.keywords.every((keyword) => { // single keyword if (typeof keyword === "string" && name.includes(keyword)) { return true } // keyword choices return Array.isArray(keyword) && keyword.some((k) => name.includes(k)) }) ) { candidates.push(name) } } } else { candidates = assetNames } if (candidates.length === 0) { return undefined } // prefer the candidates that contain more optional keywords if ( opts.optionalKeywords !== undefined && opts.optionalKeywords.length !== 0 ) { // rate the candidates based on the number of optional keywords they contain const candidateScores = candidates.map((name) => { let score = 0 for (const keyword of opts.optionalKeywords!) { // single keyword if (typeof keyword === "string" && name.includes(keyword)) { score++ } // keyword choices else if (Array.isArray(keyword) && keyword.some((k) => name.includes(k))) { score++ } } return score }) // find the candidate with the highest score const maxScore = Math.max(...candidateScores) const maxIndex = candidateScores.indexOf(maxScore) return { tag, name: candidates[maxIndex] } } // return the first candidate if no optional keywords are given return { tag, name: candidates[0] } }