1#!/usr/bin/env nix-shell
2#! nix-shell -i bash -p coreutils gnugrep gnused
3
4################################################################################
5# nix-diff.sh #
6################################################################################
7# This script "diffs" Nix profile generations. #
8# #
9# Example: #
10################################################################################
11# > nix-diff.sh 90 92 #
12# + gnumake-4.2.1 #
13# + gnumake-4.2.1-doc #
14# - htmldoc-1.8.29 #
15################################################################################
16# The example shows that as of generation 92 and since generation 90, #
17# gnumake-4.2.1 and gnumake-4.2.1-doc have been installed, while #
18# htmldoc-1.8.29 has been removed. #
19# #
20# The example above shows the default, minimal output mode of this script. #
21# For more features, run `nix-diff.sh -h` for usage instructions. #
22################################################################################
23
24usage() {
25 cat <<EOF
26usage: nix-diff.sh [-h | [-p profile | -s] [-q] [-l] [range]]
27-h: print this message before exiting
28-q: list the derivations installed in the parent generation
29-l: diff every available intermediate generation between parent and
30 child
31-p profile: specify the Nix profile to use
32 * defaults to ~/.nix-profile
33-s: use the system profile
34 * equivalent to: -p /nix/var/nix/profiles/system
35profile: * should be something like /nix/var/nix/profiles/default, not a
36 generation link like /nix/var/nix/profiles/default-2-link
37range: the range of generations to diff
38 * the following patterns are allowed, where A, B, and N are positive
39 integers, and G is the currently active generation:
40 A..B => diffs from generation A to generation B
41 ~N => diffs from the Nth newest generation (older than G) to G
42 A => diffs from generation A to G
43 * defaults to ~1
44EOF
45}
46
47usage_tip() {
48 echo 'run `nix-diff.sh -h` for usage instructions' >&2
49 exit 1
50}
51
52while getopts :hqlp:s opt; do
53 case $opt in
54 h)
55 usage
56 exit
57 ;;
58 q)
59 opt_query=1
60 ;;
61 l)
62 opt_log=1
63 ;;
64 p)
65 opt_profile=$OPTARG
66 ;;
67 s)
68 opt_profile=/nix/var/nix/profiles/system
69 ;;
70 \?)
71 echo "error: invalid option -$OPTARG" >&2
72 usage_tip
73 ;;
74 esac
75done
76shift $((OPTIND-1))
77
78if [ -n "$opt_profile" ]; then
79 if ! [ -L "$opt_profile" ]; then
80 echo "error: expecting \`$opt_profile\` to be a symbolic link" >&2
81 usage_tip
82 fi
83else
84 opt_profile=$(readlink ~/.nix-profile)
85 if (( $? != 0 )); then
86 echo 'error: unable to dereference `~/.nix-profile`' >&2
87 echo 'specify the profile manually with the `-p` flag' >&2
88 usage_tip
89 fi
90fi
91
92list_gens() {
93 nix-env -p "$opt_profile" --list-generations \
94 | sed -r 's:^\s*::' \
95 | cut -d' ' -f1
96}
97
98current_gen() {
99 nix-env -p "$opt_profile" --list-generations \
100 | grep -E '\(current\)\s*$' \
101 | sed -r 's:^\s*::' \
102 | cut -d' ' -f1
103}
104
105neg_gen() {
106 local i=0 from=$1 n=$2 tmp
107 for gen in $(list_gens | sort -rn); do
108 if ((gen < from)); then
109 tmp=$gen
110 ((i++))
111 ((i == n)) && break
112 fi
113 done
114 if ((i < n)); then
115 echo -n "error: there aren't $n generation(s) older than" >&2
116 echo " generation $from" >&2
117 return 1
118 fi
119 echo $tmp
120}
121
122match() {
123 argv=("$@")
124 for i in $(seq $(($#-1))); do
125 if grep -E "^${argv[$i]}\$" <(echo "$1") >/dev/null; then
126 echo $i
127 return
128 fi
129 done
130 echo 0
131}
132
133case $(match "$1" '' '[0-9]+' '[0-9]+\.\.[0-9]+' '~[0-9]+') in
134 1)
135 diffTo=$(current_gen)
136 diffFrom=$(neg_gen $diffTo 1)
137 (($? == 1)) && usage_tip
138 ;;
139 2)
140 diffFrom=$1
141 diffTo=$(current_gen)
142 ;;
143 3)
144 diffFrom=${1%%.*}
145 diffTo=${1##*.}
146 ;;
147 4)
148 diffTo=$(current_gen)
149 diffFrom=$(neg_gen $diffTo ${1#*~})
150 (($? == 1)) && usage_tip
151 ;;
152 0)
153 echo 'error: invalid invocation' >&2
154 usage_tip
155 ;;
156esac
157
158dirA="${opt_profile}-${diffFrom}-link"
159dirB="${opt_profile}-${diffTo}-link"
160
161declare -a temp_files
162temp_length() {
163 echo -n ${#temp_files[@]}
164}
165temp_make() {
166 temp_files[$(temp_length)]=$(mktemp)
167}
168temp_clean() {
169 rm -f ${temp_files[@]}
170}
171temp_name() {
172 echo -n "${temp_files[$(($(temp_length)-1))]}"
173}
174trap 'temp_clean' EXIT
175
176temp_make
177versA=$(temp_name)
178refs=$(nix-store -q --references "$dirA")
179(( $? != 0 )) && exit 1
180echo "$refs" \
181 | grep -v env-manifest.nix \
182 | sort \
183 > "$versA"
184
185print_tag() {
186 local gen=$1
187 nix-env -p "$opt_profile" --list-generations \
188 | grep -E "^\s*${gen}" \
189 | sed -r 's:^\s*::' \
190 | sed -r 's:\s*$::'
191}
192
193if [ -n "$opt_query" ]; then
194 print_tag $diffFrom
195 cat "$versA" \
196 | sed -r 's:^[^-]+-(.*)$: \1:'
197
198 print_line=1
199fi
200
201if [ -n "$opt_log" ]; then
202 gens=$(for gen in $(list_gens); do
203 ((diffFrom < gen && gen < diffTo)) && echo $gen
204 done)
205 # Force the $diffTo generation to be included in this list, instead of using
206 # `gen <= diffTo` in the preceding loop, so we encounter an error upon the
207 # event of its nonexistence.
208 gens=$(echo "$gens"
209 echo $diffTo)
210else
211 gens=$diffTo
212fi
213
214temp_make
215add=$(temp_name)
216temp_make
217rem=$(temp_name)
218temp_make
219out=$(temp_name)
220
221for gen in $gens; do
222
223 [ -n "$print_line" ] && echo
224
225 temp_make
226 versB=$(temp_name)
227
228 dirB="${opt_profile}-${gen}-link"
229 refs=$(nix-store -q --references "$dirB")
230 (( $? != 0 )) && exit 1
231 echo "$refs" \
232 | grep -v env-manifest.nix \
233 | sort \
234 > "$versB"
235
236 in=$(comm -3 -1 "$versA" "$versB")
237 sed -r 's:^[^-]*-(.*)$:\1+:' <(echo "$in") \
238 | sort -f \
239 > "$add"
240
241 un=$(comm -3 -2 "$versA" "$versB")
242 sed -r 's:^[^-]*-(.*)$:\1-:' <(echo "$un") \
243 | sort -f \
244 > "$rem"
245
246 cat "$rem" "$add" \
247 | sort -f \
248 | sed -r 's:(.*)-$:- \1:' \
249 | sed -r 's:(.*)\+$:\+ \1:' \
250 | grep -v '^$' \
251 > "$out"
252
253 if [ -n "$opt_query" -o -n "$opt_log" ]; then
254
255 lines=$(wc -l "$out" | cut -d' ' -f1)
256 tag=$(print_tag "$gen")
257 (( $? != 0 )) && exit 1
258 if [ $lines -eq 0 ]; then
259 echo "$tag (no change)"
260 else
261 echo "$tag"
262 fi
263 cat "$out" \
264 | sed 's:^: :'
265
266 print_line=1
267
268 else
269 echo "diffing from generation $diffFrom to $diffTo"
270 cat "$out"
271 fi
272
273 versA=$versB
274
275done
276
277exit 0