feat!: resolve the version of installed apt package

BREAKING the return type of apt install packages now returns an array instead of a single value
This commit is contained in:
Amin Yahyaabadi 2024-09-05 13:29:46 -07:00
parent cb3bbf182c
commit e80fe67578
No known key found for this signature in database
GPG Key ID: F52AF77F636088F0
35 changed files with 224 additions and 231 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,17 +0,0 @@
{
"problemMatcher": [
{
"owner": "gcc",
"pattern": [
{
"regexp": "^(.*?):(\\d+):(\\d*):?\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5
}
]
}
]
}

File diff suppressed because one or more lines are too long

View File

@ -1,17 +0,0 @@
{
"problemMatcher": [
{
"owner": "llvm",
"pattern": [
{
"regexp": "^(.*?):(\\d+):(\\d*):?\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5
}
]
}
]
}

View File

@ -1,18 +0,0 @@
{
"problemMatcher": [
{
"owner": "msvc",
"pattern": [
{
"regexp": "^(?:\\s+\\d+>)?(\\S.*)\\((\\d+),?(\\d+)?(?:,\\d+,\\d+)?\\)\\s*:\\s+(error|warning|info)\\s+(\\w{1,2}\\d+)\\s*:\\s*(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"code": 5,
"message": 6
}
]
}
]
}

View File

@ -1,18 +0,0 @@
{
"problemMatcher": [
{
"owner": "python",
"pattern": [
{
"regexp": "^\\s*File\\s\\\"(.*)\\\",\\sline\\s(\\d+),\\sin\\s(.*)$",
"file": 1,
"line": 2
},
{
"regexp": "^\\s*raise\\s(.*)\\(\\'(.*)\\'\\)$",
"message": 2
}
]
}
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,17 +0,0 @@
{
"problemMatcher": [
{
"owner": "gcc",
"pattern": [
{
"regexp": "^(.*?):(\\d+):(\\d*):?\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5
}
]
}
]
}

File diff suppressed because one or more lines are too long

View File

@ -1,17 +0,0 @@
{
"problemMatcher": [
{
"owner": "llvm",
"pattern": [
{
"regexp": "^(.*?):(\\d+):(\\d*):?\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5
}
]
}
]
}

View File

@ -1,18 +0,0 @@
{
"problemMatcher": [
{
"owner": "msvc",
"pattern": [
{
"regexp": "^(?:\\s+\\d+>)?(\\S.*)\\((\\d+),?(\\d+)?(?:,\\d+,\\d+)?\\)\\s*:\\s+(error|warning|info)\\s+(\\w{1,2}\\d+)\\s*:\\s*(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"code": 5,
"message": 6
}
]
}
]
}

View File

@ -1,18 +0,0 @@
{
"problemMatcher": [
{
"owner": "python",
"pattern": [
{
"regexp": "^\\s*File\\s\\\"(.*)\\\",\\sline\\s(\\d+),\\sin\\s(.*)$",
"file": 1,
"line": 2
},
{
"regexp": "^\\s*raise\\s(.*)\\(\\'(.*)\\'\\)$",
"message": 2
}
]
}
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -32,9 +32,10 @@
"bump": "ncu -u -x numerous,execa,prettier,@types/node,eslint,@types/eslint && pnpm update && pnpx typesync && pnpm run clean",
"clean": "shx rm -rf ./dist ./packages/*/dist ./exe ./.parcel-cache && shx mkdir -p ./dist/legacy ./dist/modern ./dist/modern ",
"copy.json": "shx cp ./src/*/*.json ./dist/legacy/ && shx cp ./dist/legacy/*.json ./dist/modern",
"dev.root.tsc": "tsc --noEmit --pretty --watch",
"dev.vite": "cross-env NODE_ENV=development vite build --watch",
"dev.packages": "turbo dev",
"dev": "run-p dev.packages dev.vite",
"dev": "run-p dev.packages dev.vite dev.root.tsc",
"docs": "shx rm -rf packages/*/README.md && pnpm -r exec readme --path ../../dev/readme/template.md -y && pnpm -r exec ts-readme",
"format": "run-s lint.dprint",
"lint": "turbo lint && run-p --aggregate-output --continue-on-error lint.**",

View File

@ -0,0 +1,57 @@
import { ubuntuVersion as getUbuntuVersion } from "../../../src/utils/env/ubuntu_version.js"
import { aptCacheShow } from "../src/qualify-install.js"
describe("aptCacheShow", () => {
const ubuntuVersionP = getUbuntuVersion()
let ubuntuVersion: number[] | null
beforeAll(async () => {
ubuntuVersion = await ubuntuVersionP
})
it("should find the package if installed", async () => {
if (ubuntuVersion === null) {
return
}
const info = await aptCacheShow("dpkg", undefined)
expect(info).not.toBeUndefined()
expect(info?.name).toBe("dpkg")
})
it("should find the version of the package if installed", async () => {
if (ubuntuVersion === null) {
return
}
const info = await aptCacheShow("gcc", undefined)
expect(info).not.toBeUndefined()
expect(info?.name).toBe("gcc")
if (ubuntuVersion[0] === 22) {
expect(info?.version).toBe("4:11.2.0-1ubuntu1")
}
})
it("should find the version of the package if installed", async () => {
if (ubuntuVersion === null) {
return
}
if (ubuntuVersion[0] !== 22) {
return
}
const info = await aptCacheShow("gcc", "4:11.2.0-1ubuntu1")
expect(info).not.toBeUndefined()
expect(info?.name).toBe("gcc")
expect(info?.version).toBe("4:11.2.0-1ubuntu1")
})
it("will not find the package via imprecise version", async () => {
const info = await aptCacheShow("gcc", "11")
expect(info).toBeUndefined()
})
})

View File

@ -0,0 +1,62 @@
import { ubuntuVersion as getUbuntuVersion } from "../../../src/utils/env/ubuntu_version.js"
import { findAptPackageInfo } from "../src/qualify-install.js"
describe("findAptPackageInfo", () => {
const ubuntuVersionP = getUbuntuVersion()
let ubuntuVersion: number[] | null
beforeAll(async () => {
ubuntuVersion = await ubuntuVersionP
})
it("should find the package if installed", async () => {
if (ubuntuVersion === null) {
return
}
const info = await findAptPackageInfo("dpkg", undefined)
expect(info).not.toBeUndefined()
expect(info?.name).toBe("dpkg")
})
it("should find the version of the package if installed", async () => {
if (ubuntuVersion === null) {
return
}
const info = await findAptPackageInfo("gcc", undefined)
expect(info).not.toBeUndefined()
expect(info?.name).toBe("gcc")
if (ubuntuVersion[0] === 22) {
expect(info?.version).toBe("4:11.2.0-1ubuntu1")
}
})
it("should find the version of the package if installed", async () => {
if (ubuntuVersion === null) {
return
}
if (ubuntuVersion[0] !== 22) {
return
}
const info = await findAptPackageInfo("gcc", "4:11.2.0-1ubuntu1")
expect(info).not.toBeUndefined()
expect(info?.name).toBe("gcc")
expect(info?.version).toBe("4:11.2.0-1ubuntu1")
})
it("will find the package even with imprecise version", async () => {
const info = await findAptPackageInfo("gcc", "11")
expect(info).not.toBeUndefined()
expect(info?.name).toBe("gcc-11")
if (ubuntuVersion![0] === 22) {
expect(info?.version).toBe("4:11.2.0-1ubuntu1")
}
})
})

View File

@ -0,0 +1,4 @@
{
"extends": "../tsconfig.json",
"include": ["**/*.ts"]
}

View File

@ -0,0 +1,2 @@
import jestConfig from "../../jest.config.mjs"
export default jestConfig

View File

@ -1,6 +1,6 @@
{
"name": "setup-apt",
"version": "2.0.0",
"version": "3.0.0",
"description": "Setup apt packages and repositories in Debian/Ubuntu-based distributions",
"repository": "https://github.com/aminya/setup-cpp",
"homepage": "https://github.com/aminya/setup-cpp/tree/master/packages/setup-apt",
@ -14,7 +14,8 @@
"dev": "tsc --watch --pretty",
"lint.tsc": "tsc --noEmit --pretty",
"lint.eslint": "eslint **/*.{ts,tsx,js,jsx,cjs,mjs,json,yaml} --no-error-on-unmatched-pattern --cache --cache-location ./.cache/eslint/ --fix",
"prepublishOnly": "pnpm run build"
"prepublishOnly": "pnpm run build",
"test": "jest --coverage"
},
"dependencies": {
"@types/node": "^12",

View File

@ -17,7 +17,7 @@ export async function initApt(apt: string) {
], apt)
if (toInstall.length !== 0) {
execRootSync(apt, ["install", "-y", "--fix-broken", "-o", aptTimeout, ...toInstall], {
execRootSync(apt, ["install", "-y", "--fix-broken", "-o", aptTimeout, ...toInstall.map(pack => pack.qualified)], {
...defaultExecOptions,
env: getAptEnv(apt),
})

View File

@ -7,13 +7,13 @@ import { addAptRepository } from "./apt-repository.js"
import { aptTimeout } from "./apt-timeout.js"
import { getApt } from "./get-apt.js"
import { initAptMemoized } from "./init-apt.js"
import { filterAndQualifyAptPackages } from "./qualify-install.js"
import { AptPackageInfo, filterAndQualifyAptPackages } from "./qualify-install.js"
import { updateAptReposMemoized } from "./update.js"
/**
* The information about an installation result
*/
export type InstallationInfo = {
export type InstallationInfo = AptPackageInfo & {
/** The install dir of the package (Defaults to `undefined`) */
installDir?: string
/** The bin dir of the package (Defaults to `/usr/bin`) */
@ -67,7 +67,7 @@ const retryErrors = [
])
* ```
*/
export async function installAptPack(packages: AptPackage[], update = false): Promise<InstallationInfo> {
export async function installAptPack(packages: AptPackage[], update = false): Promise<InstallationInfo[]> {
const apt: string = getApt()
for (const { name, version } of packages) {
@ -83,10 +83,11 @@ export async function installAptPack(packages: AptPackage[], update = false): Pr
await addRepositories(apt, packages)
const needToInstall = await filterAndQualifyAptPackages(packages, apt)
const needToInstallArgs = needToInstall.map((pack) => pack.qualified)
if (needToInstall.length === 0) {
info("All packages are already installed")
return { binDir: "/usr/bin/" }
return needToInstall.map((pack) => ({ ...pack, binDir: "/usr/bin/" }))
}
// Initialize apt if needed
@ -97,17 +98,17 @@ export async function installAptPack(packages: AptPackage[], update = false): Pr
await addAptKeys(packages)
// Install
execRootSync(apt, ["install", "--fix-broken", "-y", ...needToInstall], {
execRootSync(apt, ["install", "--fix-broken", "-y", ...needToInstallArgs], {
...defaultExecOptions,
env: getAptEnv(apt),
})
} catch (err) {
if (isExecaError(err)) {
if (retryErrors.some((error) => err.stderr.includes(error))) {
warning(`Failed to install packages ${needToInstall}. Retrying...`)
warning(`Failed to install packages ${needToInstallArgs}. Retrying...`)
execRootSync(
apt,
["install", "--fix-broken", "-y", "-o", aptTimeout, ...needToInstall],
["install", "--fix-broken", "-y", "-o", aptTimeout, ...needToInstallArgs],
{ ...defaultExecOptions, env: getAptEnv(apt) },
)
}
@ -116,7 +117,7 @@ export async function installAptPack(packages: AptPackage[], update = false): Pr
}
}
return { binDir: "/usr/bin/" }
return needToInstall.map((pack) => ({ ...pack, binDir: "/usr/bin/" }))
}
async function addRepositories(apt: string, packages: AptPackage[]) {

View File

@ -1,4 +1,3 @@
import { warning } from "ci-log"
import escapeRegex from "escape-string-regexp"
import { execa } from "execa"
import { getAptEnv } from "./apt-env.js"
@ -7,16 +6,6 @@ import type { AptPackage } from "./install.js"
import { isAptPackInstalled } from "./is-installed.js"
import { updateAptReposMemoized, updatedRepos } from "./update.js"
/**
* The type of apt package to install
*/
export enum AptPackageType {
NameDashVersion = 0,
NameEqualsVersion = 1,
Name = 2,
None = 3,
}
/**
* Filter out the packages that are already installed and qualify the packages into a full package name/version
*/
@ -31,66 +20,110 @@ export async function filterAndQualifyAptPackages(packages: AptPackage[], apt: s
* If the package is already installed, return undefined
*/
export async function qualifiedNeededAptPackage(pack: AptPackage, apt: string = getApt()) {
// Qualify the package into full package name/version
const qualified = await getAptArg(apt, pack.name, pack.version)
const info = await findAptPackageInfo(pack.name, pack.version, apt)
if (info === undefined) {
throw new Error(`Could not find package ${pack.name} ${pack.version}`)
}
// filter out the package that are already installed
return (await isAptPackInstalled(qualified)) ? undefined : qualified
return (await isAptPackInstalled(info.qualified)) ? undefined : info
}
async function aptPackageType(apt: string, name: string, version: string | undefined): Promise<AptPackageType> {
if (version !== undefined && version !== "") {
export type AptPackageInfo = {
name: string
version: string
qualified: string
}
/**
* Get the version of the package from apt-cache show
* If the version is not found, check if apt-cache search can find the version
* If the version is still not found, update the repos and try again
*/
export async function findAptPackageInfo(
name: string,
version: string | undefined,
apt: string = getApt(),
): Promise<AptPackageInfo | undefined> {
const info = await aptCacheShow(name, version, apt)
if (info !== undefined) {
return info
}
if (version !== undefined) {
// check if apt-cache search can find the version
const { stdout } = await execa("apt-cache", [
"search",
"--names-only",
`^${escapeRegex(name)}-${escapeRegex(version)}$`,
], { env: getAptEnv(apt), stdio: "pipe" })
if (stdout.trim() !== "") {
return AptPackageType.NameDashVersion
}
try {
// check if apt-get show can find the version
// eslint-disable-next-line @typescript-eslint/no-shadow
const { stdout } = await execa("apt-cache", ["show", `${name}=${version}`], { env: getAptEnv(apt) })
if (stdout.trim() === "") {
return AptPackageType.NameEqualsVersion
}
} catch {
// ignore
}
}
const stdOutTrim = (stdout as string).trim()
try {
const { stdout: showStdout } = await execa("apt-cache", ["show", name], { env: getAptEnv(apt), stdio: "pipe" })
if (showStdout.trim() !== "") {
return AptPackageType.Name
if (stdOutTrim !== "") {
// get the package name by splitting the first line by " - "
const packages = stdOutTrim.split("\n")
const actualName = packages[0].split(" - ")[0]
// get the version from apt-cache show
return aptCacheShow(actualName, undefined, apt)
}
} catch {
// ignore
}
// If apt-cache fails, update the repos and try again
if (!updatedRepos) {
updateAptReposMemoized(apt)
return aptPackageType(apt, name, version)
return findAptPackageInfo(apt, name, version)
}
return AptPackageType.None
return undefined
}
async function getAptArg(apt: string, name: string, version: string | undefined) {
const package_type = await aptPackageType(apt, name, version)
switch (package_type) {
case AptPackageType.NameDashVersion:
return `${name}-${version}`
case AptPackageType.NameEqualsVersion:
return `${name}=${version}`
case AptPackageType.Name:
if (version !== undefined && version !== "") {
warning(`Could not find package ${name} with version ${version}. Installing the latest version.`)
/**
* Get the info about a package from apt-cache show
*
* @param name The name of the package
* @param version The version of the package
* @param apt The apt command to use
*
* @returns The package info or undefined if the package is not found
*/
export async function aptCacheShow(
name: string,
version: string | undefined,
apt: string = getApt(),
): Promise<AptPackageInfo | undefined> {
try {
const { stdout } = await execa("apt-cache", [
"show",
version !== undefined && version !== ""
? `${name}=${version}`
: name,
], { env: getAptEnv(apt), stdout: "pipe" })
const stdoutTrim = (stdout as string).trim()
if (stdoutTrim === "") {
return undefined
}
// parse the version from the output
// Version: 4:11.2.0-1ubuntu1
const versionMatch = stdoutTrim.match(/^Version: (.*)$/m)
if (versionMatch !== null) {
const actualVersion = versionMatch[1]
return {
name,
version: actualVersion,
qualified: `${name}-${actualVersion}`,
}
return name
default:
throw new Error(`Could not find package ${name} ${version ?? ""}`)
}
return undefined
} catch {
// ignore
}
return undefined
}