1use strict;
2use File::Find;
3use File::Copy;
4use File::Path;
5use File::Basename;
6use File::Slurp;
7
8my $etc = $ARGV[0] or die;
9my $static = "/etc/static";
10
11sub atomicSymlink {
12 my ($source, $target) = @_;
13 my $tmp = "$target.tmp";
14 unlink $tmp;
15 symlink $source, $tmp or return 0;
16 if (rename $tmp, $target) {
17 return 1;
18 } else {
19 unlink $tmp;
20 return 0;
21 }
22}
23
24
25# Atomically update /etc/static to point at the etc files of the
26# current configuration.
27atomicSymlink $etc, $static or die;
28
29# Returns 1 if the argument points to the files in /etc/static. That
30# means either argument is a symlink to a file in /etc/static or a
31# directory with all children being static.
32sub isStatic {
33 my $path = shift;
34
35 if (-l $path) {
36 my $target = readlink $path;
37 return substr($target, 0, length "/etc/static/") eq "/etc/static/";
38 }
39
40 if (-d $path) {
41 opendir DIR, "$path" or return 0;
42 my @names = readdir DIR or die;
43 closedir DIR;
44
45 foreach my $name (@names) {
46 next if $name eq "." || $name eq "..";
47 unless (isStatic("$path/$name")) {
48 return 0;
49 }
50 }
51 return 1;
52 }
53
54 return 0;
55}
56
57# Remove dangling symlinks that point to /etc/static. These are
58# configuration files that existed in a previous configuration but not
59# in the current one. For efficiency, don't look under /etc/nixos
60# (where all the NixOS sources live).
61sub cleanup {
62 if ($File::Find::name eq "/etc/nixos") {
63 $File::Find::prune = 1;
64 return;
65 }
66 if (-l $_) {
67 my $target = readlink $_;
68 if (substr($target, 0, length $static) eq $static) {
69 my $x = "/etc/static/" . substr($File::Find::name, length "/etc/");
70 unless (-l $x) {
71 print STDERR "removing obsolete symlink ‘$File::Find::name’...\n";
72 unlink "$_";
73 }
74 }
75 }
76}
77
78find(\&cleanup, "/etc");
79
80
81# Use /etc/.clean to keep track of copied files.
82my @oldCopied = read_file("/etc/.clean", chomp => 1, err_mode => 'quiet');
83open CLEAN, ">>/etc/.clean";
84
85
86# For every file in the etc tree, create a corresponding symlink in
87# /etc to /etc/static. The indirection through /etc/static is to make
88# switching to a new configuration somewhat more atomic.
89my %created;
90my @copied;
91
92sub link {
93 my $fn = substr $File::Find::name, length($etc) + 1 or next;
94
95 # nixos-enter sets up /etc/resolv.conf as a bind mount, so skip it.
96 if ($fn eq "resolv.conf" and $ENV{'IN_NIXOS_ENTER'}) {
97 return;
98 }
99
100 my $target = "/etc/$fn";
101 File::Path::make_path(dirname $target);
102 $created{$fn} = 1;
103
104 # Rename doesn't work if target is directory.
105 if (-l $_ && -d $target) {
106 if (isStatic $target) {
107 rmtree $target or warn;
108 } else {
109 warn "$target directory contains user files. Symlinking may fail.";
110 }
111 }
112
113 if (-e "$_.mode") {
114 my $mode = read_file("$_.mode"); chomp $mode;
115 if ($mode eq "direct-symlink") {
116 atomicSymlink readlink("$static/$fn"), $target or warn "could not create symlink $target";
117 } else {
118 my $uid = read_file("$_.uid"); chomp $uid;
119 my $gid = read_file("$_.gid"); chomp $gid;
120 copy "$static/$fn", "$target.tmp" or warn;
121 $uid = getpwnam $uid unless $uid =~ /^\+/;
122 $gid = getgrnam $gid unless $gid =~ /^\+/;
123 chown int($uid), int($gid), "$target.tmp" or warn;
124 chmod oct($mode), "$target.tmp" or warn;
125 unless (rename "$target.tmp", $target) {
126 warn "could not create target $target";
127 unlink "$target.tmp";
128 }
129 }
130 push @copied, $fn;
131 print CLEAN "$fn\n";
132 } elsif (-l "$_") {
133 atomicSymlink "$static/$fn", $target or warn "could not create symlink $target";
134 }
135}
136
137find(\&link, $etc);
138
139
140# Delete files that were copied in a previous version but not in the
141# current.
142foreach my $fn (@oldCopied) {
143 if (!defined $created{$fn}) {
144 $fn = "/etc/$fn";
145 print STDERR "removing obsolete file ‘$fn’...\n";
146 unlink "$fn";
147 }
148}
149
150
151# Rewrite /etc/.clean.
152close CLEAN;
153write_file("/etc/.clean", map { "$_\n" } sort @copied);
154
155# Create /etc/NIXOS tag if not exists.
156# When /etc is not on a persistent filesystem, it will be wiped after reboot,
157# so we need to check and re-create it during activation.
158open TAG, ">>/etc/NIXOS";
159close TAG;