summaryrefslogtreecommitdiffstats
path: root/.envrc
blob: 75fb4741a05ae0a4af895a9bc70828582e1e925d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# Load environment variables from `nix-shell` and export it out.
#
# Usage: use_nix [-s <nix-expression>] [-w <path>] [-w <path>] ...
#   -s nix-expression: The nix expression to use for building the shell environment.
#   -w path: watch a file for changes. It can be specified multiple times. The
#      shell specified with -s is automatically watched.
#
#   If no nix-expression were given with -s, it will attempt to find and load
#   the shell using the following files in order: shell.nix and default.nix.
#
# Example:
#   -  use_nix
#   -  use_nix -s shell.nix -w .nixpkgs-version.json
#
# The dependencies pulled by nix-shell are added to Nix's garbage collector
# roots, such that the environment remains persistent.
#
# Nix-shell is invoked only once per environment, and the output is cached for
# better performance. If any of the watched files change, then the environment
# is rebuilt.
#
# To remove old environments, and allow the GC to collect their dependencies:
# rm -f .direnv
#
use_nix() {
	if ! validate_version; then
		echo "This .envrc requires direnv version 2.18.2 or above."
		exit 1
	fi

	# define all local variables
	local shell
	local files_to_watch=()

	local opt OPTARG OPTIND # define vars used by getopts locally
	while getopts ":n:s:w:" opt; do
		case "${opt}" in
			s)
				shell="${OPTARG}"
				files_to_watch=("${files_to_watch[@]}" "${shell}")
				;;
			w)
				files_to_watch=("${files_to_watch[@]}" "${OPTARG}")
				;;
			:)
				fail "Invalid option: $OPTARG requires an argument"
				;;
			\?)
				fail "Invalid option: $OPTARG"
				;;
		esac
	done
	shift $((OPTIND -1))

	if [[ -z "${shell}" ]]; then
		if [[ -f shell.nix ]]; then
			shell=shell.nix
			files_to_watch=("${files_to_watch[@]}" shell.nix)
		elif [[ -f default.nix ]]; then
			shell=default.nix
			files_to_watch=("${files_to_watch[@]}" default.nix)
		else
			fail "ERR: no shell was given"
		fi
	fi

	local f
	for f in "${files_to_watch[@]}"; do
		if ! [[ -f "${f}" ]]; then
			fail "cannot watch file ${f} because it does not exist"
		fi
	done

	# compute the hash of all the files that makes up the development environment
	local env_hash="$(hash_contents "${files_to_watch[@]}")"

	# define the paths
	local dir="$(direnv_layout_dir)"
	local wd="${dir}/wd-${env_hash}"
	local drv="${wd}/env.drv"
	local dump="${wd}/dump.env"

	# Generate the environment if we do not have one generated already.
	if [[ ! -f "${drv}" ]]; then
		mkdir -p "${wd}"

		log_status "use nix: deriving new environment"
		IN_NIX_SHELL=1 nix-instantiate --add-root "${drv}" --indirect "${shell}" > /dev/null
		nix-store -r $(nix-store --query --references "${drv}") --add-root "${wd}/dep" --indirect > /dev/null
		if [[ "${?}" -ne 0 ]] || [[ ! -f "${drv}" ]]; then
			rm -rf "${wd}"
			fail "use nix: was not able to derive the new environment. Please run 'direnv reload' to try again."
		fi

		log_status "use nix: updating cache"
		nix-shell --pure "${drv}" --show-trace --run "$(join_args "$direnv" dump bash)" > "${dump}"
		if [[ "${?}" -ne 0 ]] || [[ ! -f "${dump}" ]] || ! grep -q IN_NIX_SHELL "${dump}"; then
			rm -rf "${wd}"
			fail "use nix: was not able to update the cache of the environment. Please run 'direnv reload' to try again."
		fi
	fi

	# evaluate the dump created by nix-shell earlier, but have to merge the PATH
	# with the current PATH
	# NOTE: we eval the dump here as opposed to direnv_load it because we don't
	# want to persist environment variables coming from the shell at the time of
	# the dump. See https://github.com/direnv/direnv/issues/405 for context.
	local path_backup="${PATH}"
	eval $(cat "${dump}")
	export PATH="${PATH}:${path_backup}"

	# cleanup the environment of variables that are not requried, or are causing issues.
	unset shellHook  # when shellHook is present, then any nix-shell'd script will execute it!

	# watch all the files we were asked to watch for the environment
	for f in "${files_to_watch[@]}"; do
		watch_file "${f}"
	done
}

fail() {
	log_error "${@}"
	exit 1
}

hash_contents() {
	if has md5sum; then
		cat "${@}" | md5sum | cut -c -32
	elif has md5; then
		cat "${@}" | md5 -q
	fi
}

hash_file() {
	if has md5sum; then
		md5sum "${@}" | cut -c -32
	elif has md5; then
		md5 -q "${@}"
	fi
}

validate_version() {
	local version="$("${direnv}" version)"
	local major="$(echo "${version}" | cut -d. -f1)"
	local minor="$(echo "${version}" | cut -d. -f2)"
	local patch="$(echo "${version}" | cut -d. -f3)"

	if [[ "${major}" -gt 2 ]]; then return 0; fi
	if [[ "${major}" -eq 2 ]] && [[ "${minor}" -gt 18 ]]; then return 0; fi
	if [[ "${major}" -eq 2 ]] && [[ "${minor}" -eq 18 ]] && [[ "${patch}" -ge 2 ]]; then return 0; fi
	return 1
}

use_nix -s shell.nix -w .nixpkgs-version.json