Skip to content

Commit 0752a52

Browse files
authored
Nix feature (#228)
1 parent 748be93 commit 0752a52

20 files changed

+961
-0
lines changed

.github/workflows/test-all.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ jobs:
3838
"rust",
3939
"sshd",
4040
"terraform",
41+
"nix",
4142
]
4243
baseImage:
4344
[
@@ -88,6 +89,7 @@ jobs:
8889
"rust",
8990
"sshd",
9091
"terraform",
92+
"nix",
9193
]
9294
steps:
9395
- uses: actions/checkout@v2

.github/workflows/test-pr.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ jobs:
3838
rust: ./**/rust/**
3939
sshd: ./**/sshd/**
4040
terraform: ./**/terraform/**
41+
nix: ./**/nix/**
4142
4243
test:
4344
needs: [detect-changes]

src/nix/NOTES.md

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
## OS Support
2+
3+
This Feature should work on recent versions of Debian/Ubuntu, RedHat Enterprise Linux, Fedora, RockyLinux, and Alpine Linux.
4+
5+
## Location of Flakes
6+
7+
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.
8+
9+
> 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.
10+
11+
## Multi-user vs. single-user installs
12+
13+
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.
14+
15+
| Installation Model | Pros | Cons |
16+
| --- | --- | --- |
17+
| *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). |
18+
| *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. |
19+
20+
### Manually starting the Nix daemon
21+
22+
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:
23+
24+
```bash
25+
sudo /usr/local/share/nix-entrypoint.sh
26+
```
27+
28+
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`.

src/nix/devcontainer-feature.json

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"id": "nix",
3+
"version": "1.0.0",
4+
"name": "Nix Package Manager",
5+
"documentationURL": "https://github.com/devcontainers/features/tree/main/src/nix",
6+
"description": "Installs the Nix package manager and optionally a set of packages.",
7+
"options": {
8+
"version": {
9+
"type": "string",
10+
"proposals": ["latest", "2.11"],
11+
"default": "latest",
12+
"description": "Version of Nix to install."
13+
},
14+
"multiUser": {
15+
"type": "boolean",
16+
"default": true,
17+
"description": "Perform a multi-user install (instead of single user)"
18+
},
19+
"packages": {
20+
"type": "string",
21+
"default": "",
22+
"description": "Optional comma separated list of Nix packages to install in profile."
23+
},
24+
"flakeUri": {
25+
"type": "string",
26+
"default": "",
27+
"description": "Optional URI to a Nix Flake to install in profile."
28+
}
29+
},
30+
"installsAfter": [
31+
"ghcr.io/devcontainers/features/common-utils"
32+
],
33+
"containerEnv": {
34+
"PATH": "/nix/var/nix/profiles/default/bin:/nix/var/nix/profiles/default/sbin:${PATH}"
35+
},
36+
"entrypoint": "/usr/local/share/nix-entrypoint.sh"
37+
}

src/nix/install.sh

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#!/bin/bash
2+
# Move to the same directory as this script
3+
set -e
4+
FEATURE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
5+
cd "${FEATURE_DIR}"
6+
7+
# Option defaults
8+
VERSION="${VERSION:-"latest"}"
9+
MULTIUSER="${MULTIUSER:-"true"}"
10+
PACKAGES="${PACKAGES//,/ }"
11+
FLAKEURI="${FLAKEURI:-""}"
12+
USERNAME="${USERNAME:-"${_REMOTE_USER:-"automatic"}"}"
13+
14+
# Nix keys for securly verifying installer download signature per https://nixos.org/download.html#nix-verify-installation
15+
NIX_GPG_KEYS="B541D55301270E0BCF15CA5D8170B4726D7198DE"
16+
GPG_KEY_SERVERS="keyserver hkp://keyserver.ubuntu.com:80
17+
keyserver hkps://keys.openpgp.org
18+
keyserver hkp://keyserver.pgp.com"
19+
20+
if [ "$(id -u)" -ne 0 ]; then
21+
echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.'
22+
exit 1
23+
fi
24+
25+
# Import common utils
26+
. ./utils.sh
27+
28+
detect_user USERNAME
29+
30+
if [ -e "/nix" ]; then
31+
echo "(!) Nix is already installed! Skipping installation."
32+
else
33+
if [ "${USERNAME}" = "root" ] && [ "${MULTIUSER}" != "true" ]; then
34+
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."
35+
exit 1
36+
fi
37+
38+
# Verify dependencies
39+
apt_get_update_if_exists
40+
check_command curl "curl ca-certificates" "curl ca-certificates" "curl ca-certificates"
41+
check_command gpg2 gnupg2 gnupg gnupg2
42+
check_command dirmngr dirmngr dirmngr dirmngr
43+
check_command xz xz-utils xz xz
44+
check_command git git git git
45+
check_command xargs findutils findutils findutils
46+
47+
# Determine version
48+
find_version_from_git_tags VERSION https://github.com/NixOS/nix "tags/"
49+
50+
# Download and verify install per https://nixos.org/download.html#nix-verify-installation
51+
tmpdir="$(mktemp -d)"
52+
echo "(*) Downloading Nix installer..."
53+
set +e
54+
curl -sSLf -o "${tmpdir}/install-nix" https://releases.nixos.org/nix/nix-${VERSION}/install
55+
exit_code=$?
56+
set -e
57+
if [ "$exit_code" != "0" ]; then
58+
# Handle situation where git tags are ahead of what was is available to actually download
59+
echo "(!) Nix version ${VERSION} failed to download. Attempting to fall back one version to retry..."
60+
find_prev_version_from_git_tags VERSION https://github.com/NixOS/nix "tags/"
61+
curl -sSLf -o "${tmpdir}/install-nix" https://releases.nixos.org/nix/nix-${VERSION}/install
62+
fi
63+
curl -sSLf -o "${tmpdir}/install-nix.asc" https://releases.nixos.org/nix/nix-${VERSION}/install.asc
64+
cd "${tmpdir}"
65+
receive_gpg_keys NIX_GPG_KEYS
66+
gpg2 --verify ./install-nix.asc
67+
cd "${FEATURE_DIR}"
68+
69+
# Do a multi or single-user setup based on feature config
70+
if [ "${MULTIUSER}" = "true" ]; then
71+
echo "(*) Performing multi-user install..."
72+
sh "${tmpdir}/install-nix" --daemon
73+
else
74+
home_dir="$(eval echo ~${USERNAME})"
75+
if [ ! -e "${home_dir}" ]; then
76+
echo "(!) Home directory ${home_dir} does not exist for ${USERNAME}. Nix install will fail."
77+
exit 1
78+
fi
79+
echo "(*) Performing single-user install..."
80+
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"
81+
# Install per https://nixos.org/manual/nix/stable/installation/installing-binary.html#single-user-installation
82+
mkdir -p /nix
83+
chown ${USERNAME} /nix ${tmpdir}
84+
su ${USERNAME} -c "sh \"${tmpdir}/install-nix\" --no-daemon --no-modify-profile"
85+
# nix installer does not update ~/.bashrc, and USER may or may not be defined, so update rc/profile files directly to handle that
86+
snippet='
87+
if [ "${PATH#*$HOME/.nix-profile/bin}" = "${PATH}" ]; then if [ -z "$USER" ]; then USER=$(whoami); fi; . $HOME/.nix-profile/etc/profile.d/nix.sh; fi
88+
'
89+
update_rc_file "$home_dir/.bashrc" "${snippet}"
90+
update_rc_file "$home_dir/.zshenv" "${snippet}"
91+
update_rc_file "$home_dir/.profile" "${snippet}"
92+
fi
93+
rm -rf "${tmpdir}" "/tmp/tmp-gnupg"
94+
fi
95+
96+
# Set nix config
97+
mkdir -p /etc/nix
98+
create_or_update_file /etc/nix/nix.conf 'sandbox = false'
99+
if [ ! -z "${FLAKEURI}" ] && [ "${FLAKEURI}" != "none" ]; then
100+
create_or_update_file /etc/nix/nix.conf 'experimental-features = nix-command flakes'
101+
fi
102+
103+
# Create entrypoint if needed
104+
if [ ! -e "/usr/local/share/nix-entrypoint.sh" ]; then
105+
if [ "${MULTIUSER}" = "true" ]; then
106+
echo "(*) Setting up entrypoint..."
107+
cp -f nix-entrypoint.sh /usr/local/share/
108+
else
109+
echo -e '#!/bin/bash\nexec "$@"' > /usr/local/share/nix-entrypoint.sh
110+
fi
111+
chmod +x /usr/local/share/nix-entrypoint.sh
112+
fi
113+
114+
# Install packages, flakes, etc if specified
115+
chmod +x,o+r ${FEATURE_DIR} ${FEATURE_DIR}/post-install-steps.sh
116+
if [ "${MULTIUSER}" = "true" ]; then
117+
/usr/local/share/nix-entrypoint.sh
118+
su ${USERNAME} -c "
119+
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
120+
${FEATURE_DIR}/post-install-steps.sh
121+
"
122+
else
123+
su ${USERNAME} -c "
124+
. \$HOME/.nix-profile/etc/profile.d/nix.sh
125+
${FEATURE_DIR}/post-install-steps.sh
126+
"
127+
fi
128+
129+
echo "Done!"

src/nix/nix-entrypoint.sh

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/bin/bash
2+
# Attempt to start daemon
3+
set +e
4+
if ! pidof nix-daemon > /dev/null 2>&1; then
5+
start_ok=false
6+
if [ "$(id -u)" = "0" ]; then
7+
( . /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 ) &
8+
if [ "$?" = "0" ]; then
9+
start_ok=true
10+
fi
11+
elif type sudo > /dev/null 2>&1; then
12+
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' &
13+
if [ "$?" = "0" ]; then
14+
start_ok=true
15+
fi
16+
fi
17+
if [ "${start_ok}" = "false" ]; then
18+
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 &"'
19+
fi
20+
fi
21+
exec "$@"

src/nix/post-install-steps.sh

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/bin/bash
2+
set -e
3+
echo "(*) Executing post-installation steps..."
4+
5+
# Install list of packages in profile if specified.
6+
if [ ! -z "${PACKAGES}" ] && [ "${PACKAGES}" != "none" ]; then
7+
echo "Installing packages \"${PACKAGES}\" in profile..."
8+
nix-env --install ${PACKAGES}
9+
fi
10+
11+
# Install Nix flake in profile if specified
12+
if [ ! -z "${FLAKEURI}" ] && [ "${FLAKEURI}" != "none" ]; then
13+
echo "Installing flake ${FLAKEURI} in profile..."
14+
nix profile install "${FLAKEURI}"
15+
fi
16+
17+
nix-collect-garbage --delete-old
18+
nix-store --optimise

0 commit comments

Comments
 (0)