2024-08-16 17:13:47 +08:00
|
|
|
import { defaultExecOptions, execRootSync } from "admina"
|
|
|
|
import { info, warning } from "ci-log"
|
2024-08-29 06:17:21 +08:00
|
|
|
import type { ExecaError } from "execa"
|
|
|
|
import { getAptEnv } from "./apt-env.js"
|
2024-08-29 04:54:48 +08:00
|
|
|
import { type AddAptKeyOptions, addAptKey } from "./apt-key.js"
|
2024-08-29 06:17:21 +08:00
|
|
|
import { addAptRepository } from "./apt-repository.js"
|
|
|
|
import { aptTimeout } from "./apt-timeout.js"
|
|
|
|
import { getApt } from "./get-apt.js"
|
2024-08-29 06:29:31 +08:00
|
|
|
import { initAptMemoized } from "./init-apt.js"
|
2024-08-29 06:17:21 +08:00
|
|
|
import { filterAndQualifyAptPackages } from "./qualify-install.js"
|
2024-08-29 06:29:31 +08:00
|
|
|
import { updateAptReposMemoized } from "./update.js"
|
2024-08-16 17:13:47 +08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The information about an installation result
|
|
|
|
*/
|
|
|
|
export type InstallationInfo = {
|
|
|
|
/** The install dir of the package (Defaults to `undefined`) */
|
|
|
|
installDir?: string
|
|
|
|
/** The bin dir of the package (Defaults to `/usr/bin`) */
|
|
|
|
binDir: string
|
|
|
|
/** The bin path of the package (Defaults to `undefined`) */
|
|
|
|
bin?: string
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The information about an apt package
|
|
|
|
*/
|
|
|
|
export type AptPackage = {
|
|
|
|
/** The name of the package */
|
|
|
|
name: string
|
|
|
|
/** The version of the package (optional) */
|
|
|
|
version?: string
|
2024-08-29 05:23:40 +08:00
|
|
|
/** The repository to add before installing the package (optional) */
|
|
|
|
repository?: string
|
|
|
|
/** The key to add before installing the package (optional) */
|
|
|
|
key?: AddAptKeyOptions
|
2024-08-16 17:13:47 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
const retryErrors = [
|
|
|
|
"E: Could not get lock",
|
|
|
|
"dpkg: error processing archive",
|
|
|
|
"dpkg: error: dpkg status database is locked by another process",
|
|
|
|
]
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Install a package using apt
|
|
|
|
*
|
|
|
|
* @param packages The packages to install (name, and optional info like version and repositories)
|
|
|
|
* @param update Whether to update the package list before installing (Defaults to `false`)
|
2024-08-29 05:12:56 +08:00
|
|
|
*
|
|
|
|
* @returns The installation information
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* ```ts
|
|
|
|
* await installAptPack([{ name: "ca-certificates" }, { name: "gnupg" }])
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* ```ts
|
|
|
|
await installAptPack([
|
|
|
|
{
|
|
|
|
name: "gcc",
|
|
|
|
version,
|
2024-08-29 05:23:40 +08:00
|
|
|
repository: "ppa:ubuntu-toolchain-r/test",
|
|
|
|
key: { key: "1E9377A2BA9EF27F", fileName: "ubuntu-toolchain-r-test.gpg" },
|
2024-08-29 05:12:56 +08:00
|
|
|
},
|
|
|
|
])
|
|
|
|
* ```
|
2024-08-16 17:13:47 +08:00
|
|
|
*/
|
|
|
|
export async function installAptPack(packages: AptPackage[], update = false): Promise<InstallationInfo> {
|
|
|
|
const apt: string = getApt()
|
|
|
|
|
|
|
|
for (const { name, version } of packages) {
|
|
|
|
info(`Installing ${name} ${version ?? ""} via ${apt}`)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update the repos if needed
|
|
|
|
if (update) {
|
2024-08-29 06:29:31 +08:00
|
|
|
updateAptReposMemoized(apt)
|
2024-08-16 17:13:47 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Add the repos if needed
|
|
|
|
await addRepositories(apt, packages)
|
|
|
|
|
|
|
|
const needToInstall = await filterAndQualifyAptPackages(apt, packages)
|
|
|
|
|
|
|
|
if (needToInstall.length === 0) {
|
|
|
|
info("All packages are already installed")
|
|
|
|
return { binDir: "/usr/bin/" }
|
|
|
|
}
|
|
|
|
|
|
|
|
// Initialize apt if needed
|
2024-08-29 06:29:31 +08:00
|
|
|
await initAptMemoized(apt)
|
2024-08-16 17:13:47 +08:00
|
|
|
|
|
|
|
try {
|
2024-08-29 04:54:48 +08:00
|
|
|
// Add the keys if needed
|
|
|
|
await addAptKeys(packages)
|
|
|
|
|
|
|
|
// Install
|
2024-08-16 17:37:02 +08:00
|
|
|
execRootSync(apt, ["install", "--fix-broken", "-y", ...needToInstall], {
|
|
|
|
...defaultExecOptions,
|
|
|
|
env: getAptEnv(apt),
|
|
|
|
})
|
2024-08-16 17:13:47 +08:00
|
|
|
} catch (err) {
|
|
|
|
if (isExecaError(err)) {
|
|
|
|
if (retryErrors.some((error) => err.stderr.includes(error))) {
|
|
|
|
warning(`Failed to install packages ${needToInstall}. Retrying...`)
|
|
|
|
execRootSync(
|
|
|
|
apt,
|
|
|
|
["install", "--fix-broken", "-y", "-o", aptTimeout, ...needToInstall],
|
2024-08-16 17:37:02 +08:00
|
|
|
{ ...defaultExecOptions, env: getAptEnv(apt) },
|
2024-08-16 17:13:47 +08:00
|
|
|
)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return { binDir: "/usr/bin/" }
|
|
|
|
}
|
|
|
|
|
2024-08-29 06:17:21 +08:00
|
|
|
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)))
|
|
|
|
}
|
|
|
|
|
2024-08-29 04:54:48 +08:00
|
|
|
async function addAptKeys(packages: AptPackage[]) {
|
|
|
|
await Promise.all(packages.map(async (pack) => {
|
2024-08-29 05:23:40 +08:00
|
|
|
if (pack.key !== undefined) {
|
|
|
|
await addAptKey(pack.key)
|
2024-08-29 04:54:48 +08:00
|
|
|
}
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
2024-08-16 17:13:47 +08:00
|
|
|
function isExecaError(err: unknown): err is ExecaError {
|
|
|
|
return typeof (err as ExecaError).stderr === "string"
|
|
|
|
}
|