mirror of https://github.com/aminya/setup-cpp
refactor: refactor and add docs for setup-apt functions
This commit is contained in:
parent
1865b24b57
commit
ad1b1ee820
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
|
@ -19,77 +19,131 @@ npm install --save setup-apt
|
|||
|
||||
<!-- INSERT GENERATED DOCS START -->
|
||||
|
||||
### `InstallationInfo` (type)
|
||||
|
||||
### `aptTimeout` (variable)
|
||||
|
||||
### `AptPackage` (type)
|
||||
|
||||
### `installAptPack` (function)
|
||||
|
||||
A function that installs a package using apt
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- packages (`AptPackage[]`)
|
||||
- update (`boolean`)
|
||||
|
||||
**returns:** Promise<InstallationInfo>
|
||||
|
||||
### `hasNala` (function)
|
||||
|
||||
**returns:** boolean
|
||||
|
||||
### `getApt` (function)
|
||||
|
||||
**returns:** string
|
||||
|
||||
### `addAptKeyViaServer` (function)
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- keys (`string[]`)
|
||||
- name (`string`)
|
||||
- server (`string`)
|
||||
|
||||
**returns:** Promise<string>
|
||||
|
||||
### `addAptKeyViaDownload` (function)
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- name (`string`)
|
||||
- url (`string`)
|
||||
|
||||
**returns:** Promise<string>
|
||||
|
||||
### `updateAptAlternatives` (function)
|
||||
|
||||
Update the alternatives for a package
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- name (`string`)
|
||||
- path (`string`)
|
||||
- rcOptions (`RcOptions`)
|
||||
- priority (`number`)
|
||||
- name (`string`) - The name of the package
|
||||
- path (`string`) - The path to the binary
|
||||
- priority (`number`) - The priority of the alternative (Defaults to `40`)
|
||||
|
||||
**returns:** Promise<void>
|
||||
|
||||
### `addUpdateAlternativesToRc` (function)
|
||||
|
||||
Add the update-alternatives command to the rc file
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- name (`string`) - The name of the package
|
||||
- path (`string`) - The path to the binary
|
||||
- rcOptions (`RcOptions`) - The options for the rc file to add the update-alternatives command to
|
||||
- priority (`number`) - The priority of the alternative (Defaults to `40`)
|
||||
|
||||
**returns:** Promise<void>
|
||||
|
||||
### `isAptPackInstalled` (function)
|
||||
|
||||
Check if a package is installed
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- pack (`string`)
|
||||
- pack (`string`) - The package to check
|
||||
|
||||
**returns:** Promise<boolean>
|
||||
|
||||
### `isAptPackRegexInstalled` (function)
|
||||
|
||||
Check if a package matching a regexp is installed
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- regexp (`string`)
|
||||
- regexp (`string`) - The regexp to check
|
||||
|
||||
**returns:** Promise<boolean>
|
||||
|
||||
### `updateRepos` (function)
|
||||
|
||||
Update the apt repositories
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- apt (`string`) - The apt command to use (optional)
|
||||
|
||||
**returns:** void
|
||||
|
||||
### `InstallationInfo` (type)
|
||||
|
||||
The information about an installation result
|
||||
|
||||
### `aptTimeout` (variable)
|
||||
|
||||
The timeout to use for apt commands
|
||||
Wait up to 300 seconds if the apt-get lock is held
|
||||
|
||||
### `AptPackage` (type)
|
||||
|
||||
The information about an apt package
|
||||
|
||||
### `installAptPack` (function)
|
||||
|
||||
Install a package using apt
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- packages (`AptPackage[]`) - The packages to install (name, and optional info like version and repositories)
|
||||
- update (`boolean`) - Whether to update the package list before installing (Defaults to `false`)
|
||||
|
||||
**returns:** Promise<InstallationInfo>
|
||||
|
||||
### `hasNala` (function)
|
||||
|
||||
Check if nala is installed
|
||||
|
||||
**returns:** boolean
|
||||
|
||||
### `getApt` (function)
|
||||
|
||||
Get the apt command to use
|
||||
If nala is installed, use that, otherwise use apt-get
|
||||
|
||||
**returns:** string
|
||||
|
||||
### `getEnv` (function)
|
||||
|
||||
Get the environment variables to use for the apt command
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- apt (`string`) - The apt command to use
|
||||
|
||||
**returns:** ProcessEnv
|
||||
|
||||
### `addAptKeyViaServer` (function)
|
||||
|
||||
Add an apt key via a keyserver
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- keys (`string[]`) - The keys to add
|
||||
- name (`string`) - The name of the key
|
||||
- server (`string`) - The keyserver to use (Defaults to `keyserver.ubuntu.com`)
|
||||
|
||||
**returns:** Promise<string>
|
||||
|
||||
### `addAptKeyViaDownload` (function)
|
||||
|
||||
Add an apt key via a download
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- name (`string`) - The name of the key
|
||||
- url (`string`) - The URL of the key
|
||||
|
||||
**returns:** Promise<string>
|
||||
|
||||
<!-- INSERT GENERATED DOCS END -->
|
||||
|
||||
## 🤝 Contributing
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import { promises } from "fs"
|
||||
import { execRoot } from "admina"
|
||||
import { GITHUB_ACTIONS } from "ci-info"
|
||||
import { sourceRC } from "os-env"
|
||||
import type { RcOptions } from "os-env/dist/rc-file.js"
|
||||
const { appendFile } = promises
|
||||
|
||||
/**
|
||||
* Update the alternatives for a package
|
||||
* @param name The name of the package
|
||||
* @param path The path to the binary
|
||||
* @param priority The priority of the alternative (Defaults to `40`)
|
||||
*/
|
||||
export async function updateAptAlternatives(name: string, path: string, priority: number = 40) {
|
||||
await execRoot("update-alternatives", ["--install", `/usr/bin/${name}`, name, path, priority.toString()])
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the update-alternatives command to the rc file
|
||||
* @param name The name of the package
|
||||
* @param path The path to the binary
|
||||
* @param rcOptions The options for the rc file to add the update-alternatives command to
|
||||
* @param priority The priority of the alternative (Defaults to `40`)
|
||||
*/
|
||||
export async function addUpdateAlternativesToRc(
|
||||
name: string,
|
||||
path: string,
|
||||
rcOptions: RcOptions,
|
||||
priority: number = 40,
|
||||
) {
|
||||
if (GITHUB_ACTIONS) {
|
||||
await updateAptAlternatives(name, path, priority)
|
||||
} else {
|
||||
await sourceRC(rcOptions)
|
||||
await appendFile(
|
||||
rcOptions.rcPath,
|
||||
`\nif [ $UID -eq 0 ]; then update-alternatives --install /usr/bin/${name} ${name} ${path} ${priority}; fi\n`,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
import { execRoot, execRootSync } from "admina"
|
||||
import { warning } from "ci-log"
|
||||
import { execa } from "execa"
|
||||
import { pathExists } from "path-exists"
|
||||
import { installAptPack } from "./install.js"
|
||||
|
||||
function initGpg() {
|
||||
execRootSync("gpg", ["-k"])
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an apt key via a keyserver
|
||||
* @param keys The keys to add
|
||||
* @param name The name of the key
|
||||
* @param server The keyserver to use (Defaults to `keyserver.ubuntu.com`)
|
||||
* @returns The file name of the key that was added or `undefined` if it failed
|
||||
*/
|
||||
export async function addAptKeyViaServer(keys: string[], name: string, server = "keyserver.ubuntu.com") {
|
||||
try {
|
||||
const fileName = `/etc/apt/trusted.gpg.d/${name}`
|
||||
if (!(await pathExists(fileName))) {
|
||||
initGpg()
|
||||
|
||||
await Promise.all(
|
||||
keys.map(async (key) => {
|
||||
await execRoot("gpg", [
|
||||
"--no-default-keyring",
|
||||
"--keyring",
|
||||
`gnupg-ring:${fileName}`,
|
||||
"--keyserver",
|
||||
server,
|
||||
"--recv-keys",
|
||||
key,
|
||||
])
|
||||
await execRoot("chmod", ["644", fileName])
|
||||
}),
|
||||
)
|
||||
}
|
||||
return fileName
|
||||
} catch (err) {
|
||||
warning(`Failed to add apt key via server ${server}: ${err}`)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an apt key via a download
|
||||
* @param name The name of the key
|
||||
* @param url The URL of the key
|
||||
* @returns The file name of the key that was added
|
||||
*/
|
||||
export async function addAptKeyViaDownload(name: string, url: string) {
|
||||
const fileName = `/etc/apt/trusted.gpg.d/${name}`
|
||||
if (!(await pathExists(fileName))) {
|
||||
initGpg()
|
||||
await installAptPack([{ name: "curl" }, { name: "ca-certificates" }], undefined)
|
||||
await execa("curl", ["-s", url, "-o", `/tmp/${name}`])
|
||||
execRootSync("gpg", ["--no-default-keyring", "--keyring", `gnupg-ring:${fileName}`, "--import", `/tmp/${name}`])
|
||||
execRootSync("chmod", ["644", fileName])
|
||||
}
|
||||
return fileName
|
||||
}
|
|
@ -1,354 +1,5 @@
|
|||
import { promises } from "fs"
|
||||
import { defaultExecOptions, execRoot, execRootSync } from "admina"
|
||||
import { GITHUB_ACTIONS } from "ci-info"
|
||||
import { info, warning } from "ci-log"
|
||||
import escapeRegex from "escape-string-regexp"
|
||||
import { type ExecaError, execa } from "execa"
|
||||
import { sourceRC } from "os-env"
|
||||
import type { RcOptions } from "os-env/dist/rc-file.js"
|
||||
import { pathExists } from "path-exists"
|
||||
import which from "which"
|
||||
const { appendFile } = promises
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/* eslint-disable require-atomic-updates */
|
||||
let didUpdate: boolean = false
|
||||
let didInit: boolean = false
|
||||
|
||||
// wait up to 300 seconds if the apt-get lock is held
|
||||
export const aptTimeout = "Dpkg::Lock::Timeout=300"
|
||||
|
||||
export type AptPackage = {
|
||||
name: string
|
||||
version?: string
|
||||
repositories?: string[]
|
||||
}
|
||||
|
||||
const retryErrors = [
|
||||
"E: Could not get lock",
|
||||
"dpkg: error processing archive",
|
||||
"dpkg: error: dpkg status database is locked by another process",
|
||||
]
|
||||
|
||||
/** A function that installs a package using apt */
|
||||
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) {
|
||||
updateRepos(apt)
|
||||
didUpdate = true
|
||||
}
|
||||
|
||||
// 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
|
||||
if (!didInit) {
|
||||
await initApt(apt)
|
||||
didInit = true
|
||||
}
|
||||
|
||||
// Install
|
||||
try {
|
||||
execRootSync(apt, ["install", "--fix-broken", "-y", ...needToInstall], { ...defaultExecOptions, env: getEnv(apt) })
|
||||
} 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],
|
||||
{ ...defaultExecOptions, env: getEnv(apt) },
|
||||
)
|
||||
}
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
return { binDir: "/usr/bin/" }
|
||||
}
|
||||
|
||||
function isExecaError(err: unknown): err is ExecaError {
|
||||
return typeof (err as ExecaError).stderr === "string"
|
||||
}
|
||||
|
||||
export function hasNala() {
|
||||
return which.sync("nala", { nothrow: true }) !== null
|
||||
}
|
||||
|
||||
export function getApt() {
|
||||
let apt: string
|
||||
if (hasNala()) {
|
||||
apt = "nala"
|
||||
} else {
|
||||
apt = "apt-get"
|
||||
}
|
||||
return apt
|
||||
}
|
||||
|
||||
function getEnv(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
|
||||
}
|
||||
|
||||
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.repositories ?? []))]
|
||||
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: getEnv(apt) })
|
||||
}
|
||||
updateRepos(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: getEnv(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: getEnv(apt) })
|
||||
if (stdout.trim() === "") {
|
||||
return AptPackageType.NameEqualsVersion
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout: showStdout } = await execa("apt-cache", ["show", name], { env: getEnv(apt), stdio: "pipe" })
|
||||
if (showStdout.trim() !== "") {
|
||||
return AptPackageType.Name
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// If apt-cache fails, update the repos and try again
|
||||
if (!didUpdate) {
|
||||
updateRepos(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 ?? ""}`)
|
||||
}
|
||||
}
|
||||
|
||||
function updateRepos(apt: string) {
|
||||
execRootSync(
|
||||
apt,
|
||||
apt !== "nala" ? ["update", "-y", "-o", aptTimeout] : ["update", "-o", aptTimeout],
|
||||
{ ...defaultExecOptions, env: getEnv(apt) },
|
||||
)
|
||||
}
|
||||
|
||||
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: getEnv(apt) },
|
||||
)
|
||||
}
|
||||
|
||||
/** Install gnupg and certificates (usually missing from docker containers) */
|
||||
async function initApt(apt: string) {
|
||||
// Update the repos if needed
|
||||
if (!didUpdate) {
|
||||
updateRepos(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: getEnv(apt),
|
||||
})
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
addAptKeyViaServer(["3B4FE6ACC0B21F32", "40976EAF437D05B5"], "setup-cpp-ubuntu-archive.gpg"),
|
||||
addAptKeyViaServer(["1E9377A2BA9EF27F"], "launchpad-toolchain.gpg"),
|
||||
])
|
||||
}
|
||||
|
||||
function initGpg() {
|
||||
execRootSync("gpg", ["-k"])
|
||||
}
|
||||
|
||||
export async function addAptKeyViaServer(keys: string[], name: string, server = "keyserver.ubuntu.com") {
|
||||
try {
|
||||
const fileName = `/etc/apt/trusted.gpg.d/${name}`
|
||||
if (!(await pathExists(fileName))) {
|
||||
initGpg()
|
||||
|
||||
await Promise.all(
|
||||
keys.map(async (key) => {
|
||||
await execRoot("gpg", [
|
||||
"--no-default-keyring",
|
||||
"--keyring",
|
||||
`gnupg-ring:${fileName}`,
|
||||
"--keyserver",
|
||||
server,
|
||||
"--recv-keys",
|
||||
key,
|
||||
])
|
||||
await execRoot("chmod", ["644", fileName])
|
||||
}),
|
||||
)
|
||||
}
|
||||
return fileName
|
||||
} catch (err) {
|
||||
warning(`Failed to add apt key via server ${server}: ${err}`)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export async function addAptKeyViaDownload(name: string, url: string) {
|
||||
const fileName = `/etc/apt/trusted.gpg.d/${name}`
|
||||
if (!(await pathExists(fileName))) {
|
||||
initGpg()
|
||||
await installAptPack([{ name: "curl" }, { name: "ca-certificates" }], undefined)
|
||||
await execa("curl", ["-s", url, "-o", `/tmp/${name}`])
|
||||
execRootSync("gpg", ["--no-default-keyring", "--keyring", `gnupg-ring:${fileName}`, "--import", `/tmp/${name}`])
|
||||
execRootSync("chmod", ["644", fileName])
|
||||
}
|
||||
return fileName
|
||||
}
|
||||
|
||||
export async function updateAptAlternatives(name: string, path: string, rcOptions: RcOptions, priority: number = 40) {
|
||||
if (GITHUB_ACTIONS) {
|
||||
await execRoot("update-alternatives", ["--install", `/usr/bin/${name}`, name, path, priority.toString()])
|
||||
} else {
|
||||
await sourceRC(rcOptions)
|
||||
await appendFile(
|
||||
rcOptions.rcPath,
|
||||
`\nif [ $UID -eq 0 ]; then update-alternatives --install /usr/bin/${name} ${name} ${path} ${priority}; fi\n`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function isAptPackInstalled(pack: string) {
|
||||
try {
|
||||
// check if a package is installed
|
||||
const { stdout } = await execa("dpkg", ["-s", pack], { env: getEnv("apt-get"), stdio: "pipe" })
|
||||
if (typeof stdout !== "string") {
|
||||
return false
|
||||
}
|
||||
const lines = stdout.split("\n")
|
||||
// check if the output contains a line that starts with "Status: install ok installed"
|
||||
return lines.some((line) => line.startsWith("Status: install ok installed"))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function isAptPackRegexInstalled(regexp: string) {
|
||||
try {
|
||||
// check if a package matching the regexp is installed
|
||||
const { stdout } = await execa("dpkg", ["-l", regexp], { env: getEnv("apt-get"), stdio: "pipe" })
|
||||
if (typeof stdout !== "string") {
|
||||
return false
|
||||
}
|
||||
const lines = stdout.split("\n")
|
||||
// check if the output contains any lines that start with "ii"
|
||||
return lines.some((line) => line.startsWith("ii"))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
export * from "./alternatives.js"
|
||||
export * from "./apt-key.js"
|
||||
export * from "./install.js"
|
||||
export * from "./is-installed.js"
|
||||
export * from "./update.js"
|
||||
|
|
|
@ -0,0 +1,290 @@
|
|||
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 { addAptKeyViaServer } from "./apt-key.js"
|
||||
import { isAptPackInstalled } from "./is-installed.js"
|
||||
import { updateRepos } from "./update.js"
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
/* eslint-disable require-atomic-updates */
|
||||
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
|
||||
*/
|
||||
export type AptPackage = {
|
||||
/** The name of the package */
|
||||
name: string
|
||||
/** The version of the package (optional) */
|
||||
version?: string
|
||||
/** The repositories to add before installing the package (optional) */
|
||||
repositories?: string[]
|
||||
}
|
||||
|
||||
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`)
|
||||
*/
|
||||
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) {
|
||||
updateRepos(apt)
|
||||
didUpdate = true
|
||||
}
|
||||
|
||||
// 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
|
||||
if (!didInit) {
|
||||
await initApt(apt)
|
||||
didInit = true
|
||||
}
|
||||
|
||||
// Install
|
||||
try {
|
||||
execRootSync(apt, ["install", "--fix-broken", "-y", ...needToInstall], { ...defaultExecOptions, env: getEnv(apt) })
|
||||
} 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],
|
||||
{ ...defaultExecOptions, env: getEnv(apt) },
|
||||
)
|
||||
}
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
return { binDir: "/usr/bin/" }
|
||||
}
|
||||
|
||||
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 getEnv(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.repositories ?? []))]
|
||||
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: getEnv(apt) })
|
||||
}
|
||||
updateRepos(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: getEnv(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: getEnv(apt) })
|
||||
if (stdout.trim() === "") {
|
||||
return AptPackageType.NameEqualsVersion
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout: showStdout } = await execa("apt-cache", ["show", name], { env: getEnv(apt), stdio: "pipe" })
|
||||
if (showStdout.trim() !== "") {
|
||||
return AptPackageType.Name
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// If apt-cache fails, update the repos and try again
|
||||
if (!didUpdate) {
|
||||
updateRepos(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: getEnv(apt) },
|
||||
)
|
||||
}
|
||||
|
||||
/** Install gnupg and certificates (usually missing from docker containers) */
|
||||
async function initApt(apt: string) {
|
||||
// Update the repos if needed
|
||||
if (!didUpdate) {
|
||||
updateRepos(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: getEnv(apt),
|
||||
})
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
addAptKeyViaServer(["3B4FE6ACC0B21F32", "40976EAF437D05B5"], "setup-cpp-ubuntu-archive.gpg"),
|
||||
addAptKeyViaServer(["1E9377A2BA9EF27F"], "launchpad-toolchain.gpg"),
|
||||
])
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import { execa } from "execa"
|
||||
import { getEnv } from "./install.js"
|
||||
|
||||
/**
|
||||
* Check if a package is installed
|
||||
* @param pack The package to check
|
||||
* @returns `true` if the package is installed, `false` otherwise
|
||||
*/
|
||||
export async function isAptPackInstalled(pack: string) {
|
||||
try {
|
||||
// check if a package is installed
|
||||
const { stdout } = await execa("dpkg", ["-s", pack], { env: getEnv("apt-get"), stdio: "pipe" })
|
||||
if (typeof stdout !== "string") {
|
||||
return false
|
||||
}
|
||||
const lines = stdout.split("\n")
|
||||
// check if the output contains a line that starts with "Status: install ok installed"
|
||||
return lines.some((line) => line.startsWith("Status: install ok installed"))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a package matching a regexp is installed
|
||||
* @param regexp The regexp to check
|
||||
* @returns `true` if a package is installed, `false` otherwise
|
||||
*/
|
||||
export async function isAptPackRegexInstalled(regexp: string) {
|
||||
try {
|
||||
// check if a package matching the regexp is installed
|
||||
const { stdout } = await execa("dpkg", ["-l", regexp], { env: getEnv("apt-get"), stdio: "pipe" })
|
||||
if (typeof stdout !== "string") {
|
||||
return false
|
||||
}
|
||||
const lines = stdout.split("\n")
|
||||
// check if the output contains any lines that start with "ii"
|
||||
return lines.some((line) => line.startsWith("ii"))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { defaultExecOptions, execRootSync } from "admina"
|
||||
import { aptTimeout, getApt, getEnv } from "./install.js"
|
||||
|
||||
/**
|
||||
* Update the apt repositories
|
||||
* @param apt The apt command to use (optional)
|
||||
*/
|
||||
export function updateRepos(apt: string = getApt()) {
|
||||
execRootSync(
|
||||
apt,
|
||||
apt !== "nala" ? ["update", "-y", "-o", aptTimeout] : ["update", "-o", aptTimeout],
|
||||
{ ...defaultExecOptions, env: getEnv(apt) },
|
||||
)
|
||||
}
|
|
@ -33,7 +33,7 @@ Replaces a tilde with the user's home directory
|
|||
**returns:** string
|
||||
|
||||
```tsx
|
||||
UntildifyUser('~/foo'); // /home/user/foo
|
||||
UntildifyUser("~/foo") // /home/user/foo
|
||||
```
|
||||
|
||||
<!-- INSERT GENERATED DOCS END -->
|
||||
|
|
|
@ -7,7 +7,7 @@ import { pathExists } from "path-exists"
|
|||
import { addExeExt, join } from "patha"
|
||||
import semverCoerce from "semver/functions/coerce"
|
||||
import semverMajor from "semver/functions/major"
|
||||
import { installAptPack, updateAptAlternatives } from "setup-apt"
|
||||
import { addUpdateAlternativesToRc, installAptPack } from "setup-apt"
|
||||
import { rcOptions } from "../cli-options.js"
|
||||
import { setupMacOSSDK } from "../macos-sdk/macos-sdk.js"
|
||||
import { hasDnf } from "../utils/env/hasDnf.js"
|
||||
|
@ -231,10 +231,10 @@ async function activateGcc(version: string, binDir: string, priority: number = 4
|
|||
|
||||
if (isUbuntu()) {
|
||||
promises.push(
|
||||
updateAptAlternatives("cc", `${binDir}/gcc-${majorVersion}`, rcOptions, priority),
|
||||
updateAptAlternatives("cxx", `${binDir}/g++-${majorVersion}`, rcOptions, priority),
|
||||
updateAptAlternatives("gcc", `${binDir}/gcc-${majorVersion}`, rcOptions, priority),
|
||||
updateAptAlternatives("g++", `${binDir}/g++-${majorVersion}`, rcOptions, priority),
|
||||
addUpdateAlternativesToRc("cc", `${binDir}/gcc-${majorVersion}`, rcOptions, priority),
|
||||
addUpdateAlternativesToRc("cxx", `${binDir}/g++-${majorVersion}`, rcOptions, priority),
|
||||
addUpdateAlternativesToRc("gcc", `${binDir}/gcc-${majorVersion}`, rcOptions, priority),
|
||||
addUpdateAlternativesToRc("g++", `${binDir}/g++-${majorVersion}`, rcOptions, priority),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
@ -245,10 +245,10 @@ async function activateGcc(version: string, binDir: string, priority: number = 4
|
|||
|
||||
if (isUbuntu()) {
|
||||
promises.push(
|
||||
updateAptAlternatives("cc", `${binDir}/gcc-${version}`, rcOptions, priority),
|
||||
updateAptAlternatives("cxx", `${binDir}/g++-${version}`, rcOptions, priority),
|
||||
updateAptAlternatives("gcc", `${binDir}/gcc-${version}`, rcOptions, priority),
|
||||
updateAptAlternatives("g++", `${binDir}/g++-${version}`, rcOptions, priority),
|
||||
addUpdateAlternativesToRc("cc", `${binDir}/gcc-${version}`, rcOptions, priority),
|
||||
addUpdateAlternativesToRc("cxx", `${binDir}/g++-${version}`, rcOptions, priority),
|
||||
addUpdateAlternativesToRc("gcc", `${binDir}/gcc-${version}`, rcOptions, priority),
|
||||
addUpdateAlternativesToRc("g++", `${binDir}/g++-${version}`, rcOptions, priority),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import memoize from "micro-memoize"
|
|||
import { addEnv } from "os-env"
|
||||
import { pathExists } from "path-exists"
|
||||
import { addExeExt, join } from "patha"
|
||||
import { installAptPack, updateAptAlternatives } from "setup-apt"
|
||||
import { addUpdateAlternativesToRc, installAptPack } from "setup-apt"
|
||||
import { rcOptions } from "../cli-options.js"
|
||||
import { setupGcc } from "../gcc/gcc.js"
|
||||
import { setupMacOSSDK } from "../macos-sdk/macos-sdk.js"
|
||||
|
@ -131,13 +131,13 @@ export async function activateLLVM(directory: string) {
|
|||
if (isUbuntu()) {
|
||||
const priority = 60
|
||||
actPromises.push(
|
||||
updateAptAlternatives("cc", `${directory}/bin/clang`, rcOptions, priority),
|
||||
updateAptAlternatives("cxx", `${directory}/bin/clang++`, rcOptions, priority),
|
||||
updateAptAlternatives("clang", `${directory}/bin/clang`, rcOptions),
|
||||
updateAptAlternatives("clang++", `${directory}/bin/clang++`, rcOptions),
|
||||
updateAptAlternatives("lld", `${directory}/bin/lld`, rcOptions),
|
||||
updateAptAlternatives("ld.lld", `${directory}/bin/ld.lld`, rcOptions),
|
||||
updateAptAlternatives("llvm-ar", `${directory}/bin/llvm-ar`, rcOptions),
|
||||
addUpdateAlternativesToRc("cc", `${directory}/bin/clang`, rcOptions, priority),
|
||||
addUpdateAlternativesToRc("cxx", `${directory}/bin/clang++`, rcOptions, priority),
|
||||
addUpdateAlternativesToRc("clang", `${directory}/bin/clang`, rcOptions),
|
||||
addUpdateAlternativesToRc("clang++", `${directory}/bin/clang++`, rcOptions),
|
||||
addUpdateAlternativesToRc("lld", `${directory}/bin/lld`, rcOptions),
|
||||
addUpdateAlternativesToRc("ld.lld", `${directory}/bin/ld.lld`, rcOptions),
|
||||
addUpdateAlternativesToRc("llvm-ar", `${directory}/bin/llvm-ar`, rcOptions),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue