diff options
author | Chuck Lantz <clantz@microsoft.com> | 2022-11-01 20:02:02 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-11-01 20:02:02 +0300 |
commit | 0752a52b0c8ac76448666822938dce8f4d55bb69 (patch) | |
tree | 39abf9124899027643a68e5d33afc78f92ecedd9 | |
parent | 748be9320c99cbf6e6a2885d3b5dcc39e7ad3b78 (diff) |
Nix feature (#228)
-rw-r--r-- | .github/workflows/test-all.yaml | 2 | ||||
-rw-r--r-- | .github/workflows/test-pr.yaml | 1 | ||||
-rw-r--r-- | src/nix/NOTES.md | 28 | ||||
-rw-r--r-- | src/nix/devcontainer-feature.json | 37 | ||||
-rwxr-xr-x | src/nix/install.sh | 129 | ||||
-rwxr-xr-x | src/nix/nix-entrypoint.sh | 21 | ||||
-rwxr-xr-x | src/nix/post-install-steps.sh | 18 | ||||
-rwxr-xr-x | src/nix/utils.sh | 300 | ||||
-rwxr-xr-x | test/nix/flake.sh | 31 | ||||
-rwxr-xr-x | test/nix/multi-user-install.sh | 32 | ||||
-rwxr-xr-x | test/nix/os-alpine.sh | 34 | ||||
-rwxr-xr-x | test/nix/os-debian.sh | 22 | ||||
-rwxr-xr-x | test/nix/os-fedora.sh | 22 | ||||
-rwxr-xr-x | test/nix/os-rockylinux.sh | 22 | ||||
-rwxr-xr-x | test/nix/os-ubuntu.sh | 22 | ||||
-rwxr-xr-x | test/nix/packages.sh | 35 | ||||
-rw-r--r-- | test/nix/scenarios.json | 93 | ||||
-rwxr-xr-x | test/nix/single-user-install.sh | 40 | ||||
-rwxr-xr-x | test/nix/test.sh | 34 | ||||
-rwxr-xr-x | test/nix/version.sh | 38 |
20 files changed, 961 insertions, 0 deletions
diff --git a/.github/workflows/test-all.yaml b/.github/workflows/test-all.yaml index 6d2ba07..28bf5e7 100644 --- a/.github/workflows/test-all.yaml +++ b/.github/workflows/test-all.yaml @@ -38,6 +38,7 @@ jobs: "rust", "sshd", "terraform", + "nix", ] baseImage: [ @@ -88,6 +89,7 @@ jobs: "rust", "sshd", "terraform", + "nix", ] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/test-pr.yaml b/.github/workflows/test-pr.yaml index 8d21b03..f7ad2f0 100644 --- a/.github/workflows/test-pr.yaml +++ b/.github/workflows/test-pr.yaml @@ -38,6 +38,7 @@ jobs: rust: ./**/rust/** sshd: ./**/sshd/** terraform: ./**/terraform/** + nix: ./**/nix/** test: needs: [detect-changes] diff --git a/src/nix/NOTES.md b/src/nix/NOTES.md new file mode 100644 index 0000000..f261964 --- /dev/null +++ b/src/nix/NOTES.md @@ -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`.
\ No newline at end of file diff --git a/src/nix/devcontainer-feature.json b/src/nix/devcontainer-feature.json new file mode 100644 index 0000000..ffd3dc9 --- /dev/null +++ b/src/nix/devcontainer-feature.json @@ -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" +}
\ No newline at end of file diff --git a/src/nix/install.sh b/src/nix/install.sh new file mode 100755 index 0000000..2fc4367 --- /dev/null +++ b/src/nix/install.sh @@ -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!"
\ No newline at end of file diff --git a/src/nix/nix-entrypoint.sh b/src/nix/nix-entrypoint.sh new file mode 100755 index 0000000..cac3e63 --- /dev/null +++ b/src/nix/nix-entrypoint.sh @@ -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 "$@" diff --git a/src/nix/post-install-steps.sh b/src/nix/post-install-steps.sh new file mode 100755 index 0000000..aa46679 --- /dev/null +++ b/src/nix/post-install-steps.sh @@ -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 diff --git a/src/nix/utils.sh b/src/nix/utils.sh new file mode 100755 index 0000000..68cdd9d --- /dev/null +++ b/src/nix/utils.sh @@ -0,0 +1,300 @@ +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + export DEBIAN_FRONTEND=noninteractive + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Function to run apt-get if command exists +apt_get_update_if_exists() +{ + if type apt-get > /dev/null 2>&1; then + apt-get update + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if type dpkg > /dev/null 2>&1 && dpkg -s $1 > /dev/null 2>&1; then + return 0 + elif type apk > /dev/null 2>&1 && apk -e info $2 > /dev/null 2>&1; then + return 0 + elif type rpm > /dev/null 2>&1 && rpm -q $3 > /dev/null 2>&1; then + return 0 + else + echo "Unable to find package manager to check for packages." + exit 1 + fi + install_packages "$@" + return $? +} + +# Checks if command exists, installs it if not +# check_command <command> "<apt packages to install>" "<apk packages to install>" "<dnf/yum packages to install>" +check_command() { + command_to_check=$1 + shift + if type "${command_to_check}" > /dev/null 2>&1; then + return 0 + fi + install_packages "$@" + return $? +} + +# Installs packages using the appropriate package manager (apt, apk, dnf, or yum) +# install_packages "<apt packages to install>" "<apk packages to install>" "<dnf/yum packages to install>" +install_packages() { + if type apt-get > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends $1 + elif type apk > /dev/null 2>&1; then + apk add $2 + elif type dnf > /dev/null 2>&1; then + dnf install -y $3 + elif type yum > /dev/null 2>&1; then + yum install -y $3 + else + echo "Unable to find package manager to install ${command_to_check}" + exit 1 + fi +} + +# If in automatic mode, determine if a user already exists, if not use root +detect_user() { + local user_variable_name=${1:-username} + local possible_users=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + if [ "${!user_variable_name}" = "auto" ] || [ "${!user_variable_name}" = "automatic" ]; then + declare -g ${user_variable_name}="" + for current_user in ${possible_users[@]}; do + if id -u "${current_user}" > /dev/null 2>&1; then + declare -g ${user_variable_name}="${current_user}" + break + fi + done + fi + if [ "${!user_variable_name}" = "" ] || [ "${!user_variable_name}" = "none" ] || ! id -u "${!user_variable_name}" > /dev/null 2>&1; then + declare -g ${user_variable_name}=root + fi +} + +# Import the specified key in a variable name passed in as +receive_gpg_keys() { + local keys=${!1} + local keyring_args="" + if [ ! -z "$2" ]; then + mkdir -p "$(dirname \"$2\")" + keyring_args="--no-default-keyring --keyring $2" + fi + + # Use a temporary locaiton for gpg keys to avoid polluting image + export GNUPGHOME="/tmp/tmp-gnupg" + mkdir -p ${GNUPGHOME} + chmod 700 ${GNUPGHOME} + echo -e "disable-ipv6\n${GPG_KEY_SERVERS}" > ${GNUPGHOME}/dirmngr.conf + # GPG key download sometimes fails for some reason and retrying fixes it. + local retry_count=0 + local gpg_ok="false" + set +e + until [ "${gpg_ok}" = "true" ] || [ "${retry_count}" -eq "5" ]; + do + echo "(*) Downloading GPG key..." + ( echo "${keys}" | xargs -n 1 gpg -q ${keyring_args} --recv-keys) 2>&1 && gpg_ok="true" + if [ "${gpg_ok}" != "true" ]; then + echo "(*) Failed getting key, retring in 10s..." + (( retry_count++ )) + sleep 10s + fi + done + set -e + if [ "${gpg_ok}" = "false" ]; then + echo "(!) Failed to get gpg key." + exit 1 + fi +} + +# Figure out correct version of a three part version number is not passed +find_version_from_git_tags() { + local variable_name=$1 + local requested_version=${!variable_name} + if [ "${requested_version}" = "none" ]; then return; fi + local repository=$2 + # Normally a "v" is used before the version number, but support alternate cases + local prefix=${3:-"tags/v"} + # Some repositories use "_" instead of "." for version number part separation, support that + local separator=${4:-"."} + # Some tools release versions that omit the last digit (e.g. go) + local last_part_optional=${5:-"false"} + # Some repositories may have tags that include a suffix (e.g. actions/node-versions) + local version_suffix_regex=$6 + + local escaped_separator=${separator//./\\.} + local break_fix_digit_regex + if [ "${last_part_optional}" = "true" ]; then + break_fix_digit_regex="(${escaped_separator}[0-9]+)?" + else + break_fix_digit_regex="${escaped_separator}[0-9]+" + fi + local version_regex="[0-9]+${escaped_separator}[0-9]+${break_fix_digit_regex}${version_suffix_regex//./\\.}" + # If we're passed a matching version number, just return it, otherwise look for a version + if ! echo "${requested_version}" | grep -E "^${versionMatchRegex}$" > /dev/null 2>&1; then + local version_list="$(git ls-remote --tags ${repository} | grep -oP "${prefix}\\K${version_regex}$" | tr -d ' ' | tr "${separator}" "." | sort -rV)" + if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then + declare -g ${variable_name}="$(echo "${version_list}" | head -n 1)" + else + set +e + declare -g ${variable_name}="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|${version_suffix_regex//./\\.}|$)")" + set -e + fi + if [ -z "${!variable_name}" ] || ! echo "${version_list}" | grep "^${!variable_name//./\\.}$" > /dev/null 2>&1; then + echo -e "Invalid ${variable_name} value: ${requested_version}\nValid values:\n${version_list}" >&2 + exit 1 + fi + fi + echo "Adjusted ${variable_name}=${!variable_name}" +} + +# Soft version matching that resolves a version for a given package in the *current apt-cache* +# Return value is stored in first argument (the unprocessed version) +apt_cache_version_soft_match() { + # Version + local variable_name="$1" + local requested_version=${!variable_name} + # Package Name + local package_name="$2" + # Exit on no match? + local exit_on_no_match="${3:-true}" + + # Ensure we've exported useful variables + . /etc/os-release + local architecture="$(dpkg --print-architecture)" + + dot_escaped="${requested_version//./\\.}" + dot_plus_escaped="${dot_escaped//+/\\+}" + # Regex needs to handle debian package version number format: https://www.systutorials.com/docs/linux/man/5-deb-version/ + version_regex="^(.+:)?${dot_plus_escaped}([\\.\\+ ~:-]|$)" + set +e # Don't exit if finding version fails - handle gracefully + fuzzy_version="$(apt-cache madison ${package_name} | awk -F"|" '{print $2}' | sed -e 's/^[ \t]*//' | grep -E -m 1 "${version_regex}")" + set -e + if [ -z "${fuzzy_version}" ]; then + echo "(!) No full or partial for package \"${package_name}\" match found in apt-cache for \"${requested_version}\" on OS ${ID} ${VERSION_CODENAME} (${architecture})." + + if $exit_on_no_match; then + echo "Available versions:" + apt-cache madison ${package_name} | awk -F"|" '{print $2}' | grep -oP '^(.+:)?\K.+' + exit 1 # Fail entire script + else + echo "Continuing to fallback method (if available)" + return 1; + fi + fi + + # Globally assign fuzzy_version to this value + # Use this value as the return value of this function + declare -g ${variable_name}="=${fuzzy_version}" + echo "${variable_name}=${!variable_name}" +} + +# Checks if a marker file exists with the correct contents +# check_marker <marker path> [argument to be validated]... +check_marker() { + local marker_path="$1" + shift + local verifier_string="$(echo "$@")" + if [ -e "${marker_path}" ] && [ "${verifier_string}" = "$(cat ${marker_path})" ]; then + return 1 + else + return 0 + fi +} + +# Updates marker for future checking +# update_marker <marker path> [argument to be validated]... +update_marker() { + local marker_path="$1" + shift + mkdir -p "$(dirname "${marker_path}")" + echo "$(echo "$@")" > "${marker_path}" +} + +# run_if_exists <command> <command arguments>... +run_if_exists() { + if [ -e "$1" ]; then + "$@" + fi +} + +# run_as_user_if_exists <username> <command> <command arguments>... +run_as_user_if_exists() { + local username=$1 + shift + if [ -e "$1" ]; then + local command_string="$@" + su "${username}" -c "${command_string//"/\\"}" + fi +} + +# symlink_if_ne <source> <target> +symlink_if_ne() { + if [ ! -e "$2" ]; then + ln -s "$1" "$2" + fi +} + +# Update a rc/profile file if it exists and string is not already present +update_rc_file() { + # see if folder containing file exists + local rc_file_folder="$(dirname "$1")" + if [ ! -d "${rc_file_folder}" ]; then + echo "${rc_file_folder} does not exist. Skipping update of $1." + elif [ ! -e "$1" ] || [[ "$(cat "$1")" != *"$2"* ]]; then + echo "$2" >> "$1" + fi +} + +# Update a file if with string if not already present +# create_or_update_file <file> <string> +create_or_update_file() { + if [ ! -e "$1" ] || [[ "$(cat "$1")" != *"$2"* ]]; then + echo "$2" >> "$1" + fi +} + +# Use semver logic to decrement a version number then look for the closest match +find_prev_version_from_git_tags() { + local variable_name=$1 + local current_version=${!variable_name} + local repository=$2 + # Normally a "v" is used before the version number, but support alternate cases + local prefix=${3:-"tags/v"} + # Some repositories use "_" instead of "." for version number part separation, support that + local separator=${4:-"."} + # Some tools release versions that omit the last digit (e.g. go) + local last_part_optional=${5:-"false"} + # Some repositories may have tags that include a suffix (e.g. actions/node-versions) + local version_suffix_regex=$6 + # Try one break fix version number less if we get a failure. Use "set +e" since "set -e" can cause failures in valid scenarios. + set +e + major="$(echo "${current_version}" | grep -oE '^[0-9]+' || echo '')" + minor="$(echo "${current_version}" | grep -oP '^[0-9]+\.\K[0-9]+' || echo '')" + breakfix="$(echo "${current_version}" | grep -oP '^[0-9]+\.[0-9]+\.\K[0-9]+' 2>/dev/null || echo '')" + set -e + # Handle situations like Go's odd version pattern where "0" releases omit the last part + if [ "${breakfix}" = "" ] || [ "${breakfix}" = "0" ]; then + ((minor=minor-1)) + declare -g ${variable_name}="${major}.${minor}" + # Look for latest version from previous minor release + find_version_from_git_tags "${variable_name}" "${repository}" "${prefix}" "${separator}" "${last_part_optional}" + else + ((breakfix=breakfix-1)) + if [ "${breakfix}" = "0" ] && [ "${last_part_optional}" = "true" ]; then + declare -g ${variable_name}="${major}.${minor}" + else + declare -g ${variable_name}="${major}.${minor}.${breakfix}" + fi + fi +}
\ No newline at end of file diff --git a/test/nix/flake.sh b/test/nix/flake.sh new file mode 100755 index 0000000..03fb667 --- /dev/null +++ b/test/nix/flake.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set -e + +# Optional: Import test library bundled with the devcontainer CLI +source dev-container-features-test-lib + +uid="$(id -u)" +echo "Current user UID is ${uid}." +if [ "${uid}" != "1000" ]; then + echo "Current user UID was adjusted." +fi +set +e +vscode_uid="$(id -u vscode)" +set -e +if [ "${vscode_uid}" != "" ]; then + echo "User vscode UID is ${vscode_uid}." + if [ "${vscode_uid}" != "1000" ]; then + echo "User vscode UID was adjusted." + fi +fi +nix_uid="$(stat /nix -c "%u")" +echo "/nix UID is ${nix_uid}." + +# Feature-specific tests +# The 'check' command comes from the dev-container-features-test-lib. +check "nix" type nix +check "hello" type hello + +# Report result +# If any of the checks above exited with a non-zero exit code, the test will fail. +reportResults &2>1
\ No newline at end of file diff --git a/test/nix/multi-user-install.sh b/test/nix/multi-user-install.sh new file mode 100755 index 0000000..08c64f1 --- /dev/null +++ b/test/nix/multi-user-install.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -e + +# Optional: Import test library bundled with the devcontainer CLI +source dev-container-features-test-lib + +uid="$(id -u)" +echo "Current user UID is ${uid}." +if [ "${uid}" != "1000" ]; then + echo "Current user UID was adjusted." +fi +set +e +vscode_uid="$(id -u vscode)" +set -e +if [ "${vscode_uid}" != "" ]; then + echo "User vscode UID is ${vscode_uid}." + if [ "${vscode_uid}" != "1000" ]; then + echo "User vscode UID was adjusted." + fi +fi +nix_uid="$(stat /nix -c "%u")" +echo "/nix UID is ${nix_uid}." + +# Feature-specific tests +# The 'check' command comes from the dev-container-features-test-lib. +check "nix-env" type nix-env +check "install" nix-env --install vim +check "vim_installed" type vim + +# Report result +# If any of the checks above exited with a non-zero exit code, the test will fail. +reportResults &2>1
\ No newline at end of file diff --git a/test/nix/os-alpine.sh b/test/nix/os-alpine.sh new file mode 100755 index 0000000..88ca51f --- /dev/null +++ b/test/nix/os-alpine.sh @@ -0,0 +1,34 @@ +#!/bin/bash +set -e + +# Optional: Import test library bundled with the devcontainer CLI +source dev-container-features-test-lib + +uid="$(id -u)" +echo "Current user UID is ${uid}." +if [ "${uid}" != "1000" ]; then + echo "Current user UID was adjusted." +fi +set +e +vscode_uid="$(id -u vscode)" +set -e +if [ "${vscode_uid}" != "" ]; then + echo "User vscode UID is ${vscode_uid}." + if [ "${vscode_uid}" != "1000" ]; then + echo "User vscode UID was adjusted." + fi +fi +nix_uid="$(stat /nix -c "%u")" +echo "/nix UID is ${nix_uid}." + +cat /etc/os-release + +# Feature-specific tests +# The 'check' command comes from the dev-container-features-test-lib. +check "nix-env" type nix-env +check "install" nix-env --install vim +check "vim_installed" type vim + +# Report result +# If any of the checks above exited with a non-zero exit code, the test will fail. +reportResults &2>1
\ No newline at end of file diff --git a/test/nix/os-debian.sh b/test/nix/os-debian.sh new file mode 100755 index 0000000..fe2b978 --- /dev/null +++ b/test/nix/os-debian.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e + +# Optional: Import test library bundled with the devcontainer CLI +source dev-container-features-test-lib + +uid="$(id -u)" +echo "Current user UID is ${uid}." +nix_uid="$(stat /nix -c "%u")" +echo "/nix UID is ${nix_uid}." + +cat /etc/os-release + +# Feature-specific tests +# The 'check' command comes from the dev-container-features-test-lib. +check "nix-env" type nix-env +check "install" nix-env --install vim +check "vim_installed" type vim + +# Report result +# If any of the checks above exited with a non-zero exit code, the test will fail. +reportResults &2>1
\ No newline at end of file diff --git a/test/nix/os-fedora.sh b/test/nix/os-fedora.sh new file mode 100755 index 0000000..fe2b978 --- /dev/null +++ b/test/nix/os-fedora.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e + +# Optional: Import test library bundled with the devcontainer CLI +source dev-container-features-test-lib + +uid="$(id -u)" +echo "Current user UID is ${uid}." +nix_uid="$(stat /nix -c "%u")" +echo "/nix UID is ${nix_uid}." + +cat /etc/os-release + +# Feature-specific tests +# The 'check' command comes from the dev-container-features-test-lib. +check "nix-env" type nix-env +check "install" nix-env --install vim +check "vim_installed" type vim + +# Report result +# If any of the checks above exited with a non-zero exit code, the test will fail. +reportResults &2>1
\ No newline at end of file diff --git a/test/nix/os-rockylinux.sh b/test/nix/os-rockylinux.sh new file mode 100755 index 0000000..fe2b978 --- /dev/null +++ b/test/nix/os-rockylinux.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e + +# Optional: Import test library bundled with the devcontainer CLI +source dev-container-features-test-lib + +uid="$(id -u)" +echo "Current user UID is ${uid}." +nix_uid="$(stat /nix -c "%u")" +echo "/nix UID is ${nix_uid}." + +cat /etc/os-release + +# Feature-specific tests +# The 'check' command comes from the dev-container-features-test-lib. +check "nix-env" type nix-env +check "install" nix-env --install vim +check "vim_installed" type vim + +# Report result +# If any of the checks above exited with a non-zero exit code, the test will fail. +reportResults &2>1
\ No newline at end of file diff --git a/test/nix/os-ubuntu.sh b/test/nix/os-ubuntu.sh new file mode 100755 index 0000000..fe2b978 --- /dev/null +++ b/test/nix/os-ubuntu.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e + +# Optional: Import test library bundled with the devcontainer CLI +source dev-container-features-test-lib + +uid="$(id -u)" +echo "Current user UID is ${uid}." +nix_uid="$(stat /nix -c "%u")" +echo "/nix UID is ${nix_uid}." + +cat /etc/os-release + +# Feature-specific tests +# The 'check' command comes from the dev-container-features-test-lib. +check "nix-env" type nix-env +check "install" nix-env --install vim +check "vim_installed" type vim + +# Report result +# If any of the checks above exited with a non-zero exit code, the test will fail. +reportResults &2>1
\ No newline at end of file diff --git a/test/nix/packages.sh b/test/nix/packages.sh new file mode 100755 index 0000000..ad896e9 --- /dev/null +++ b/test/nix/packages.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -e + +# Optional: Import test library bundled with the devcontainer CLI +source dev-container-features-test-lib + +uid="$(id -u)" +echo "Current user UID is ${uid}." +if [ "${uid}" != "1000" ]; then + echo "Current user UID was adjusted." +fi +set +e +vscode_uid="$(id -u vscode)" +set -e +if [ "${vscode_uid}" != "" ]; then + echo "User vscode UID is ${vscode_uid}." + if [ "${vscode_uid}" != "1000" ]; then + echo "User vscode UID was adjusted." + fi +fi +nix_uid="$(stat /nix -c "%u")" +echo "/nix UID is ${nix_uid}." + +cat /etc/os-release + +# Feature-specific tests +# The 'check' command comes from the dev-container-features-test-lib. +check "nix-env" type nix-env +check "vim_installed" type vim +check "node_installed" type node +check "yarn_installed" type yarn + +# Report result +# If any of the checks above exited with a non-zero exit code, the test will fail. +reportResults &2>1
\ No newline at end of file diff --git a/test/nix/scenarios.json b/test/nix/scenarios.json new file mode 100644 index 0000000..72ad993 --- /dev/null +++ b/test/nix/scenarios.json @@ -0,0 +1,93 @@ +{ + "single-user-install": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "remoteUser": "vscode", + "features": { + "nix": { + "multiUser": false + } + } + }, + "version": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "remoteUser": "vscode", + "features": { + "nix": { + "multiUser": false, + "version": "2.10" + } + } + }, + "multi-user-install": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "remoteUser": "vscode", + "features": { + "nix": { + "multiUser": true + } + } + }, + "os-ubuntu": { + "image": "ubuntu", + "remoteUser": "root", + "features": { + "nix": { + "multiUser": true + } + } + }, + "os-debian": { + "image": "debian", + "remoteUser": "root", + "features": { + "nix": { + "multiUser": true + } + } + }, + "os-rockylinux": { + "image": "rockylinux:9", + "remoteUser": "root", + "features": { + "nix": { + "multiUser": true + } + } + }, + "os-fedora": { + "image": "fedora", + "remoteUser": "root", + "features": { + "nix": { + "multiUser": true + } + } + }, + "os-alpine": { + "image": "mcr.microsoft.com/devcontainers/base:alpine", + "remoteUser": "vscode", + "features": { + "nix": { + "multiUser": true + } + } + }, + "packages": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "remoteUser": "vscode", + "features": { + "nix": { + "packages": "nodejs,vim,yarn" + } + } + }, + "flake": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "remoteUser": "vscode", + "features": { + "nix": { + "flakeUri": "github:nixos/nixpkgs/nixpkgs-unstable#hello" + } + } + } +}
\ No newline at end of file diff --git a/test/nix/single-user-install.sh b/test/nix/single-user-install.sh new file mode 100755 index 0000000..c7dba77 --- /dev/null +++ b/test/nix/single-user-install.sh @@ -0,0 +1,40 @@ +#!/bin/bash +set -e + +# Optional: Import test library bundled with the devcontainer CLI +source dev-container-features-test-lib + +uid="$(id -u)" +echo "Current user UID is ${uid}." +if [ "${uid}" != "1000" ]; then + echo "Current user UID was adjusted." +fi +set +e +vscode_uid="$(id -u vscode)" +set -e +if [ "${vscode_uid}" != "" ]; then + echo "User vscode UID is ${vscode_uid}." + if [ "${vscode_uid}" != "1000" ]; then + echo -e "User vscode UID was adjusted.\nWARNING: This scenario is not expected to work, so adjusting owner on Nix." + sudo chown -R vscode /nix + fi +fi +nix_uid="$(stat /nix -c "%u")" +echo "/nix UID is ${nix_uid}." +if [ "${nix_uid}" != "${vscode_uid}" ]; then + echo -e "WARNING: User UID does not match /nix owner. This scenario is not expected to work, so adjusting owner of /nix for testing purposes." + sudo chown -R vscode /nix +fi + + +cat /etc/os-release + +# Feature-specific tests +# The 'check' command comes from the dev-container-features-test-lib. +check "nix-env" type nix-env +check "install" nix-env --install vim +check "vim_installed" type vim + +# Report result +# If any of the checks above exited with a non-zero exit code, the test will fail. +reportResults &2>1
\ No newline at end of file diff --git a/test/nix/test.sh b/test/nix/test.sh new file mode 100755 index 0000000..88ca51f --- /dev/null +++ b/test/nix/test.sh @@ -0,0 +1,34 @@ +#!/bin/bash +set -e + +# Optional: Import test library bundled with the devcontainer CLI +source dev-container-features-test-lib + +uid="$(id -u)" +echo "Current user UID is ${uid}." +if [ "${uid}" != "1000" ]; then + echo "Current user UID was adjusted." +fi +set +e +vscode_uid="$(id -u vscode)" +set -e +if [ "${vscode_uid}" != "" ]; then + echo "User vscode UID is ${vscode_uid}." + if [ "${vscode_uid}" != "1000" ]; then + echo "User vscode UID was adjusted." + fi +fi +nix_uid="$(stat /nix -c "%u")" +echo "/nix UID is ${nix_uid}." + +cat /etc/os-release + +# Feature-specific tests +# The 'check' command comes from the dev-container-features-test-lib. +check "nix-env" type nix-env +check "install" nix-env --install vim +check "vim_installed" type vim + +# Report result +# If any of the checks above exited with a non-zero exit code, the test will fail. +reportResults &2>1
\ No newline at end of file diff --git a/test/nix/version.sh b/test/nix/version.sh new file mode 100755 index 0000000..91bf243 --- /dev/null +++ b/test/nix/version.sh @@ -0,0 +1,38 @@ +#!/bin/bash +set -e + +# Optional: Import test library bundled with the devcontainer CLI +source dev-container-features-test-lib + +uid="$(id -u)" +echo "Current user UID is ${uid}." +if [ "${uid}" != "1000" ]; then + echo "Current user UID was adjusted." +fi +set +e +vscode_uid="$(id -u vscode)" +set -e +if [ "${vscode_uid}" != "" ]; then + echo "User vscode UID is ${vscode_uid}." + if [ "${vscode_uid}" != "1000" ]; then + echo "User vscode UID was adjusted." + fi +fi +nix_uid="$(stat /nix -c "%u")" +echo "/nix UID is ${nix_uid}." +if [ "${nix_uid}" != "${vscode_uid}" ]; then + echo -e "WARNING: User UID does not match /nix owner. This scenario is not expected to work, so adjusting owner of /nix for testing purposes." + sudo chown -R vscode /nix +fi + +cat /etc/os-release + +# Feature-specific tests +# The 'check' command comes from the dev-container-features-test-lib. +check "nix-env" type nix-env +check "install" nix-env --install vim +check "vim_installed" type vim + +# Report result +# If any of the checks above exited with a non-zero exit code, the test will fail. +reportResults &2>1
\ No newline at end of file |