fix: refactor apt functions into separate files

This commit is contained in:
Amin Yahyaabadi 2024-08-28 15:17:21 -07:00
parent adb1af1e17
commit 0dbc7a0d00
No known key found for this signature in database
GPG Key ID: F52AF77F636088F0
16 changed files with 296 additions and 277 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

@ -0,0 +1,21 @@
/**
* Get the environment variables to use for the apt command
* @param apt The apt command to use
* @private Used internally
*/
export function getAptEnv(apt: string) {
const env: NodeJS.ProcessEnv = { ...process.env, DEBIAN_FRONTEND: "noninteractive" }
if (apt === "nala") {
// if LANG/LC_ALL is not set, enable utf8 otherwise nala fails because of ASCII encoding
if (env.LANG === undefined) {
env.LANG = "C.UTF-8"
}
if (env.LC_ALL === undefined) {
env.LC_ALL = "C.UTF-8"
}
}
return env
}

View File

@ -0,0 +1,23 @@
import { defaultExecOptions, execRootSync } from "admina"
import { getAptEnv } from "./apt-env.js"
import { aptTimeout } from "./apt-timeout.js"
import { getApt } from "./get-apt.js"
import { isAptPackInstalled } from "./is-installed.js"
import { updateAptRepos } from "./update.js"
export async function addAptRepository(repo: string, apt = getApt()) {
await installAddAptRepo(apt)
execRootSync("add-apt-repository", ["-y", "--no-update", repo], { ...defaultExecOptions, env: getAptEnv(apt) })
updateAptRepos(apt)
}
export async function installAddAptRepo(apt: string) {
if (await isAptPackInstalled("software-properties-common")) {
return
}
execRootSync(
apt,
["install", "-y", "--fix-broken", "-o", aptTimeout, "software-properties-common"],
{ ...defaultExecOptions, env: getAptEnv(apt) },
)
}

View File

@ -0,0 +1,7 @@
/**
* The timeout to use for apt commands
* Wait up to 300 seconds if the apt-get lock is held
* @private Used internally
*/
export const aptTimeout = "Dpkg::Lock::Timeout=300"

View File

@ -0,0 +1,22 @@
import which from "which"
/**
* Check if nala is installed
*/
export function hasNala() {
return which.sync("nala", { nothrow: true }) !== null
}
/**
* Get the apt command to use
* If nala is installed, use that, otherwise use apt-get
*/
export function getApt() {
let apt: string
if (hasNala()) {
apt = "nala"
} else {
apt = "apt-get"
}
return apt
}

View File

@ -1,5 +1,9 @@
export * from "./alternatives.js"
export * from "./apt-key.js"
export * from "./apt-repository.js"
export * from "./apt-timeout.js"
export * from "./get-apt.js"
export * from "./init-apt.js"
export * from "./install.js"
export * from "./is-installed.js"
export * from "./update.js"

View File

@ -0,0 +1,24 @@
import { defaultExecOptions, execRootSync } from "admina"
import { getAptEnv } from "./apt-env.js"
import { aptTimeout } from "./apt-timeout.js"
import { filterAndQualifyAptPackages } from "./qualify-install.js"
import { updateAptRepos } from "./update.js"
/** Install gnupg and certificates (usually missing from docker containers) */
export async function initApt(apt: string) {
// Update the repos
updateAptRepos(apt)
const toInstall = await filterAndQualifyAptPackages(apt, [
{ name: "ca-certificates" },
{ name: "gnupg" },
{ name: "apt-utils" },
])
if (toInstall.length !== 0) {
execRootSync(apt, ["install", "-y", "--fix-broken", "-o", aptTimeout, ...toInstall], {
...defaultExecOptions,
env: getAptEnv(apt),
})
}
}

View File

@ -1,10 +1,13 @@
import { defaultExecOptions, execRootSync } from "admina"
import { info, warning } from "ci-log"
import escapeRegex from "escape-string-regexp"
import { type ExecaError, execa } from "execa"
import which from "which"
import type { ExecaError } from "execa"
import { getAptEnv } from "./apt-env.js"
import { type AddAptKeyOptions, addAptKey } from "./apt-key.js"
import { isAptPackInstalled } from "./is-installed.js"
import { addAptRepository } from "./apt-repository.js"
import { aptTimeout } from "./apt-timeout.js"
import { getApt } from "./get-apt.js"
import { initApt } from "./init-apt.js"
import { filterAndQualifyAptPackages } from "./qualify-install.js"
import { updateAptRepos } from "./update.js"
/**
@ -20,16 +23,9 @@ export type InstallationInfo = {
}
/* eslint-disable require-atomic-updates */
let didUpdate: boolean = false
export let didUpdate: boolean = false
let didInit: boolean = false
/**
* The timeout to use for apt commands
* Wait up to 300 seconds if the apt-get lock is held
* @private Used internally
*/
export const aptTimeout = "Dpkg::Lock::Timeout=300"
/**
* The information about an apt package
*/
@ -131,6 +127,11 @@ export async function installAptPack(packages: AptPackage[], update = false): Pr
return { binDir: "/usr/bin/" }
}
async function addRepositories(apt: string, packages: AptPackage[]) {
const allRepositories = [...new Set(packages.flatMap((pack) => pack.repository ?? []))]
await Promise.all(allRepositories.map((repo) => addAptRepository(repo, apt)))
}
async function addAptKeys(packages: AptPackage[]) {
await Promise.all(packages.map(async (pack) => {
if (pack.key !== undefined) {
@ -142,179 +143,3 @@ async function addAptKeys(packages: AptPackage[]) {
function isExecaError(err: unknown): err is ExecaError {
return typeof (err as ExecaError).stderr === "string"
}
/**
* Check if nala is installed
*/
export function hasNala() {
return which.sync("nala", { nothrow: true }) !== null
}
/**
* Get the apt command to use
* If nala is installed, use that, otherwise use apt-get
*/
export function getApt() {
let apt: string
if (hasNala()) {
apt = "nala"
} else {
apt = "apt-get"
}
return apt
}
/**
* Get the environment variables to use for the apt command
* @param apt The apt command to use
* @private Used internally
*/
export function getAptEnv(apt: string) {
const env: NodeJS.ProcessEnv = { ...process.env, DEBIAN_FRONTEND: "noninteractive" }
if (apt === "nala") {
// if LANG/LC_ALL is not set, enable utf8 otherwise nala fails because of ASCII encoding
if (env.LANG === undefined) {
env.LANG = "C.UTF-8"
}
if (env.LC_ALL === undefined) {
env.LC_ALL = "C.UTF-8"
}
}
return env
}
/**
* 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
*/
async function filterAndQualifyAptPackages(apt: string, packages: AptPackage[]) {
return (await Promise.all(packages.map((pack) => qualifiedNeededAptPackage(apt, pack))))
.filter((pack) => pack !== undefined)
}
async function qualifiedNeededAptPackage(apt: string, pack: AptPackage) {
// Qualify the packages into full package name/version
const qualified = await getAptArg(apt, pack.name, pack.version)
// filter out the packages that are already installed
return (await isAptPackInstalled(qualified)) ? undefined : qualified
}
async function addRepositories(apt: string, packages: AptPackage[]) {
const allRepositories = [...new Set(packages.flatMap((pack) => pack.repository ?? []))]
if (allRepositories.length !== 0) {
if (!didInit) {
await initApt(apt)
didInit = true
}
await installAddAptRepo(apt)
for (const repo of allRepositories) {
// eslint-disable-next-line no-await-in-loop
execRootSync("add-apt-repository", ["-y", "--no-update", repo], { ...defaultExecOptions, env: getAptEnv(apt) })
}
updateAptRepos(apt)
didUpdate = true
}
}
async function aptPackageType(apt: string, name: string, version: string | undefined): Promise<AptPackageType> {
if (version !== undefined && 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
}
}
try {
const { stdout: showStdout } = await execa("apt-cache", ["show", name], { env: getAptEnv(apt), stdio: "pipe" })
if (showStdout.trim() !== "") {
return AptPackageType.Name
}
} catch {
// ignore
}
// If apt-cache fails, update the repos and try again
if (!didUpdate) {
updateAptRepos(getApt())
didUpdate = true
return aptPackageType(apt, name, version)
}
return AptPackageType.None
}
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.`)
}
return name
default:
throw new Error(`Could not find package ${name} ${version ?? ""}`)
}
}
async function installAddAptRepo(apt: string) {
if (await isAptPackInstalled("software-properties-common")) {
return
}
execRootSync(
apt,
["install", "-y", "--fix-broken", "-o", aptTimeout, "software-properties-common"],
{ ...defaultExecOptions, env: getAptEnv(apt) },
)
}
/** Install gnupg and certificates (usually missing from docker containers) */
async function initApt(apt: string) {
// Update the repos if needed
if (!didUpdate) {
updateAptRepos(apt)
didUpdate = true
}
const toInstall = await filterAndQualifyAptPackages(apt, [
{ name: "ca-certificates" },
{ name: "gnupg" },
{ name: "apt-utils" },
])
if (toInstall.length !== 0) {
execRootSync(apt, ["install", "-y", "--fix-broken", "-o", aptTimeout, ...toInstall], {
...defaultExecOptions,
env: getAptEnv(apt),
})
}
}

View File

@ -1,5 +1,5 @@
import { execa } from "execa"
import { getAptEnv } from "./install.js"
import { getAptEnv } from "./apt-env.js"
/**
* Check if a package is installed

View File

@ -0,0 +1,91 @@
import { warning } from "ci-log"
import escapeRegex from "escape-string-regexp"
import { execa } from "execa"
import { getAptEnv } from "./apt-env.js"
import { getApt } from "./get-apt.js"
import { type AptPackage, didUpdate } from "./install.js"
import { isAptPackInstalled } from "./is-installed.js"
import { updateAptRepos } 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
*/
export async function filterAndQualifyAptPackages(apt: string, packages: AptPackage[]) {
return (await Promise.all(packages.map((pack) => qualifiedNeededAptPackage(apt, pack))))
.filter((pack) => pack !== undefined)
}
async function qualifiedNeededAptPackage(apt: string, pack: AptPackage) {
// Qualify the packages into full package name/version
const qualified = await getAptArg(apt, pack.name, pack.version)
// filter out the packages that are already installed
return (await isAptPackInstalled(qualified)) ? undefined : qualified
}
async function aptPackageType(apt: string, name: string, version: string | undefined): Promise<AptPackageType> {
if (version !== undefined && 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
}
}
try {
const { stdout: showStdout } = await execa("apt-cache", ["show", name], { env: getAptEnv(apt), stdio: "pipe" })
if (showStdout.trim() !== "") {
return AptPackageType.Name
}
} catch {
// ignore
}
// If apt-cache fails, update the repos and try again
if (!didUpdate) {
updateAptRepos(getApt())
return aptPackageType(apt, name, version)
}
return AptPackageType.None
}
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.`)
}
return name
default:
throw new Error(`Could not find package ${name} ${version ?? ""}`)
}
}

View File

@ -1,5 +1,7 @@
import { defaultExecOptions, execRootSync } from "admina"
import { aptTimeout, getApt, getAptEnv } from "./install.js"
import { getAptEnv } from "./apt-env.js"
import { aptTimeout } from "./apt-timeout.js"
import { getApt } from "./get-apt.js"
/**
* Update the apt repositories