# 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"
    }