Skip to content

Commit

Permalink
Nix feature (#228)
Browse files Browse the repository at this point in the history
  • Loading branch information
Chuxel authored Nov 1, 2022
1 parent 748be93 commit 0752a52
Show file tree
Hide file tree
Showing 20 changed files with 961 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/test-all.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ jobs:
"rust",
"sshd",
"terraform",
"nix",
]
baseImage:
[
Expand Down Expand Up @@ -88,6 +89,7 @@ jobs:
"rust",
"sshd",
"terraform",
"nix",
]
steps:
- uses: actions/checkout@v2
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/test-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ jobs:
rust: ./**/rust/**
sshd: ./**/sshd/**
terraform: ./**/terraform/**
nix: ./**/nix/**
test:
needs: [detect-changes]
Expand Down
28 changes: 28 additions & 0 deletions src/nix/NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
## OS Support

This Feature should work on recent versions of Debian/Ubuntu, RedHat Enterprise Linux, Fedora, RockyLinux, and Alpine Linux.

## Location of Flakes

Currently `flakeUri` works best with a remote URI (e.g., `github:nixos/nixpkgs/nixpkgs-unstable#hello`) as local files need to be in the image.

> Proposed support for lifecycle hooks in Features ([#60](https://github.com/devcontainers/spec/issues/60)) would allow for expressions files or Flakes to exist in the source tree to be automatically installed on initial container startup, but today you will have to manually add the appropriate install command to `postCreateCommand` to your `devcontainer.json` instead.
## Multi-user vs. single-user installs

This Dev Container Feature supports two installation models for Nix: multi-user and single user. Multi-user is the default, but each has pros and cons.

| Installation Model | Pros | Cons |
| --- | --- | --- |
| *Multi-User* | Nix can be used with any user including root.<br /><br />Also still works if the UID or GID of any user is updated. | Only works with Nix 2.11 and up due to a Nix installer limitation.<br /><br />Container must run either: run as root (but `remoteUser` in devcontainer.json can be non-root), or includes `sudo` with the `remoteUser` being configured to use it. <br /><br />Note that automated start of the `nix-daemon` requires passwordless `sudo` if the container itself (e.g., `containerUser`) is not running as root. Manual startup using `sudo` can require a password, however (more next). |
| *Single-User* | Does not require the container to run as root or `sudo` to be included in the image. | Only works with the user specified in the `userName` property or an auto-detected user. If this user's UID/GID is updated, that user will no longer be able to work with Nix. This is primarily a consideration when running on Linux where the UID/GID is sync'd to the local user. |

### Manually starting the Nix daemon

If you have `sudo` in your base image, but have a password set so automatic startup is not possible, you can manually start the Nix daemon by running the following command in a terminal:

```bash
sudo /usr/local/share/nix-entrypoint.sh
```

This same command can be used to restart the daemon if it has stopped for some reason. Logs are available at `/tmp/nix-daemon.log`.
37 changes: 37 additions & 0 deletions src/nix/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"id": "nix",
"version": "1.0.0",
"name": "Nix Package Manager",
"documentationURL": "https://github.com/devcontainers/features/tree/main/src/nix",
"description": "Installs the Nix package manager and optionally a set of packages.",
"options": {
"version": {
"type": "string",
"proposals": ["latest", "2.11"],
"default": "latest",
"description": "Version of Nix to install."
},
"multiUser": {
"type": "boolean",
"default": true,
"description": "Perform a multi-user install (instead of single user)"
},
"packages": {
"type": "string",
"default": "",
"description": "Optional comma separated list of Nix packages to install in profile."
},
"flakeUri": {
"type": "string",
"default": "",
"description": "Optional URI to a Nix Flake to install in profile."
}
},
"installsAfter": [
"ghcr.io/devcontainers/features/common-utils"
],
"containerEnv": {
"PATH": "/nix/var/nix/profiles/default/bin:/nix/var/nix/profiles/default/sbin:${PATH}"
},
"entrypoint": "/usr/local/share/nix-entrypoint.sh"
}
129 changes: 129 additions & 0 deletions src/nix/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#!/bin/bash
# Move to the same directory as this script
set -e
FEATURE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "${FEATURE_DIR}"

# Option defaults
VERSION="${VERSION:-"latest"}"
MULTIUSER="${MULTIUSER:-"true"}"
PACKAGES="${PACKAGES//,/ }"
FLAKEURI="${FLAKEURI:-""}"
USERNAME="${USERNAME:-"${_REMOTE_USER:-"automatic"}"}"

# Nix keys for securly verifying installer download signature per https://nixos.org/download.html#nix-verify-installation
NIX_GPG_KEYS="B541D55301270E0BCF15CA5D8170B4726D7198DE"
GPG_KEY_SERVERS="keyserver hkp://keyserver.ubuntu.com:80
keyserver hkps://keys.openpgp.org
keyserver hkp://keyserver.pgp.com"

if [ "$(id -u)" -ne 0 ]; then
echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.'
exit 1
fi

# Import common utils
. ./utils.sh

detect_user USERNAME

if [ -e "/nix" ]; then
echo "(!) Nix is already installed! Skipping installation."
else
if [ "${USERNAME}" = "root" ] && [ "${MULTIUSER}" != "true" ]; then
echo "(!) A single user install is not allowed for root. Add a non-root user to your image or set multiUser to true in your feature configuration."
exit 1
fi

# Verify dependencies
apt_get_update_if_exists
check_command curl "curl ca-certificates" "curl ca-certificates" "curl ca-certificates"
check_command gpg2 gnupg2 gnupg gnupg2
check_command dirmngr dirmngr dirmngr dirmngr
check_command xz xz-utils xz xz
check_command git git git git
check_command xargs findutils findutils findutils

# Determine version
find_version_from_git_tags VERSION https://github.com/NixOS/nix "tags/"

# Download and verify install per https://nixos.org/download.html#nix-verify-installation
tmpdir="$(mktemp -d)"
echo "(*) Downloading Nix installer..."
set +e
curl -sSLf -o "${tmpdir}/install-nix" https://releases.nixos.org/nix/nix-${VERSION}/install
exit_code=$?
set -e
if [ "$exit_code" != "0" ]; then
# Handle situation where git tags are ahead of what was is available to actually download
echo "(!) Nix version ${VERSION} failed to download. Attempting to fall back one version to retry..."
find_prev_version_from_git_tags VERSION https://github.com/NixOS/nix "tags/"
curl -sSLf -o "${tmpdir}/install-nix" https://releases.nixos.org/nix/nix-${VERSION}/install
fi
curl -sSLf -o "${tmpdir}/install-nix.asc" https://releases.nixos.org/nix/nix-${VERSION}/install.asc
cd "${tmpdir}"
receive_gpg_keys NIX_GPG_KEYS
gpg2 --verify ./install-nix.asc
cd "${FEATURE_DIR}"

# Do a multi or single-user setup based on feature config
if [ "${MULTIUSER}" = "true" ]; then
echo "(*) Performing multi-user install..."
sh "${tmpdir}/install-nix" --daemon
else
home_dir="$(eval echo ~${USERNAME})"
if [ ! -e "${home_dir}" ]; then
echo "(!) Home directory ${home_dir} does not exist for ${USERNAME}. Nix install will fail."
exit 1
fi
echo "(*) Performing single-user install..."
echo -e "\n**NOTE: Nix will only work for user ${USERNAME} on Linux if the host machine user's UID is $(id -u ${USERNAME}). You will need to chown /nix otherwise.**\n"
# Install per https://nixos.org/manual/nix/stable/installation/installing-binary.html#single-user-installation
mkdir -p /nix
chown ${USERNAME} /nix ${tmpdir}
su ${USERNAME} -c "sh \"${tmpdir}/install-nix\" --no-daemon --no-modify-profile"
# nix installer does not update ~/.bashrc, and USER may or may not be defined, so update rc/profile files directly to handle that
snippet='
if [ "${PATH#*$HOME/.nix-profile/bin}" = "${PATH}" ]; then if [ -z "$USER" ]; then USER=$(whoami); fi; . $HOME/.nix-profile/etc/profile.d/nix.sh; fi
'
update_rc_file "$home_dir/.bashrc" "${snippet}"
update_rc_file "$home_dir/.zshenv" "${snippet}"
update_rc_file "$home_dir/.profile" "${snippet}"
fi
rm -rf "${tmpdir}" "/tmp/tmp-gnupg"
fi

# Set nix config
mkdir -p /etc/nix
create_or_update_file /etc/nix/nix.conf 'sandbox = false'
if [ ! -z "${FLAKEURI}" ] && [ "${FLAKEURI}" != "none" ]; then
create_or_update_file /etc/nix/nix.conf 'experimental-features = nix-command flakes'
fi

# Create entrypoint if needed
if [ ! -e "/usr/local/share/nix-entrypoint.sh" ]; then
if [ "${MULTIUSER}" = "true" ]; then
echo "(*) Setting up entrypoint..."
cp -f nix-entrypoint.sh /usr/local/share/
else
echo -e '#!/bin/bash\nexec "$@"' > /usr/local/share/nix-entrypoint.sh
fi
chmod +x /usr/local/share/nix-entrypoint.sh
fi

# Install packages, flakes, etc if specified
chmod +x,o+r ${FEATURE_DIR} ${FEATURE_DIR}/post-install-steps.sh
if [ "${MULTIUSER}" = "true" ]; then
/usr/local/share/nix-entrypoint.sh
su ${USERNAME} -c "
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
${FEATURE_DIR}/post-install-steps.sh
"
else
su ${USERNAME} -c "
. \$HOME/.nix-profile/etc/profile.d/nix.sh
${FEATURE_DIR}/post-install-steps.sh
"
fi

echo "Done!"
21 changes: 21 additions & 0 deletions src/nix/nix-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/bash
# Attempt to start daemon
set +e
if ! pidof nix-daemon > /dev/null 2>&1; then
start_ok=false
if [ "$(id -u)" = "0" ]; then
( . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh; /nix/var/nix/profiles/default/bin/nix-daemon > /tmp/nix-daemon.log 2>&1 ) &
if [ "$?" = "0" ]; then
start_ok=true
fi
elif type sudo > /dev/null 2>&1; then
sudo -n sh -c '. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh; /nix/var/nix/profiles/default/bin/nix-daemon > /tmp/nix-daemon.log 2>&1' &
if [ "$?" = "0" ]; then
start_ok=true
fi
fi
if [ "${start_ok}" = "false" ]; then
echo -e 'Failed to start nix-daemon as root. Set multiUser to false in your feature configuraiton if you would\nprefer to run the container as a non-root. You may also start the daemon manually if you have sudo\ninstalled and configured for your user by running "sudo -c nix-daemon &"'
fi
fi
exec "$@"
18 changes: 18 additions & 0 deletions src/nix/post-install-steps.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/bash
set -e
echo "(*) Executing post-installation steps..."

# Install list of packages in profile if specified.
if [ ! -z "${PACKAGES}" ] && [ "${PACKAGES}" != "none" ]; then
echo "Installing packages \"${PACKAGES}\" in profile..."
nix-env --install ${PACKAGES}
fi

# Install Nix flake in profile if specified
if [ ! -z "${FLAKEURI}" ] && [ "${FLAKEURI}" != "none" ]; then
echo "Installing flake ${FLAKEURI} in profile..."
nix profile install "${FLAKEURI}"
fi

nix-collect-garbage --delete-old
nix-store --optimise
Loading

0 comments on commit 0752a52

Please sign in to comment.