All files install.ts

0% Statements 0/30
0% Branches 0/11
0% Functions 0/8
0% Lines 0/27

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138                                                                                                                                                                                                                                                                                   
import { defaultExecOptions, execRootSync } from "admina"
import { info, warning } from "ci-log"
import type { ExecaError } from "execa"
import { getAptEnv } from "./apt-env.js"
import { type AddAptKeyOptions, addAptKey } from "./apt-key.js"
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 { updateAptReposMemoized } 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
}
 
/**
 * 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 repository to add before installing the package (optional) */
  repository?: string
  /** The key to add before installing the package (optional) */
  key?: AddAptKeyOptions
}
 
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`)
 *
 * @returns The installation information
 *
 * @example
 * ```ts
 * await installAptPack([{ name: "ca-certificates" }, { name: "gnupg" }])
 * ```
 *
 * @example
 * ```ts
  await installAptPack([
    {
      name: "gcc",
      version,
      repository: "ppa:ubuntu-toolchain-r/test",
      key: { key: "1E9377A2BA9EF27F", fileName: "ubuntu-toolchain-r-test.gpg" },
    },
  ])
 * ```
 */
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
  Iif (update) {
    updateAptReposMemoized(apt)
  }
 
  // Add the repos if needed
  await addRepositories(apt, packages)
 
  const needToInstall = await filterAndQualifyAptPackages(packages, apt)
 
  Iif (needToInstall.length === 0) {
    info("All packages are already installed")
    return { binDir: "/usr/bin/" }
  }
 
  // Initialize apt if needed
  await initAptMemoized(apt)
 
  try {
    // Add the keys if needed
    await addAptKeys(packages)
 
    // Install
    execRootSync(apt, ["install", "--fix-broken", "-y", ...needToInstall], {
      ...defaultExecOptions,
      env: getAptEnv(apt),
    })
  } catch (err) {
    if (isExecaError(err)) {
      Iif (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: getAptEnv(apt) },
        )
      }
    } else {
      throw err
    }
  }
 
  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) => {
    Iif (pack.key !== undefined) {
      await addAptKey(pack.key)
    }
  }))
}
 
function isExecaError(err: unknown): err is ExecaError {
  return typeof (err as ExecaError).stderr === "string"
}