2024-04-16 00:23:29 +02:00
import * as actionsCore from "@actions/core" ;
2024-04-11 17:58:56 +02:00
import * as github from "@actions/github" ;
2024-04-16 00:23:29 +02:00
import * as actionsExec from "@actions/exec" ;
2024-04-11 17:58:56 +02:00
import { access , writeFile , readFile } from "node:fs/promises" ;
import { join } from "node:path" ;
import fs from "node:fs" ;
import { userInfo } from "node:os" ;
import stringArgv from "string-argv" ;
import * as path from "path" ;
2024-04-16 00:23:29 +02:00
import { IdsToolbox , inputs , platform } from "detsys-ts" ;
2024-04-11 17:58:56 +02:00
import { randomUUID } from "node:crypto" ;
class NixInstallerAction {
constructor ( ) {
this . idslib = new IdsToolbox ( {
name : "nix-installer" ,
fetchStyle : "nix-style" ,
legacySourcePrefix : "nix-installer" ,
} ) ;
2024-04-16 00:23:29 +02:00
this . platform = platform . getNixPlatform ( platform . getArchOs ( ) ) ;
this . nixPackageUrl = inputs . getStringOrNull ( "nix-package-url" ) ;
2024-04-16 00:09:42 +02:00
this . backtrace = inputs . getStringOrNull ( "backtrace" ) ;
2024-04-16 00:23:29 +02:00
this . extraArgs = inputs . getStringOrNull ( "extra-args" ) ;
this . extraConf = inputs . getMultilineStringOrNull ( "extra-conf" ) ;
2024-04-16 00:09:42 +02:00
this . flakehub = inputs . getBool ( "flakehub" ) ;
this . kvm = inputs . getBool ( "kvm" ) ;
2024-04-16 00:23:29 +02:00
this . forceDockerShim = inputs . getBool ( "force-docker-shim" ) ;
this . githubToken = inputs . getStringOrNull ( "github-token" ) ;
this . githubServerUrl = inputs . getStringOrNull ( "github-server-url" ) ;
2024-04-16 00:09:42 +02:00
this . init = inputs . getStringOrNull ( "init" ) ;
2024-04-16 00:23:29 +02:00
this . localRoot = inputs . getStringOrNull ( "local-root" ) ;
this . logDirectives = inputs . getStringOrNull ( "log-directives" ) ;
2024-04-16 00:09:42 +02:00
this . logger = inputs . getStringOrNull ( "logger" ) ;
2024-04-16 00:23:29 +02:00
this . sslCertFile = inputs . getStringOrNull ( "ssl-cert-file" ) ;
2024-04-16 00:09:42 +02:00
this . proxy = inputs . getStringOrNull ( "proxy" ) ;
2024-04-16 00:23:29 +02:00
this . macCaseSensitive = inputs . getStringOrNull ( "mac-case-sensitive" ) ;
this . macEncrypt = inputs . getStringOrNull ( "mac-encrypt" ) ;
this . macRootDisk = inputs . getStringOrNull ( "mac-root-disk" ) ;
this . macVolumeLabel = inputs . getStringOrNull ( "mac-volume-label" ) ;
this . modifyProfile = inputs . getBool ( "modify-profile" ) ;
this . nixBuildGroupId = inputs . getNumberOrNull ( "nix-build-group-id" ) ;
this . nixBuildGroupName = inputs . getStringOrNull ( "nix-build-group-name" ) ;
this . nixBuildUserBase = inputs . getNumberOrNull ( "nix-build-user-base" ) ;
this . nixBuildUserCount = inputs . getNumberOrNull ( "nix-build-user-count" ) ;
this . nixBuildUserPrefix = inputs . getStringOrNull ( "nix-build-user-prefix" ) ;
2024-04-16 00:09:42 +02:00
this . planner = inputs . getStringOrNull ( "planner" ) ;
this . reinstall = inputs . getBool ( "reinstall" ) ;
2024-04-16 00:23:29 +02:00
this . startDaemon = inputs . getBool ( "start-daemon" ) ;
this . trustRunnerUser = inputs . getBool ( "trust-runner-user" ) ;
2024-04-11 17:58:56 +02:00
}
async detectAndForceDockerShim ( ) {
// Detect if we're in a GHA runner which is Linux, doesn't have Systemd, and does have Docker.
// This is a common case in self-hosted runners, providers like [Namespace](https://namespace.so/),
// and especially GitHub Enterprise Server.
if ( process . env . RUNNER _OS !== "Linux" ) {
2024-04-16 00:23:29 +02:00
if ( this . forceDockerShim ) {
actionsCore . warning ( "Ignoring force-docker-shim which is set to true, as it is only supported on Linux." ) ;
this . forceDockerShim = false ;
2024-04-11 17:58:56 +02:00
}
return ;
}
const systemdCheck = fs . statSync ( "/run/systemd/system" , {
throwIfNoEntry : false ,
} ) ;
if ( systemdCheck ? . isDirectory ( ) ) {
2024-04-16 00:23:29 +02:00
if ( this . forceDockerShim ) {
actionsCore . warning ( "Systemd is detected, but ignoring it since force-docker-shim is enabled." ) ;
2024-04-11 17:58:56 +02:00
}
else {
this . idslib . addFact ( "has_systemd" , true ) ;
return ;
}
}
this . idslib . addFact ( "has_systemd" , false ) ;
2024-04-16 00:23:29 +02:00
actionsCore . debug ( "Linux detected without systemd, testing for Docker with `docker info` as an alternative daemon supervisor." ) ;
2024-04-11 17:58:56 +02:00
this . idslib . addFact ( "has_docker" , false ) ; // Set to false here, and only in the success case do we set it to true
2024-04-16 00:23:29 +02:00
let exitCode ;
2024-04-11 17:58:56 +02:00
try {
2024-04-16 00:23:29 +02:00
exitCode = await actionsExec . exec ( "docker" , [ "info" ] , {
2024-04-11 17:58:56 +02:00
silent : true ,
listeners : {
stdout : ( data ) => {
const trimmed = data . toString ( "utf-8" ) . trimEnd ( ) ;
if ( trimmed . length >= 0 ) {
2024-04-16 00:23:29 +02:00
actionsCore . debug ( trimmed ) ;
2024-04-11 17:58:56 +02:00
}
} ,
stderr : ( data ) => {
const trimmed = data . toString ( "utf-8" ) . trimEnd ( ) ;
if ( trimmed . length >= 0 ) {
2024-04-16 00:23:29 +02:00
actionsCore . debug ( trimmed ) ;
2024-04-11 17:58:56 +02:00
}
} ,
} ,
} ) ;
}
catch ( e ) {
2024-04-16 00:23:29 +02:00
actionsCore . debug ( "Docker not detected, not enabling docker shim." ) ;
2024-04-11 17:58:56 +02:00
return ;
}
2024-04-16 00:23:29 +02:00
if ( exitCode !== 0 ) {
if ( this . forceDockerShim ) {
actionsCore . warning ( "docker info check failed, but trying anyway since force-docker-shim is enabled." ) ;
2024-04-11 17:58:56 +02:00
}
else {
return ;
}
}
this . idslib . addFact ( "has_docker" , true ) ;
2024-04-16 00:23:29 +02:00
if ( ! this . forceDockerShim &&
2024-04-11 17:58:56 +02:00
( await this . detectDockerWithMountedDockerSocket ( ) ) ) {
2024-04-16 00:23:29 +02:00
actionsCore . debug ( "Detected a Docker container with a Docker socket mounted, not enabling docker shim." ) ;
2024-04-11 17:58:56 +02:00
return ;
}
2024-04-16 00:23:29 +02:00
actionsCore . startGroup ( "Enabling the Docker shim for running Nix on Linux in CI without Systemd." ) ;
2024-04-11 17:58:56 +02:00
if ( this . init !== "none" ) {
2024-04-16 00:23:29 +02:00
actionsCore . info ( ` Changing init from ' ${ this . init } ' to 'none' ` ) ;
2024-04-11 17:58:56 +02:00
this . init = "none" ;
}
if ( this . planner !== "linux" ) {
2024-04-16 00:23:29 +02:00
actionsCore . info ( ` Changing planner from ' ${ this . planner } ' to 'linux' ` ) ;
2024-04-11 17:58:56 +02:00
this . planner = "linux" ;
}
2024-04-16 00:23:29 +02:00
this . forceDockerShim = true ;
actionsCore . endGroup ( ) ;
2024-04-11 17:58:56 +02:00
}
// Detect if we are running under `act` or some other system which is not using docker-in-docker,
// and instead using a mounted docker socket.
// In the case of the socket mount solution, the shim will cause issues since the given mount paths will
// equate to mount paths on the host, not mount paths to the docker container in question.
async detectDockerWithMountedDockerSocket ( ) {
2024-04-16 00:23:29 +02:00
let cgroupsBuffer ;
2024-04-11 17:58:56 +02:00
try {
// If we are inside a docker container, the last line of `/proc/self/cgroup` should be
// 0::/docker/$SOME_ID
//
// If we are not, the line will likely be `0::/`
2024-04-16 00:23:29 +02:00
cgroupsBuffer = await readFile ( "/proc/self/cgroup" , {
2024-04-11 17:58:56 +02:00
encoding : "utf-8" ,
} ) ;
}
catch ( e ) {
2024-04-16 00:23:29 +02:00
actionsCore . debug ( ` Did not detect \` /proc/self/cgroup \` existence, bailing on docker container ID detection: \n ${ e } ` ) ;
2024-04-11 17:58:56 +02:00
return false ;
}
2024-04-16 00:23:29 +02:00
const cgroups = cgroupsBuffer . trim ( ) . split ( "\n" ) ;
const lastCgroup = cgroups [ cgroups . length - 1 ] ;
const lastCgroupParts = lastCgroup . split ( ":" ) ;
const lastCgroupPath = lastCgroupParts [ lastCgroupParts . length - 1 ] ;
if ( ! lastCgroupPath . includes ( "/docker/" ) ) {
actionsCore . debug ( "Did not detect a container ID, bailing on docker.sock detection" ) ;
2024-04-11 17:58:56 +02:00
return false ;
}
// We are in a docker container, now to determine if this container is visible from
// the `docker` command, and if so, if there is a `docker.socket` mounted.
2024-04-16 00:23:29 +02:00
const lastCgroupPathParts = lastCgroupPath . split ( "/" ) ;
const containerId = lastCgroupPathParts [ lastCgroupPathParts . length - 1 ] ;
2024-04-11 17:58:56 +02:00
// If we cannot `docker inspect` this discovered container ID, we'll fall through to the `catch` below.
2024-04-16 00:23:29 +02:00
let stdoutBuffer = "" ;
let stderrBuffer = "" ;
let exitCode ;
2024-04-11 17:58:56 +02:00
try {
2024-04-16 00:23:29 +02:00
exitCode = await actionsExec . exec ( "docker" , [ "inspect" , containerId ] , {
2024-04-11 17:58:56 +02:00
silent : true ,
listeners : {
stdout : ( data ) => {
2024-04-16 00:23:29 +02:00
stdoutBuffer += data . toString ( "utf-8" ) ;
2024-04-11 17:58:56 +02:00
} ,
stderr : ( data ) => {
2024-04-16 00:23:29 +02:00
stderrBuffer += data . toString ( "utf-8" ) ;
2024-04-11 17:58:56 +02:00
} ,
} ,
} ) ;
}
catch ( e ) {
2024-04-16 00:23:29 +02:00
actionsCore . debug ( ` Could not execute \` docker inspect ${ containerId } \` , bailing on docker container inspection: \n ${ e } ` ) ;
2024-04-11 17:58:56 +02:00
return false ;
}
2024-04-16 00:23:29 +02:00
if ( exitCode !== 0 ) {
actionsCore . debug ( ` Unable to inspect detected docker container with id \` ${ containerId } \` , bailing on container inspection (exit ${ exitCode } ): \n ${ stderrBuffer } ` ) ;
2024-04-11 17:58:56 +02:00
return false ;
}
2024-04-16 00:23:29 +02:00
const output = JSON . parse ( stdoutBuffer ) ;
2024-04-11 17:58:56 +02:00
// `docker inspect $ID` prints an array containing objects.
// In our use case, we should only see 1 item in the array.
if ( output . length !== 1 ) {
2024-04-16 00:23:29 +02:00
actionsCore . debug ( ` Got \` docker inspect ${ containerId } \` output which was not one item (was ${ output . length } ), bailing on docker.sock detection. ` ) ;
2024-04-11 17:58:56 +02:00
return false ;
}
const item = output [ 0 ] ;
// On this array item we want the `Mounts` field, which is an array
// containing `{ Type, Source, Destination, Mode}`.
// We are looking for a `Destination` ending with `docker.sock`.
const mounts = item [ "Mounts" ] ;
if ( typeof mounts !== "object" ) {
2024-04-16 00:23:29 +02:00
actionsCore . debug ( ` Got non-object in \` Mounts \` field of \` docker inspect ${ containerId } \` output, bailing on docker.sock detection. ` ) ;
2024-04-11 17:58:56 +02:00
return false ;
}
2024-04-16 00:23:29 +02:00
let foundDockerSockMount = false ;
2024-04-11 17:58:56 +02:00
for ( const mount of mounts ) {
const destination = mount [ "Destination" ] ;
if ( typeof destination === "string" ) {
if ( destination . endsWith ( "docker.sock" ) ) {
2024-04-16 00:23:29 +02:00
foundDockerSockMount = true ;
2024-04-11 17:58:56 +02:00
break ;
}
}
}
2024-04-16 00:23:29 +02:00
return foundDockerSockMount ;
2024-04-11 17:58:56 +02:00
}
async executionEnvironment ( ) {
2024-04-16 00:23:29 +02:00
const executionEnv = { } ;
executionEnv . NIX _INSTALLER _NO _CONFIRM = "true" ;
executionEnv . NIX _INSTALLER _DIAGNOSTIC _ATTRIBUTION = JSON . stringify ( this . idslib . getCorrelationHashes ( ) ) ;
2024-04-11 17:58:56 +02:00
if ( this . backtrace !== null ) {
2024-04-16 00:23:29 +02:00
executionEnv . RUST _BACKTRACE = this . backtrace ;
2024-04-11 17:58:56 +02:00
}
2024-04-16 00:23:29 +02:00
if ( this . modifyProfile !== null ) {
if ( this . modifyProfile ) {
executionEnv . NIX _INSTALLER _MODIFY _PROFILE = "true" ;
2024-04-11 17:58:56 +02:00
}
else {
2024-04-16 00:23:29 +02:00
executionEnv . NIX _INSTALLER _MODIFY _PROFILE = "false" ;
2024-04-11 17:58:56 +02:00
}
}
2024-04-16 00:23:29 +02:00
if ( this . nixBuildGroupId !== null ) {
executionEnv . NIX _INSTALLER _NIX _BUILD _GROUP _ID = ` ${ this . nixBuildGroupId } ` ;
2024-04-11 17:58:56 +02:00
}
2024-04-16 00:23:29 +02:00
if ( this . nixBuildGroupName !== null ) {
executionEnv . NIX _INSTALLER _NIX _BUILD _GROUP _NAME = this . nixBuildGroupName ;
2024-04-11 17:58:56 +02:00
}
2024-04-16 00:23:29 +02:00
if ( this . nixBuildUserPrefix !== null ) {
executionEnv . NIX _INSTALLER _NIX _BUILD _USER _PREFIX =
this . nixBuildUserPrefix ;
2024-04-11 17:58:56 +02:00
}
2024-04-16 00:23:29 +02:00
if ( this . nixBuildUserCount !== null ) {
executionEnv . NIX _INSTALLER _NIX _BUILD _USER _COUNT = ` ${ this . nixBuildUserCount } ` ;
2024-04-11 17:58:56 +02:00
}
2024-04-16 00:23:29 +02:00
if ( this . nixBuildUserBase !== null ) {
executionEnv . NIX _INSTALLER _NIX _BUILD _USER _ID _BASE = ` ${ this . nixBuildUserCount } ` ;
2024-04-11 17:58:56 +02:00
}
2024-04-16 00:23:29 +02:00
if ( this . nixPackageUrl !== null ) {
executionEnv . NIX _INSTALLER _NIX _PACKAGE _URL = ` ${ this . nixPackageUrl } ` ;
2024-04-11 17:58:56 +02:00
}
if ( this . proxy !== null ) {
2024-04-16 00:23:29 +02:00
executionEnv . NIX _INSTALLER _PROXY = this . proxy ;
2024-04-11 17:58:56 +02:00
}
2024-04-16 00:23:29 +02:00
if ( this . sslCertFile !== null ) {
executionEnv . NIX _INSTALLER _SSL _CERT _FILE = this . sslCertFile ;
2024-04-11 17:58:56 +02:00
}
2024-04-16 00:23:29 +02:00
executionEnv . NIX _INSTALLER _DIAGNOSTIC _ENDPOINT =
2024-04-11 17:58:56 +02:00
this . idslib . getDiagnosticsUrl ( ) ? . toString ( ) || "" ;
// TODO: Error if the user uses these on not-MacOS
2024-04-16 00:23:29 +02:00
if ( this . macEncrypt !== null ) {
2024-04-11 17:58:56 +02:00
if ( process . env . RUNNER _OS !== "macOS" ) {
throw new Error ( "`mac-encrypt` while `$RUNNER_OS` was not `macOS`" ) ;
}
2024-04-16 00:23:29 +02:00
executionEnv . NIX _INSTALLER _ENCRYPT = this . macEncrypt ;
2024-04-11 17:58:56 +02:00
}
2024-04-16 00:23:29 +02:00
if ( this . macCaseSensitive !== null ) {
2024-04-11 17:58:56 +02:00
if ( process . env . RUNNER _OS !== "macOS" ) {
throw new Error ( "`mac-case-sensitive` while `$RUNNER_OS` was not `macOS`" ) ;
}
2024-04-16 00:23:29 +02:00
executionEnv . NIX _INSTALLER _CASE _SENSITIVE = this . macCaseSensitive ;
2024-04-11 17:58:56 +02:00
}
2024-04-16 00:23:29 +02:00
if ( this . macVolumeLabel !== null ) {
2024-04-11 17:58:56 +02:00
if ( process . env . RUNNER _OS !== "macOS" ) {
throw new Error ( "`mac-volume-label` while `$RUNNER_OS` was not `macOS`" ) ;
}
2024-04-16 00:23:29 +02:00
executionEnv . NIX _INSTALLER _VOLUME _LABEL = this . macVolumeLabel ;
2024-04-11 17:58:56 +02:00
}
2024-04-16 00:23:29 +02:00
if ( this . macRootDisk !== null ) {
2024-04-11 17:58:56 +02:00
if ( process . env . RUNNER _OS !== "macOS" ) {
throw new Error ( "`mac-root-disk` while `$RUNNER_OS` was not `macOS`" ) ;
}
2024-04-16 00:23:29 +02:00
executionEnv . NIX _INSTALLER _ROOT _DISK = this . macRootDisk ;
2024-04-11 17:58:56 +02:00
}
if ( this . logger !== null ) {
2024-04-16 00:23:29 +02:00
executionEnv . NIX _INSTALLER _LOGGER = this . logger ;
2024-04-11 17:58:56 +02:00
}
2024-04-16 00:23:29 +02:00
if ( this . logDirectives !== null ) {
executionEnv . NIX _INSTALLER _LOG _DIRECTIVES = this . logDirectives ;
2024-04-11 17:58:56 +02:00
}
// TODO: Error if the user uses these on MacOS
if ( this . init !== null ) {
if ( process . env . RUNNER _OS === "macOS" ) {
throw new Error ( "`init` is not a valid option when `$RUNNER_OS` is `macOS`" ) ;
}
2024-04-16 00:23:29 +02:00
executionEnv . NIX _INSTALLER _INIT = this . init ;
2024-04-11 17:58:56 +02:00
}
2024-04-16 00:23:29 +02:00
if ( this . startDaemon !== null ) {
if ( this . startDaemon ) {
executionEnv . NIX _INSTALLER _START _DAEMON = "true" ;
2024-04-11 17:58:56 +02:00
}
else {
2024-04-16 00:23:29 +02:00
executionEnv . NIX _INSTALLER _START _DAEMON = "false" ;
2024-04-11 17:58:56 +02:00
}
}
2024-04-16 00:23:29 +02:00
let extraConf = "" ;
if ( this . githubServerUrl !== null && this . githubToken !== null ) {
const serverUrl = this . githubServerUrl . replace ( "https://" , "" ) ;
extraConf += ` access-tokens = ${ serverUrl } = ${ this . githubToken } ` ;
extraConf += "\n" ;
2024-04-11 17:58:56 +02:00
}
2024-04-16 00:23:29 +02:00
if ( this . trustRunnerUser !== null ) {
2024-04-11 17:58:56 +02:00
const user = userInfo ( ) . username ;
if ( user ) {
2024-04-16 00:23:29 +02:00
extraConf += ` trusted-users = root ${ user } ` ;
2024-04-11 17:58:56 +02:00
}
else {
2024-04-16 00:23:29 +02:00
extraConf += ` trusted-users = root ` ;
2024-04-11 17:58:56 +02:00
}
2024-04-16 00:23:29 +02:00
extraConf += "\n" ;
2024-04-11 17:58:56 +02:00
}
if ( this . flakehub ) {
2024-04-16 00:23:29 +02:00
extraConf += ` netrc-file = ${ await this . flakehubLogin ( ) } ` ;
extraConf += "\n" ;
2024-04-11 17:58:56 +02:00
}
2024-04-16 00:23:29 +02:00
if ( this . extraConf !== null && this . extraConf . length !== 0 ) {
extraConf += this . extraConf . join ( "\n" ) ;
extraConf += "\n" ;
2024-04-11 17:58:56 +02:00
}
2024-04-16 00:23:29 +02:00
executionEnv . NIX _INSTALLER _EXTRA _CONF = extraConf ;
2024-04-11 17:58:56 +02:00
if ( process . env . ACT && ! process . env . NOT _ACT ) {
this . idslib . addFact ( "in_act" , true ) ;
2024-04-16 00:23:29 +02:00
actionsCore . info ( "Detected `$ACT` environment, assuming this is a https://github.com/nektos/act created container, set `NOT_ACT=true` to override this. This will change the setting of the `init` to be compatible with `act`" ) ;
executionEnv . NIX _INSTALLER _INIT = "none" ;
2024-04-11 17:58:56 +02:00
}
if ( process . env . NSC _VM _ID && ! process . env . NOT _NAMESPACE ) {
this . idslib . addFact ( "in_namespace_so" , true ) ;
2024-04-16 00:23:29 +02:00
actionsCore . info ( "Detected Namespace runner, assuming this is a https://namespace.so created container, set `NOT_NAMESPACE=true` to override this. This will change the setting of the `init` to be compatible with Namespace" ) ;
executionEnv . NIX _INSTALLER _INIT = "none" ;
2024-04-11 17:58:56 +02:00
}
2024-04-16 00:23:29 +02:00
return executionEnv ;
2024-04-11 17:58:56 +02:00
}
2024-04-16 00:23:29 +02:00
async executeInstall ( binaryPath ) {
const executionEnv = await this . executionEnvironment ( ) ;
actionsCore . debug ( ` Execution environment: ${ JSON . stringify ( executionEnv , null , 4 ) } ` ) ;
2024-04-11 17:58:56 +02:00
const args = [ "install" ] ;
if ( this . planner ) {
this . idslib . addFact ( "nix_installer_planner" , this . planner ) ;
args . push ( this . planner ) ;
}
else {
2024-04-16 00:23:29 +02:00
this . idslib . addFact ( "nix_installer_planner" , getDefaultPlanner ( ) ) ;
args . push ( getDefaultPlanner ( ) ) ;
2024-04-11 17:58:56 +02:00
}
2024-04-16 00:23:29 +02:00
if ( this . extraArgs ) {
const extraArgs = stringArgv ( this . extraArgs ) ;
args . concat ( extraArgs ) ;
2024-04-11 17:58:56 +02:00
}
this . idslib . recordEvent ( "install_nix_start" ) ;
2024-04-16 00:23:29 +02:00
const exitCode = await actionsExec . exec ( binaryPath , args , {
2024-04-11 17:58:56 +02:00
env : {
2024-04-16 00:23:29 +02:00
... executionEnv ,
2024-04-11 17:58:56 +02:00
... process . env , // To get $PATH, etc
} ,
} ) ;
2024-04-16 00:23:29 +02:00
if ( exitCode !== 0 ) {
2024-04-11 17:58:56 +02:00
this . idslib . recordEvent ( "install_nix_failure" , {
2024-04-16 00:23:29 +02:00
exitCode ,
2024-04-11 17:58:56 +02:00
} ) ;
2024-04-16 00:23:29 +02:00
throw new Error ( ` Non-zero exit code of \` ${ exitCode } \` detected ` ) ;
2024-04-11 17:58:56 +02:00
}
this . idslib . recordEvent ( "install_nix_success" ) ;
2024-04-16 00:23:29 +02:00
return exitCode ;
2024-04-11 17:58:56 +02:00
}
async install ( ) {
2024-04-16 00:23:29 +02:00
const existingInstall = await this . detectExisting ( ) ;
if ( existingInstall ) {
2024-04-11 17:58:56 +02:00
if ( this . reinstall ) {
// We need to uninstall, then reinstall
2024-04-16 00:23:29 +02:00
actionsCore . info ( "Nix was already installed, `reinstall` is set, uninstalling for a reinstall" ) ;
await this . executeUninstall ( ) ;
2024-04-11 17:58:56 +02:00
}
else {
// We're already installed, and not reinstalling, just set GITHUB_PATH and finish early
2024-04-16 00:23:29 +02:00
await this . setGithubPath ( ) ;
actionsCore . info ( "Nix was already installed, using existing install" ) ;
2024-04-11 17:58:56 +02:00
return ;
}
}
if ( this . kvm ) {
2024-04-16 00:23:29 +02:00
actionsCore . startGroup ( "Configuring KVM" ) ;
if ( await this . setupKvm ( ) ) {
actionsCore . endGroup ( ) ;
actionsCore . info ( "\u001b[32m Accelerated KVM is enabled \u001b[33m⚡️ " ) ;
actionsCore . exportVariable ( "DETERMINATE_NIX_KVM" , "1" ) ;
2024-04-11 17:58:56 +02:00
}
else {
2024-04-16 00:23:29 +02:00
actionsCore . endGroup ( ) ;
actionsCore . info ( "KVM is not available." ) ;
actionsCore . exportVariable ( "DETERMINATE_NIX_KVM" , "0" ) ;
2024-04-11 17:58:56 +02:00
}
}
// Normal just doing of the install
2024-04-16 00:23:29 +02:00
actionsCore . startGroup ( "Installing Nix" ) ;
const binaryPath = await this . fetchBinary ( ) ;
await this . executeInstall ( binaryPath ) ;
actionsCore . endGroup ( ) ;
if ( this . forceDockerShim ) {
2024-04-11 17:58:56 +02:00
await this . spawnDockerShim ( ) ;
}
2024-04-16 00:23:29 +02:00
await this . setGithubPath ( ) ;
2024-04-11 17:58:56 +02:00
}
async spawnDockerShim ( ) {
2024-04-16 00:23:29 +02:00
actionsCore . startGroup ( "Configuring the Docker shim as the Nix Daemon's process supervisor" ) ;
2024-04-11 17:58:56 +02:00
const images = {
X64 : path . join ( _ _dirname , "/../docker-shim/amd64.tar.gz" ) ,
ARM64 : path . join ( _ _dirname , "/../docker-shim/arm64.tar.gz" ) ,
} ;
let arch ;
if ( process . env . RUNNER _ARCH === "X64" ) {
arch = "X64" ;
}
else if ( process . env . RUNNER _ARCH === "ARM64" ) {
arch = "ARM64" ;
}
else {
throw Error ( "Architecture not supported in Docker shim mode." ) ;
}
2024-04-16 00:23:29 +02:00
actionsCore . debug ( "Loading image: determinate-nix-shim:latest..." ) ;
2024-04-11 17:58:56 +02:00
{
2024-04-16 00:23:29 +02:00
const exitCode = await actionsExec . exec ( "docker" , [ "image" , "load" , "--input" , images [ arch ] ] , {
2024-04-11 17:58:56 +02:00
silent : true ,
listeners : {
stdout : ( data ) => {
const trimmed = data . toString ( "utf-8" ) . trimEnd ( ) ;
if ( trimmed . length >= 0 ) {
2024-04-16 00:23:29 +02:00
actionsCore . debug ( trimmed ) ;
2024-04-11 17:58:56 +02:00
}
} ,
stderr : ( data ) => {
const trimmed = data . toString ( "utf-8" ) . trimEnd ( ) ;
if ( trimmed . length >= 0 ) {
2024-04-16 00:23:29 +02:00
actionsCore . debug ( trimmed ) ;
2024-04-11 17:58:56 +02:00
}
} ,
} ,
} ) ;
2024-04-16 00:23:29 +02:00
if ( exitCode !== 0 ) {
throw new Error ( ` Failed to build the shim image, exit code: \` ${ exitCode } \` ` ) ;
2024-04-11 17:58:56 +02:00
}
}
{
2024-04-16 00:23:29 +02:00
actionsCore . debug ( "Starting the Nix daemon through Docker..." ) ;
2024-04-11 17:58:56 +02:00
this . idslib . recordEvent ( "start_docker_shim" ) ;
2024-04-16 00:23:29 +02:00
const exitCode = await actionsExec . exec ( "docker" , [
2024-04-11 17:58:56 +02:00
"--log-level=debug" ,
"run" ,
"--detach" ,
"--privileged" ,
"--network=host" ,
"--userns=host" ,
"--pid=host" ,
"--mount" ,
"type=bind,src=/bin,dst=/bin,readonly" ,
"--mount" ,
"type=bind,src=/lib,dst=/lib,readonly" ,
"--mount" ,
"type=bind,src=/home,dst=/home,readonly" ,
"--mount" ,
"type=bind,src=/tmp,dst=/tmp" ,
"--mount" ,
"type=bind,src=/nix,dst=/nix" ,
"--mount" ,
"type=bind,src=/etc,dst=/etc,readonly" ,
"--restart" ,
"always" ,
"--init" ,
"--name" ,
` determinate-nix-shim- ${ this . idslib . getUniqueId ( ) } - ${ randomUUID ( ) } ` ,
"determinate-nix-shim:latest" ,
] , {
silent : true ,
listeners : {
stdline : ( data ) => {
2024-04-16 00:23:29 +02:00
actionsCore . saveState ( "docker_shim_container_id" , data . trimEnd ( ) ) ;
2024-04-11 17:58:56 +02:00
} ,
stdout : ( data ) => {
const trimmed = data . toString ( "utf-8" ) . trimEnd ( ) ;
if ( trimmed . length >= 0 ) {
2024-04-16 00:23:29 +02:00
actionsCore . debug ( trimmed ) ;
2024-04-11 17:58:56 +02:00
}
} ,
stderr : ( data ) => {
const trimmed = data . toString ( "utf-8" ) . trimEnd ( ) ;
if ( trimmed . length >= 0 ) {
2024-04-16 00:23:29 +02:00
actionsCore . debug ( trimmed ) ;
2024-04-11 17:58:56 +02:00
}
} ,
} ,
} ) ;
2024-04-16 00:23:29 +02:00
if ( exitCode !== 0 ) {
throw new Error ( ` Failed to start the Nix daemon through Docker, exit code: \` ${ exitCode } \` ` ) ;
2024-04-11 17:58:56 +02:00
}
}
2024-04-16 00:23:29 +02:00
actionsCore . endGroup ( ) ;
2024-04-11 17:58:56 +02:00
return ;
}
async cleanupDockerShim ( ) {
2024-04-16 00:23:29 +02:00
const containerId = actionsCore . getState ( "docker_shim_container_id" ) ;
if ( containerId !== "" ) {
actionsCore . startGroup ( "Cleaning up the Nix daemon's Docker shim" ) ;
2024-04-11 17:58:56 +02:00
let cleaned = false ;
try {
2024-04-16 00:23:29 +02:00
await actionsExec . exec ( "docker" , [ "rm" , "--force" , containerId ] ) ;
2024-04-11 17:58:56 +02:00
cleaned = true ;
}
catch {
2024-04-16 00:23:29 +02:00
actionsCore . warning ( "failed to cleanup nix daemon container" ) ;
2024-04-11 17:58:56 +02:00
}
if ( ! cleaned ) {
2024-04-16 00:23:29 +02:00
actionsCore . info ( "trying to pkill the container's shim process" ) ;
2024-04-11 17:58:56 +02:00
try {
2024-04-16 00:23:29 +02:00
await actionsExec . exec ( "pkill" , [ containerId ] ) ;
2024-04-11 17:58:56 +02:00
cleaned = true ;
}
catch {
2024-04-16 00:23:29 +02:00
actionsCore . warning ( "failed to forcibly kill the container's shim process" ) ;
2024-04-11 17:58:56 +02:00
}
}
if ( cleaned ) {
this . idslib . recordEvent ( "clean_up_docker_shim" ) ;
}
else {
2024-04-16 00:23:29 +02:00
actionsCore . warning ( "Giving up on cleaning up the nix daemon container" ) ;
2024-04-11 17:58:56 +02:00
}
2024-04-16 00:23:29 +02:00
actionsCore . endGroup ( ) ;
2024-04-11 17:58:56 +02:00
}
}
2024-04-16 00:23:29 +02:00
async setGithubPath ( ) {
2024-04-11 17:58:56 +02:00
// Interim versions of the `nix-installer` crate may have already manipulated `$GITHUB_PATH`, as root even! Accessing that will be an error.
try {
2024-04-16 00:23:29 +02:00
const nixVarNixProfilePath = "/nix/var/nix/profiles/default/bin" ;
const homeNixProfilePath = ` ${ process . env . HOME } /.nix-profile/bin ` ;
actionsCore . addPath ( nixVarNixProfilePath ) ;
actionsCore . addPath ( homeNixProfilePath ) ;
actionsCore . info ( ` Added \` ${ nixVarNixProfilePath } \` and \` ${ homeNixProfilePath } \` to \` $ GITHUB_PATH \` ` ) ;
2024-04-11 17:58:56 +02:00
}
catch ( error ) {
2024-04-16 00:23:29 +02:00
actionsCore . info ( "Skipping setting $GITHUB_PATH in action, the `nix-installer` crate seems to have done this already. From `nix-installer` version 0.11.0 and up, this step is done in the action. Prior to 0.11.0, this was only done in the `nix-installer` binary." ) ;
2024-04-11 17:58:56 +02:00
}
}
2024-04-16 00:23:29 +02:00
async flakehubLogin ( ) {
2024-04-11 17:58:56 +02:00
this . idslib . recordEvent ( "login_to_flakehub" ) ;
2024-04-16 00:23:29 +02:00
const netrcPath = ` ${ process . env [ "RUNNER_TEMP" ] } /determinate-nix-installer-netrc ` ;
const jwt = await actionsCore . getIDToken ( "api.flakehub.com" ) ;
await writeFile ( netrcPath , [
2024-04-11 17:58:56 +02:00
` machine api.flakehub.com login flakehub password ${ jwt } ` ,
` machine flakehub.com login flakehub password ${ jwt } ` ,
] . join ( "\n" ) ) ;
2024-04-16 00:23:29 +02:00
actionsCore . info ( "Logging in to FlakeHub." ) ;
2024-04-11 17:58:56 +02:00
// the join followed by a match on ^... looks silly, but extra_config
// could contain multi-line values
2024-04-16 00:23:29 +02:00
if ( this . extraConf ? . join ( "\n" ) . match ( /^netrc-file/m ) ) {
actionsCore . warning ( "Logging in to FlakeHub conflicts with the Nix option `netrc-file`." ) ;
2024-04-11 17:58:56 +02:00
}
2024-04-16 00:23:29 +02:00
return netrcPath ;
2024-04-11 17:58:56 +02:00
}
2024-04-16 00:23:29 +02:00
async executeUninstall ( ) {
2024-04-11 17:58:56 +02:00
this . idslib . recordEvent ( "uninstall" ) ;
2024-04-16 00:23:29 +02:00
const exitCode = await actionsExec . exec ( ` /nix/nix-installer ` , [ "uninstall" ] , {
2024-04-11 17:58:56 +02:00
env : {
NIX _INSTALLER _NO _CONFIRM : "true" ,
... process . env , // To get $PATH, etc
} ,
} ) ;
2024-04-16 00:23:29 +02:00
if ( exitCode !== 0 ) {
throw new Error ( ` Non-zero exit code of \` ${ exitCode } \` detected ` ) ;
2024-04-11 17:58:56 +02:00
}
2024-04-16 00:23:29 +02:00
return exitCode ;
2024-04-11 17:58:56 +02:00
}
2024-04-16 00:23:29 +02:00
async detectExisting ( ) {
const receiptPath = "/nix/receipt.json" ;
2024-04-11 17:58:56 +02:00
try {
2024-04-16 00:23:29 +02:00
await access ( receiptPath ) ;
2024-04-11 17:58:56 +02:00
// There is a /nix/receipt.json
return true ;
}
catch {
// No /nix/receipt.json
return false ;
}
}
2024-04-16 00:23:29 +02:00
async setupKvm ( ) {
2024-04-11 17:58:56 +02:00
this . idslib . recordEvent ( "setup_kvm" ) ;
2024-04-16 00:23:29 +02:00
const currentUser = userInfo ( ) ;
const isRoot = currentUser . uid === 0 ;
const maybeSudo = isRoot ? "" : "sudo" ;
const kvmRules = "/etc/udev/rules.d/99-determinate-nix-installer-kvm.rules" ;
2024-04-11 17:58:56 +02:00
try {
2024-04-16 00:23:29 +02:00
const writeFileExitCode = await actionsExec . exec ( "sh" , [
2024-04-11 17:58:56 +02:00
"-c" ,
2024-04-16 00:23:29 +02:00
` echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | ${ maybeSudo } tee ${ kvmRules } > /dev/null ` ,
2024-04-11 17:58:56 +02:00
] , {
silent : true ,
listeners : {
stdout : ( data ) => {
const trimmed = data . toString ( "utf-8" ) . trimEnd ( ) ;
if ( trimmed . length >= 0 ) {
2024-04-16 00:23:29 +02:00
actionsCore . debug ( trimmed ) ;
2024-04-11 17:58:56 +02:00
}
} ,
stderr : ( data ) => {
const trimmed = data . toString ( "utf-8" ) . trimEnd ( ) ;
if ( trimmed . length >= 0 ) {
2024-04-16 00:23:29 +02:00
actionsCore . debug ( trimmed ) ;
2024-04-11 17:58:56 +02:00
}
} ,
} ,
} ) ;
2024-04-16 00:23:29 +02:00
if ( writeFileExitCode !== 0 ) {
throw new Error ( ` Non-zero exit code of \` ${ writeFileExitCode } \` detected while writing ' ${ kvmRules } ' ` ) ;
2024-04-11 17:58:56 +02:00
}
2024-04-16 00:23:29 +02:00
const debugRootRunThrow = async ( action , command , args ) => {
if ( ! isRoot ) {
2024-04-11 17:58:56 +02:00
args = [ command , ... args ] ;
command = "sudo" ;
}
2024-04-16 00:23:29 +02:00
const reloadExitCode = await actionsExec . exec ( command , args , {
2024-04-11 17:58:56 +02:00
silent : true ,
listeners : {
stdout : ( data ) => {
const trimmed = data . toString ( "utf-8" ) . trimEnd ( ) ;
if ( trimmed . length >= 0 ) {
2024-04-16 00:23:29 +02:00
actionsCore . debug ( trimmed ) ;
2024-04-11 17:58:56 +02:00
}
} ,
stderr : ( data ) => {
const trimmed = data . toString ( "utf-8" ) . trimEnd ( ) ;
if ( trimmed . length >= 0 ) {
2024-04-16 00:23:29 +02:00
actionsCore . debug ( trimmed ) ;
2024-04-11 17:58:56 +02:00
}
} ,
} ,
} ) ;
2024-04-16 00:23:29 +02:00
if ( reloadExitCode !== 0 ) {
throw new Error ( ` Non-zero exit code of \` ${ reloadExitCode } \` detected while ${ action } . ` ) ;
2024-04-11 17:58:56 +02:00
}
} ;
2024-04-16 00:23:29 +02:00
await debugRootRunThrow ( "reloading udev rules" , "udevadm" , [
2024-04-11 17:58:56 +02:00
"control" ,
"--reload-rules" ,
] ) ;
2024-04-16 00:23:29 +02:00
await debugRootRunThrow ( "triggering udev against kvm" , "udevadm" , [
2024-04-11 17:58:56 +02:00
"trigger" ,
"--name-match=kvm" ,
] ) ;
return true ;
}
catch ( error ) {
2024-04-16 00:23:29 +02:00
if ( isRoot ) {
await actionsExec . exec ( "rm" , [ "-f" , kvmRules ] ) ;
2024-04-11 17:58:56 +02:00
}
else {
2024-04-16 00:23:29 +02:00
await actionsExec . exec ( "sudo" , [ "rm" , "-f" , kvmRules ] ) ;
2024-04-11 17:58:56 +02:00
}
return false ;
}
}
2024-04-16 00:23:29 +02:00
async fetchBinary ( ) {
if ( ! this . localRoot ) {
2024-04-11 17:58:56 +02:00
return await this . idslib . fetchExecutable ( ) ;
}
else {
2024-04-16 00:23:29 +02:00
const localPath = join ( this . localRoot , ` nix-installer- ${ this . platform } ` ) ;
actionsCore . info ( ` Using binary ${ localPath } ` ) ;
return localPath ;
2024-04-11 17:58:56 +02:00
}
}
2024-04-16 00:23:29 +02:00
async reportOverall ( ) {
2024-04-11 17:58:56 +02:00
try {
this . idslib . recordEvent ( "conclude_workflow" , {
2024-04-16 00:23:29 +02:00
conclusion : await this . getWorkflowConclusion ( ) ,
2024-04-11 17:58:56 +02:00
} ) ;
}
catch ( error ) {
2024-04-16 00:23:29 +02:00
actionsCore . debug ( ` Error submitting post-run diagnostics report: ${ error } ` ) ;
2024-04-11 17:58:56 +02:00
}
}
2024-04-16 00:23:29 +02:00
async getWorkflowConclusion ( ) {
if ( this . githubToken == null ) {
2024-04-11 17:58:56 +02:00
return undefined ;
}
try {
2024-04-16 00:23:29 +02:00
const octokit = github . getOctokit ( this . githubToken ) ;
2024-04-11 17:58:56 +02:00
const jobs = await octokit . paginate ( octokit . rest . actions . listJobsForWorkflowRun , {
owner : github . context . repo . owner ,
repo : github . context . repo . repo ,
run _id : github . context . runId ,
} ) ;
2024-04-16 00:23:29 +02:00
actionsCore . debug ( ` awaited jobs: ${ jobs } ` ) ;
2024-04-11 17:58:56 +02:00
const job = jobs
. filter ( ( candidate ) => candidate . name === github . context . job )
. at ( 0 ) ;
if ( job === undefined ) {
return "no-jobs" ;
}
const outcomes = ( job . steps || [ ] ) . map ( ( j ) => j . conclusion || "unknown" ) ;
// Possible values: success, failure, cancelled, or skipped
// from: https://docs.github.com/en/actions/learn-github-actions/contexts
if ( outcomes . includes ( "failure" ) ) {
// Any failures fails the job
return "failure" ;
}
if ( outcomes . includes ( "cancelled" ) ) {
// Any cancellations cancels the job
return "cancelled" ;
}
// Assume success if no jobs failed or were canceled
return "success" ;
}
catch ( error ) {
2024-04-16 00:23:29 +02:00
actionsCore . debug ( ` Error determining final disposition: ${ error } ` ) ;
2024-04-11 17:58:56 +02:00
return "unavailable" ;
}
}
}
2024-04-16 00:23:29 +02:00
function getDefaultPlanner ( ) {
const envOs = process . env . RUNNER _OS ;
if ( envOs === "macOS" ) {
2024-04-11 17:58:56 +02:00
return "macos" ;
}
2024-04-16 00:23:29 +02:00
else if ( envOs === "Linux" ) {
2024-04-11 17:58:56 +02:00
return "linux" ;
}
else {
2024-04-16 00:23:29 +02:00
throw new Error ( ` Unsupported \` RUNNER_OS \` (currently \` ${ envOs } \` ) ` ) ;
2024-04-11 17:58:56 +02:00
}
}
function main ( ) {
const installer = new NixInstallerAction ( ) ;
installer . idslib . onMain ( async ( ) => {
await installer . detectAndForceDockerShim ( ) ;
await installer . install ( ) ;
} ) ;
installer . idslib . onPost ( async ( ) => {
await installer . cleanupDockerShim ( ) ;
2024-04-16 00:23:29 +02:00
await installer . reportOverall ( ) ;
2024-04-11 17:58:56 +02:00
} ) ;
installer . idslib . execute ( ) ;
}
main ( ) ;