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 rename $tmp, $target or return 0;
17 return 1;
18}
19
20
21# Atomically update /etc/static to point at the etc files of the
22# current configuration.
23atomicSymlink $etc, $static or die;
24
25# Returns 1 if the argument points to the files in /etc/static. That
26# means either argument is a symlink to a file in /etc/static or a
27# directory with all children being static.
28sub isStatic {
29 my $path = shift;
30
31 if (-l $path) {
32 my $target = readlink $path;
33 return substr($target, 0, length "/etc/static/") eq "/etc/static/";
34 }
35
36 if (-d $path) {
37 opendir DIR, "$path" or return 0;
38 my @names = readdir DIR or die;
39 closedir DIR;
40
41 foreach my $name (@names) {
42 next if $name eq "." || $name eq "..";
43 unless (isStatic("$path/$name")) {
44 return 0;
45 }
46 }
47 return 1;
48 }
49
50 return 0;
51}
52
53# Remove dangling symlinks that point to /etc/static. These are
54# configuration files that existed in a previous configuration but not
55# in the current one. For efficiency, don't look under /etc/nixos
56# (where all the NixOS sources live).
57sub cleanup {
58 if ($File::Find::name eq "/etc/nixos") {
59 $File::Find::prune = 1;
60 return;
61 }
62 if (-l $_) {
63 my $target = readlink $_;
64 if (substr($target, 0, length $static) eq $static) {
65 my $x = "/etc/static/" . substr($File::Find::name, length "/etc/");
66 unless (-l $x) {
67 print STDERR "removing obsolete symlink ‘$File::Find::name’...\n";
68 unlink "$_";
69 }
70 }
71 }
72}
73
74find(\&cleanup, "/etc");
75
76
77# Use /etc/.clean to keep track of copied files.
78my @oldCopied = read_file("/etc/.clean", chomp => 1, err_mode => 'quiet');
79open CLEAN, ">>/etc/.clean";
80
81
82# For every file in the etc tree, create a corresponding symlink in
83# /etc to /etc/static. The indirection through /etc/static is to make
84# switching to a new configuration somewhat more atomic.
85my %created;
86my @copied;
87
88sub link {
89 my $fn = substr $File::Find::name, length($etc) + 1 or next;
90 my $target = "/etc/$fn";
91 File::Path::make_path(dirname $target);
92 $created{$fn} = 1;
93
94 # Rename doesn't work if target is directory.
95 if (-l $_ && -d $target) {
96 if (isStatic $target) {
97 rmtree $target or warn;
98 } else {
99 warn "$target directory contains user files. Symlinking may fail.";
100 }
101 }
102
103 if (-e "$_.mode") {
104 my $mode = read_file("$_.mode"); chomp $mode;
105 if ($mode eq "direct-symlink") {
106 atomicSymlink readlink("$static/$fn"), $target or warn;
107 } else {
108 my $uid = read_file("$_.uid"); chomp $uid;
109 my $gid = read_file("$_.gid"); chomp $gid;
110 copy "$static/$fn", "$target.tmp" or warn;
111 chown int($uid), int($gid), "$target.tmp" or warn;
112 chmod oct($mode), "$target.tmp" or warn;
113 rename "$target.tmp", $target or warn;
114 }
115 push @copied, $fn;
116 print CLEAN "$fn\n";
117 } elsif (-l "$_") {
118 atomicSymlink "$static/$fn", $target or warn;
119 }
120}
121
122find(\&link, $etc);
123
124
125# Delete files that were copied in a previous version but not in the
126# current.
127foreach my $fn (@oldCopied) {
128 if (!defined $created{$fn}) {
129 $fn = "/etc/$fn";
130 print STDERR "removing obsolete file ‘$fn’...\n";
131 unlink "$fn";
132 }
133}
134
135
136# Rewrite /etc/.clean.
137close CLEAN;
138write_file("/etc/.clean", map { "$_\n" } @copied);