diff --git a/nixos/modules/system/activation/switch-to-configuration.pl b/nixos/modules/system/activation/switch-to-configuration.pl deleted file mode 100644 index 397b308b73118..0000000000000 --- a/nixos/modules/system/activation/switch-to-configuration.pl +++ /dev/null @@ -1,500 +0,0 @@ -#! @perl@ - -use strict; -use warnings; -use File::Basename; -use File::Slurp; -use Net::DBus; -use Sys::Syslog qw(:standard :macros); -use Cwd 'abs_path'; - -my $out = "@out@"; - -# To be robust against interruption, record what units need to be started etc. -my $startListFile = "/run/systemd/start-list"; -my $restartListFile = "/run/systemd/restart-list"; -my $reloadListFile = "/run/systemd/reload-list"; - -my $action = shift @ARGV; - -if ("@localeArchive@" ne "") { - $ENV{LOCALE_ARCHIVE} = "@localeArchive@"; -} - -if (!defined $action || ($action ne "switch" && $action ne "boot" && $action ne "test" && $action ne "dry-activate")) { - print STDERR < 'quiet') // "") =~ /ID=nixos/s; - -openlog("nixos", "", LOG_USER); - -# Install or update the bootloader. -if ($action eq "switch" || $action eq "boot") { - system("@installBootLoader@ $out") == 0 or exit 1; -} - -# Just in case the new configuration hangs the system, do a sync now. -system("@coreutils@/bin/sync", "-f", "/nix/store") unless ($ENV{"NIXOS_NO_SYNC"} // "") eq "1"; - -exit 0 if $action eq "boot"; - -# Check if we can activate the new configuration. -my $oldVersion = read_file("/run/current-system/init-interface-version", err_mode => 'quiet') // ""; -my $newVersion = read_file("$out/init-interface-version"); - -if ($newVersion ne $oldVersion) { - print STDERR <system->get_service("org.freedesktop.systemd1")->get_object("/org/freedesktop/systemd1"); - my $units = $mgr->ListUnitsByPatterns([], []); - my $res = {}; - for my $item (@$units) { - my ($id, $description, $load_state, $active_state, $sub_state, - $following, $unit_path, $job_id, $job_type, $job_path) = @$item; - next unless $following eq ''; - next if $job_id == 0 and $active_state eq 'inactive'; - $res->{$id} = { load => $load_state, state => $active_state, substate => $sub_state }; - } - return $res; -} - -sub parseFstab { - my ($filename) = @_; - my ($fss, $swaps); - foreach my $line (read_file($filename, err_mode => 'quiet')) { - chomp $line; - $line =~ s/^\s*#.*//; - next if $line =~ /^\s*$/; - my @xs = split / /, $line; - if ($xs[2] eq "swap") { - $swaps->{$xs[0]} = { options => $xs[3] // "" }; - } else { - $fss->{$xs[1]} = { device => $xs[0], fsType => $xs[2], options => $xs[3] // "" }; - } - } - return ($fss, $swaps); -} - -sub parseUnit { - my ($filename) = @_; - my $info = {}; - parseKeyValues($info, read_file($filename)) if -f $filename; - parseKeyValues($info, read_file("${filename}.d/overrides.conf")) if -f "${filename}.d/overrides.conf"; - return $info; -} - -sub parseKeyValues { - my $info = shift; - foreach my $line (@_) { - # FIXME: not quite correct. - $line =~ /^([^=]+)=(.*)$/ or next; - $info->{$1} = $2; - } -} - -sub boolIsTrue { - my ($s) = @_; - return $s eq "yes" || $s eq "true"; -} - -sub recordUnit { - my ($fn, $unit) = @_; - write_file($fn, { append => 1 }, "$unit\n") if $action ne "dry-activate"; -} - -# As a fingerprint for determining whether a unit has changed, we use -# its absolute path. If it has an override file, we append *its* -# absolute path as well. -sub fingerprintUnit { - my ($s) = @_; - return abs_path($s) . (-f "${s}.d/overrides.conf" ? " " . abs_path "${s}.d/overrides.conf" : ""); -} - -# Figure out what units need to be stopped, started, restarted or reloaded. -my (%unitsToStop, %unitsToSkip, %unitsToStart, %unitsToRestart, %unitsToReload); - -my %unitsToFilter; # units not shown - -$unitsToStart{$_} = 1 foreach - split('\n', read_file($startListFile, err_mode => 'quiet') // ""); - -$unitsToRestart{$_} = 1 foreach - split('\n', read_file($restartListFile, err_mode => 'quiet') // ""); - -$unitsToReload{$_} = 1 foreach - split '\n', read_file($reloadListFile, err_mode => 'quiet') // ""; - -my $activePrev = getActiveUnits; -while (my ($unit, $state) = each %{$activePrev}) { - my $baseUnit = $unit; - - my $prevUnitFile = "/etc/systemd/system/$baseUnit"; - my $newUnitFile = "$out/etc/systemd/system/$baseUnit"; - - # Detect template instances. - if (!-e $prevUnitFile && !-e $newUnitFile && $unit =~ /^(.*)@[^\.]*\.(.*)$/) { - $baseUnit = "$1\@.$2"; - $prevUnitFile = "/etc/systemd/system/$baseUnit"; - $newUnitFile = "$out/etc/systemd/system/$baseUnit"; - } - - my $baseName = $baseUnit; - $baseName =~ s/\.[a-z]*$//; - - if (-e $prevUnitFile && ($state->{state} eq "active" || $state->{state} eq "activating")) { - if (! -e $newUnitFile || abs_path($newUnitFile) eq "/dev/null") { - # Ignore (i.e. never stop) these units: - if ($unit eq "system.slice") { - # TODO: This can be removed a few months after 18.09 is out - # (i.e. after everyone switched away from 18.03). - # Problem: Restarting (stopping) system.slice would not only - # stop X11 but also most system units/services. We obviously - # don't want this happening to users when they switch from 18.03 - # to 18.09 or nixos-unstable. - # Reason: The following change in systemd: - # https://github.com/systemd/systemd/commit/d8e5a9338278d6602a0c552f01f298771a384798 - # The commit adds system.slice to the perpetual units, which - # means removing the unit file and adding it to the source code. - # This is done so that system.slice can't be stopped anymore but - # in our case it ironically would cause this script to stop - # system.slice because the unit was removed (and an older - # systemd version is still running). - next; - } - my $unitInfo = parseUnit($prevUnitFile); - $unitsToStop{$unit} = 1 if boolIsTrue($unitInfo->{'X-StopOnRemoval'} // "yes"); - } - - elsif ($unit =~ /\.target$/) { - my $unitInfo = parseUnit($newUnitFile); - - # Cause all active target units to be restarted below. - # This should start most changed units we stop here as - # well as any new dependencies (including new mounts and - # swap devices). FIXME: the suspend target is sometimes - # active after the system has resumed, which probably - # should not be the case. Just ignore it. - if ($unit ne "suspend.target" && $unit ne "hibernate.target" && $unit ne "hybrid-sleep.target") { - unless (boolIsTrue($unitInfo->{'RefuseManualStart'} // "no")) { - $unitsToStart{$unit} = 1; - recordUnit($startListFile, $unit); - # Don't spam the user with target units that always get started. - $unitsToFilter{$unit} = 1; - } - } - - # Stop targets that have X-StopOnReconfiguration set. - # This is necessary to respect dependency orderings - # involving targets: if unit X starts after target Y and - # target Y starts after unit Z, then if X and Z have both - # changed, then X should be restarted after Z. However, - # if target Y is in the "active" state, X and Z will be - # restarted at the same time because X's dependency on Y - # is already satisfied. Thus, we need to stop Y first. - # Stopping a target generally has no effect on other units - # (unless there is a PartOf dependency), so this is just a - # bookkeeping thing to get systemd to do the right thing. - if (boolIsTrue($unitInfo->{'X-StopOnReconfiguration'} // "no")) { - $unitsToStop{$unit} = 1; - } - } - - elsif (fingerprintUnit($prevUnitFile) ne fingerprintUnit($newUnitFile)) { - if ($unit eq "sysinit.target" || $unit eq "basic.target" || $unit eq "multi-user.target" || $unit eq "graphical.target") { - # Do nothing. These cannot be restarted directly. - } elsif ($unit =~ /\.mount$/) { - # Reload the changed mount unit to force a remount. - $unitsToReload{$unit} = 1; - recordUnit($reloadListFile, $unit); - } elsif ($unit =~ /\.socket$/ || $unit =~ /\.path$/ || $unit =~ /\.slice$/) { - # FIXME: do something? - } else { - my $unitInfo = parseUnit($newUnitFile); - if (boolIsTrue($unitInfo->{'X-ReloadIfChanged'} // "no")) { - $unitsToReload{$unit} = 1; - recordUnit($reloadListFile, $unit); - } - elsif (!boolIsTrue($unitInfo->{'X-RestartIfChanged'} // "yes") || boolIsTrue($unitInfo->{'RefuseManualStop'} // "no") ) { - $unitsToSkip{$unit} = 1; - } else { - if (!boolIsTrue($unitInfo->{'X-StopIfChanged'} // "yes")) { - # This unit should be restarted instead of - # stopped and started. - $unitsToRestart{$unit} = 1; - recordUnit($restartListFile, $unit); - } else { - # If this unit is socket-activated, then stop the - # socket unit(s) as well, and restart the - # socket(s) instead of the service. - my $socketActivated = 0; - if ($unit =~ /\.service$/) { - my @sockets = split / /, ($unitInfo->{Sockets} // ""); - if (scalar @sockets == 0) { - @sockets = ("$baseName.socket"); - } - foreach my $socket (@sockets) { - if (defined $activePrev->{$socket}) { - $unitsToStop{$socket} = 1; - $unitsToStart{$socket} = 1; - recordUnit($startListFile, $socket); - $socketActivated = 1; - } - } - } - - # If the unit is not socket-activated, record - # that this unit needs to be started below. - # We write this to a file to ensure that the - # service gets restarted if we're interrupted. - if (!$socketActivated) { - $unitsToStart{$unit} = 1; - recordUnit($startListFile, $unit); - } - - $unitsToStop{$unit} = 1; - } - } - } - } - } -} - -sub pathToUnitName { - my ($path) = @_; - # Use current version of systemctl binary before daemon is reexeced. - open my $cmd, "-|", "/run/current-system/sw/bin/systemd-escape", "--suffix=mount", "-p", $path - or die "Unable to escape $path!\n"; - my $escaped = join "", <$cmd>; - chomp $escaped; - close $cmd or die; - return $escaped; -} - -sub unique { - my %seen; - my @res; - foreach my $name (@_) { - next if $seen{$name}; - $seen{$name} = 1; - push @res, $name; - } - return @res; -} - -# Compare the previous and new fstab to figure out which filesystems -# need a remount or need to be unmounted. New filesystems are mounted -# automatically by starting local-fs.target. FIXME: might be nicer if -# we generated units for all mounts; then we could unify this with the -# unit checking code above. -my ($prevFss, $prevSwaps) = parseFstab "/etc/fstab"; -my ($newFss, $newSwaps) = parseFstab "$out/etc/fstab"; -foreach my $mountPoint (keys %$prevFss) { - my $prev = $prevFss->{$mountPoint}; - my $new = $newFss->{$mountPoint}; - my $unit = pathToUnitName($mountPoint); - if (!defined $new) { - # Filesystem entry disappeared, so unmount it. - $unitsToStop{$unit} = 1; - } elsif ($prev->{fsType} ne $new->{fsType} || $prev->{device} ne $new->{device}) { - # Filesystem type or device changed, so unmount and mount it. - $unitsToStop{$unit} = 1; - $unitsToStart{$unit} = 1; - recordUnit($startListFile, $unit); - } elsif ($prev->{options} ne $new->{options}) { - # Mount options changes, so remount it. - $unitsToReload{$unit} = 1; - recordUnit($reloadListFile, $unit); - } -} - -# Also handles swap devices. -foreach my $device (keys %$prevSwaps) { - my $prev = $prevSwaps->{$device}; - my $new = $newSwaps->{$device}; - if (!defined $new) { - # Swap entry disappeared, so turn it off. Can't use - # "systemctl stop" here because systemd has lots of alias - # units that prevent a stop from actually calling - # "swapoff". - print STDERR "stopping swap device: $device\n"; - system("@utillinux@/sbin/swapoff", $device); - } - # FIXME: update swap options (i.e. its priority). -} - - -# Should we have systemd re-exec itself? -my $prevSystemd = abs_path("/proc/1/exe") // "/unknown"; -my $newSystemd = abs_path("@systemd@/lib/systemd/systemd") or die; -my $restartSystemd = $prevSystemd ne $newSystemd; - - -sub filterUnits { - my ($units) = @_; - my @res; - foreach my $unit (sort(keys %{$units})) { - push @res, $unit if !defined $unitsToFilter{$unit}; - } - return @res; -} - -my @unitsToStopFiltered = filterUnits(\%unitsToStop); -my @unitsToStartFiltered = filterUnits(\%unitsToStart); - - -# Show dry-run actions. -if ($action eq "dry-activate") { - print STDERR "would stop the following units: ", join(", ", @unitsToStopFiltered), "\n" - if scalar @unitsToStopFiltered > 0; - print STDERR "would NOT stop the following changed units: ", join(", ", sort(keys %unitsToSkip)), "\n" - if scalar(keys %unitsToSkip) > 0; - print STDERR "would restart systemd\n" if $restartSystemd; - print STDERR "would restart the following units: ", join(", ", sort(keys %unitsToRestart)), "\n" - if scalar(keys %unitsToRestart) > 0; - print STDERR "would start the following units: ", join(", ", @unitsToStartFiltered), "\n" - if scalar @unitsToStartFiltered; - print STDERR "would reload the following units: ", join(", ", sort(keys %unitsToReload)), "\n" - if scalar(keys %unitsToReload) > 0; - exit 0; -} - - -syslog(LOG_NOTICE, "switching to system configuration $out"); - -if (scalar (keys %unitsToStop) > 0) { - print STDERR "stopping the following units: ", join(", ", @unitsToStopFiltered), "\n" - if scalar @unitsToStopFiltered; - # Use current version of systemctl binary before daemon is reexeced. - system("/run/current-system/sw/bin/systemctl", "stop", "--", sort(keys %unitsToStop)); # FIXME: ignore errors? -} - -print STDERR "NOT restarting the following changed units: ", join(", ", sort(keys %unitsToSkip)), "\n" - if scalar(keys %unitsToSkip) > 0; - -# Activate the new configuration (i.e., update /etc, make accounts, -# and so on). -my $res = 0; -print STDERR "activating the configuration...\n"; -system("$out/activate", "$out") == 0 or $res = 2; - -# Restart systemd if necessary. -if ($restartSystemd) { - print STDERR "restarting systemd...\n"; - system("@systemd@/bin/systemctl", "daemon-reexec") == 0 or $res = 2; -} - -# Forget about previously failed services. -system("@systemd@/bin/systemctl", "reset-failed"); - -# Make systemd reload its units. -system("@systemd@/bin/systemctl", "daemon-reload") == 0 or $res = 3; - -# Reload user units -open my $listActiveUsers, '-|', '@systemd@/bin/loginctl', 'list-users', '--no-legend'; -while (my $f = <$listActiveUsers>) { - next unless $f =~ /^\s*(?\d+)\s+(?\S+)/; - my ($uid, $name) = ($+{uid}, $+{user}); - print STDERR "reloading user units for $name...\n"; - - system("@su@", "-s", "@shell@", "-l", $name, "-c", "XDG_RUNTIME_DIR=/run/user/$uid @systemd@/bin/systemctl --user daemon-reload"); - system("@su@", "-s", "@shell@", "-l", $name, "-c", "XDG_RUNTIME_DIR=/run/user/$uid @systemd@/bin/systemctl --user start nixos-activation.service"); -} - -close $listActiveUsers; - -# Set the new tmpfiles -print STDERR "setting up tmpfiles\n"; -system("@systemd@/bin/systemd-tmpfiles", "--create", "--remove", "--exclude-prefix=/dev") == 0 or $res = 3; - -# Reload units that need it. This includes remounting changed mount -# units. -if (scalar(keys %unitsToReload) > 0) { - print STDERR "reloading the following units: ", join(", ", sort(keys %unitsToReload)), "\n"; - system("@systemd@/bin/systemctl", "reload", "--", sort(keys %unitsToReload)) == 0 or $res = 4; - unlink($reloadListFile); -} - -# Restart changed services (those that have to be restarted rather -# than stopped and started). -if (scalar(keys %unitsToRestart) > 0) { - print STDERR "restarting the following units: ", join(", ", sort(keys %unitsToRestart)), "\n"; - system("@systemd@/bin/systemctl", "restart", "--", sort(keys %unitsToRestart)) == 0 or $res = 4; - unlink($restartListFile); -} - -# Start all active targets, as well as changed units we stopped above. -# The latter is necessary because some may not be dependencies of the -# targets (i.e., they were manually started). FIXME: detect units -# that are symlinks to other units. We shouldn't start both at the -# same time because we'll get a "Failed to add path to set" error from -# systemd. -print STDERR "starting the following units: ", join(", ", @unitsToStartFiltered), "\n" - if scalar @unitsToStartFiltered; -system("@systemd@/bin/systemctl", "start", "--", sort(keys %unitsToStart)) == 0 or $res = 4; -unlink($startListFile); - - -# Print failed and new units. -my (@failed, @new, @restarting); -my $activeNew = getActiveUnits; -while (my ($unit, $state) = each %{$activeNew}) { - if ($state->{state} eq "failed") { - push @failed, $unit; - } - elsif ($state->{state} eq "auto-restart") { - # A unit in auto-restart state is a failure *if* it previously failed to start - my $lines = `@systemd@/bin/systemctl show '$unit'`; - my $info = {}; - parseKeyValues($info, split("\n", $lines)); - - if ($info->{ExecMainStatus} ne '0') { - push @failed, $unit; - } - } - elsif ($state->{state} ne "failed" && !defined $activePrev->{$unit}) { - push @new, $unit; - } -} - -print STDERR "the following new units were started: ", join(", ", sort(@new)), "\n" - if scalar @new > 0; - -if (scalar @failed > 0) { - print STDERR "warning: the following units failed: ", join(", ", sort(@failed)), "\n"; - foreach my $unit (@failed) { - print STDERR "\n"; - system("COLUMNS=1000 @systemd@/bin/systemctl status --no-pager '$unit' >&2"); - } - $res = 4; -} - -if ($res == 0) { - syslog(LOG_NOTICE, "finished switching to system configuration $out"); -} else { - syslog(LOG_ERR, "switching to system configuration $out failed (status $res)"); -} - -exit $res; diff --git a/nixos/modules/system/activation/switch-to-configuration.py b/nixos/modules/system/activation/switch-to-configuration.py new file mode 100644 index 0000000000000..ce1529fb5aa17 --- /dev/null +++ b/nixos/modules/system/activation/switch-to-configuration.py @@ -0,0 +1,612 @@ +#! @python@ + +import dbus +import os +from pathlib import Path +import re +import signal +import subprocess +from syslog import openlog, syslog, LOG_NOTICE, LOG_ERR +import sys +from typing import cast, Dict, Set, NewType, List, Tuple, NamedTuple, Any + +out = Path("@out@") + +# To be robust against interruption, record what units need to be started etc. +START_UNIT_FILE = Path("/run/systemd/start-list") +RESTART_UNIT_FILE = Path("/run/systemd/restart-list") +RELOAD_UNIT_FILE = Path("/run/systemd/reload-list") + +def print_err(*s: Any) -> None: + print(*s, file=sys.stderr) + +def die_with_usage() -> None: + msg = """ + Usage: {prog} [switch|boot|test] + + switch: make the configuration the boot default and activate now + boot: make the configuration the boot default + test: activate the configuration, but don\'t make it the boot default + dry-activate: show what would be done if this configuration were activated + """.format(prog=sys.argv[0]) + print_err(msg) + sys.exit(1) + +def die(why: str) -> None: + print_err("error:", why) + sys.exit(1) + +def read_if_exists(path: Path, otherwise: str) -> str: + if path.is_file(): + return path.read_text() + else: + return otherwise + +# This is a NixOS installation if it has /etc/NIXOS or a proper +# /etc/os-release. +def is_nixos() -> bool: + if Path("/etc/NIXOS").is_file(): + return True + + os_release = read_if_exists(Path("/etc/os-release"), otherwise="") + if os_release.find("ID=nixos") != -1: + return True + + return False + +# A systemd unit name. +UnitName = NewType('UnitName', str) +ActiveUnit = NamedTuple('ActiveUnit', + [('load', str), ('state', str), ('sub_state', str)]) + +def get_active_units() -> Dict[UnitName, ActiveUnit]: + bus = dbus.SystemBus() + obj = bus.get_object('org.freedesktop.systemd1', + '/org/freedesktop/systemd1') + + units = obj.ListUnitsByPatterns([], [], + dbus_interface='org.freedesktop.systemd1.Manager') + res = {} + for unit in units: + (id, description, load_state, active_state, sub_state, + following, unit_path, job_id, job_type, job_path) = unit + if following != '': + continue + if job_id == 0 and active_state == 'inactive': + continue + res[UnitName(id)] = ActiveUnit(load=load_state, + state=active_state, + sub_state=sub_state) + + return res + +Mountpoint = NewType('Mountpoint', str) +BlockDevice = NewType('BlockDevice', str) +FsMount = NamedTuple('FsMount', [('device', BlockDevice), + ('fs_type', str), + ('options', str)]) +SwapMount = NamedTuple('SwapMount', [('options', str)]) + +def parse_fstab(filename: Path) -> Tuple[Dict[Mountpoint, FsMount], Dict[BlockDevice, SwapMount]]: + fss = {} # type: Dict[Mountpoint, FsMount] + swaps = {} # type: Dict[BlockDevice, SwapMount] + for line in filename.read_text().split('\n'): + line = line.strip() + line = re.sub(r'^\s*#.*', '', line) + if line.strip() == '': + continue + + xs = re.split(' ', line) + device = BlockDevice(xs[0]) + mountpoint = Mountpoint(xs[1]) + fs_type = xs[2] + options = xs[3] if len(xs) > 4 else '' + if fs_type == 'swap': + swaps[device] = SwapMount(options) + else: + fss[mountpoint] = FsMount(device, fs_type, options) + + return (fss, swaps) + +def parse_unit(filename: Path) -> Dict[str, str]: + info = {} + info.update(parse_key_values(read_if_exists(filename, ''))) + overrides = filename.with_name(filename.name + ".d") / "overrides.conf" + info.update(parse_key_values(read_if_exists(overrides, ''))) + return info + +def parse_key_values(s: str) -> Dict[str, str]: + info = {} + for line in s.split('\n'): + m = re.match(r'^([^=]+)=(.*)$', line) + if m is None: + die('error parsing unit') + else: + info[m.group(1)] = m.group(2) + + return info + +def bool_is_true(s: str) -> bool: + return s in ['yes', 'true'] + +# As a fingerprint for determining whether a unit has changed, we use +# its absolute path. If it has an override file, we append *its* +# absolute path as well. +def fingerprint_unit(filename: Path) -> str: + fprint = str(filename.resolve()) + override = filename.with_name(filename.name+".d") / "overrides.conf" + if override.is_file(): + fprint += ' ' + str(override.resolve()) + return fprint + +class UnitTransitions: + def __init__(self, dry_run: bool) -> None: + def read_unit_list(fname: Path) -> Set[UnitName]: + units = read_if_exists(fname, "").split('\n') + return { UnitName(unit) for unit in units } + + self.dry_run = dry_run + self.to_stop = set() # type: Set[UnitName] + self.to_skip = set() # type: Set[UnitName] + self.to_start = read_unit_list(START_UNIT_FILE) + self.to_restart = read_unit_list(RESTART_UNIT_FILE) + self.to_reload = read_unit_list(RELOAD_UNIT_FILE) + self.to_filter = set() # type: Set[UnitName] + + # ========================= + # Record unit transitions + # ========================= + + def _record_unit(self, filename: Path, unit: UnitName) -> None: + if not self.dry_run: + with open(filename, 'a') as f: + f.write(unit) + f.write('\n') + + def stop_unit(self, unit: UnitName) -> None: + self.to_stop.add(unit) + + def skip_unit(self, unit: UnitName) -> None: + self.to_skip.add(unit) + + def start_unit(self, unit: UnitName) -> None: + self.to_start.add(unit) + self._record_unit(START_UNIT_FILE, unit) + + def restart_unit(self, unit: UnitName) -> None: + self.to_restart.add(unit) + self._record_unit(RESTART_UNIT_FILE, unit) + + def reload_unit(self, unit: UnitName) -> None: + self.to_reload.add(unit) + self._record_unit(RELOAD_UNIT_FILE, unit) + + def filter_unit(self, unit: UnitName) -> None: + self.to_filter.add(unit) + + # ========================= + # Accessors + # ========================= + + def _filter_units(self, units: Set[UnitName]) -> Set[UnitName]: + return { unit + for unit in units + if unit not in self.to_filter } + + def filtered_units_to_stop(self) -> Set[UnitName]: + return self._filter_units(self.to_stop) + + def filtered_units_to_start(self) -> Set[UnitName]: + return self._filter_units(self.to_start) + + def units_to_stop(self) -> Set[UnitName]: + return self.to_stop + + def units_to_start(self) -> Set[UnitName]: + return self.to_start + + def units_to_restart(self) -> Set[UnitName]: + return self.to_restart + + def units_to_reload(self) -> Set[UnitName]: + return self.to_reload + + def units_to_skip(self) -> Set[UnitName]: + return self.to_skip + + def summary(self) -> str: + res = [] # type: List[str] + + def action(units: Set[UnitName], descr: str) -> None: + if len(units) > 0: + res += [descr + ": " + ', '.join(units)] + + action(self.filtered_units_to_stop(), + "would stop the following units") + + action(self.units_to_skip(), + "would NOT stop the following changed units") + + action(self.units_to_restart(), + "would restart the following units") + + action(self.filtered_units_to_start(), + "would start the following units") + + action(self.units_to_reload(), + "would reload the following units") + + return '\n'.join(res) + +# Figure out what units need to be stopped, started, restarted or reloaded. +def determine_unit_transitions(dry_run: bool) -> UnitTransitions: + transitions = UnitTransitions(dry_run) + active_prev = get_active_units() + + for unit, state in active_prev.items(): + base_unit = unit # type: str + prev_unit_file = Path("/etc/systemd/system") / Path(base_unit) + new_unit_file = out / "etc/systemd/system" / Path(base_unit) + + # Detect template instances. + m = re.match(r"^(.*)@[^\.]*\.(.*)$", unit) + if not prev_unit_file.exists() \ + and not new_unit_file.exists() \ + and m is not None: + base_unit = "%s@%s" % (m.group(1), m.group(1)) + prev_unit_file = Path("/etc/systemd/system") / Path(base_unit) + new_unit_file = out / "etc/systemd/system" / Path(base_unit) + + base_name = re.sub(r"\.[a-z]*$", "", base_unit) + prev_exists = prev_unit_file.exists() + new_exists = new_unit_file.exists() + if prev_exists and state.state in ['active', 'activating']: + if (not new_exists) or new_unit_file.resolve() == "/dev/null": + # Ignore (i.e. never stop) these units: + if unit == "system.slice": + # TODO: This can be removed a few months after 18.09 is out + # (i.e. after everyone switched away from 18.03). + # Problem: Restarting (stopping) system.slice would not only + # stop X11 but also most system units/services. We obviously + # don't want this happening to users when they switch from 18.03 + # to 18.09 or nixos-unstable. + # Reason: The following change in systemd: + # https://github.com/systemd/systemd/commit/d8e5a9338278d6602a0c552f01f298771a384798 + # The commit adds system.slice to the perpetual units, which + # means removing the unit file and adding it to the source code. + # This is done so that system.slice can't be stopped anymore but + # in our case it ironically would cause this script to stop + # system.slice because the unit was removed (and an older + # systemd version is still running). + continue + + unit_info = parse_unit(prev_unit_file) + if bool_is_true(unit_info.get('X-StopOnRemoval', 'yes')): + transitions.stop_unit(unit) + + elif unit.endswith('.target'): + unit_info = parse_unit(new_unit_file) + + # Cause all active target units to be restarted below. + # This should start most changed units we stop here as + # well as any new dependencies (including new mounts and + # swap devices). FIXME: the suspend target is sometimes + # active after the system has resumed, which probably + # should not be the case. Just ignore it. + suspend_targets = [ "suspend.target", "hibernate.target", "hybrid-sleep.target"] + if unit not in suspend_targets: + if not bool_is_true(unit_info.get('RefuseManualStart', 'no')): + transitions.start_unit(unit) + # Don't spam the user with target units that always get started. + transitions.filter_unit(unit) + + # Stop targets that have X-StopOnReconfiguration set. + # This is necessary to respect dependency orderings + # involving targets: if unit X starts after target Y and + # target Y starts after unit Z, then if X and Z have both + # changed, then X should be restarted after Z. However, + # if target Y is in the "active" state, X and Z will be + # restarted at the same time because X's dependency on Y + # is already satisfied. Thus, we need to stop Y first. + # Stopping a target generally has no effect on other units + # (unless there is a PartOf dependency), so this is just a + # bookkeeping thing to get systemd to do the right thing. + if bool_is_true(unit_info.get('X-StopOnReconfiguration', 'no')): + transitions.stop_unit(unit) + + elif fingerprint_unit(prev_unit_file) != fingerprint_unit(new_unit_file): + system_targets = ['sysinit.target', 'basic.target', 'multi-user.target', 'graphical.target'] + if unit in system_targets: + # Do nothing. These cannot be restarted directly. + pass + elif unit.endswith('.mount'): + # Reload the changed mount unit to force a remount. + transitions.reload_unit(unit) + + elif unit.endswith('.socket') or unit.endswith('.path') or unit.endswith('.slice'): + # FIXME: do something? + pass + else: + unit_info = parse_unit(new_unit_file) + reload_if_changed = bool_is_true(unit_info.get('X-ReloadIfChanged', 'no')) + restart_if_changed = bool_is_true(unit_info.get('X-RestartIfChanged', 'yes')) + refuse_manual_stop = bool_is_true(unit_info.get('RefuseManualStop', 'no')) + stop_if_changed = bool_is_true(unit_info.get('X-StopIfChanged', 'yes')) + + if reload_if_changed: + transitions.reload_unit(unit) + elif not restart_if_changed or refuse_manual_stop: + transitions.skip_unit(unit) + else: + if not stop_if_changed: + # This unit should be restarted instead of + # stopped and started. + transitions.restart_unit(unit) + else: + # If this unit is socket-activated, then stop the + # socket unit(s) as well, and restart the + # socket(s) instead of the service. + socketActivated = False + if unit.endswith('.service'): + sockets = list(map(UnitName, unit_info.get('Sockets', '').split())) + if len(sockets) == 0: + sockets = [UnitName("{base_name}.socket".format(base_name=base_name))] + + for socket in sockets: + if socket in active_prev: + transitions.stop_unit(socket) + transitions.start_unit(socket) + socket_activated = True + + # If the unit is not socket-activated, record + # that this unit needs to be started below. + # We write this to a file to ensure that the + # service gets restarted if we're interrupted. + if not socket_activated: + transitions.start_unit(unit) + + transitions.stop_unit(unit) + + return transitions + +def path_to_unit_name(path: Path) -> UnitName: + # Use current version of systemctl binary before daemon is reexeced. + args = ["/run/current-system/sw/bin/systemd-escape", + "--suffix=mount", "-p", str(path)] + output = subprocess.check_output(args) + return UnitName(output.strip()) + +# Compare the previous and new fstab to figure out which filesystems +# need a remount or need to be unmounted. New filesystems are mounted +# automatically by starting local-fs.target. FIXME: might be nicer if +# we generated units for all mounts; then we could unify this with the +# unit checking code above. +def determine_mount_changes(transitions: UnitTransitions) -> None: + prev_fss, prev_swaps = parse_fstab(Path('/etc/fstab')) + new_fss, new_swaps = parse_fstab(out / Path('etc/fstab')) + for mountpoint in prev_fss.keys(): + unit = path_to_unit_name(Path(mountpoint)) + if mountpoint not in new_fss: + # Filesystem entry disappeared, so unmount it. + transitions.stop_unit(unit) + continue + + new = new_fss[mountpoint] + prev = prev_fss[mountpoint] + if prev.fs_type != new.fs_type or \ + prev.device != new.device: + # Filesystem type or device changed, so unmount and mount it. + transitions.stop_unit(unit) + transitions.start_unit(unit) + elif prev.options != new.options: + # Mount options changes, so remount it. + transitions.reload_unit(unit) + + # Also handles swap devices. + for device in prev_swaps.keys(): + if device not in new_swaps: + # Swap entry disappeared, so turn it off. Can't use + # "systemctl stop" here because systemd has lots of alias + # units that prevent a stop from actually calling + # "swapoff". + print_err("stopping swap device:", device) + subprocess.check_call(["@utillinux@/sbin/swapoff", device]) + + # FIXME: update swap options (i.e. its priority). + +def reload_user_units() -> None: + args = ["@systemd@/bin/loginctl", "list-users", "--no-legend"] + active_users = subprocess.check_output(args).split('\n') + for f in active_users: + m = re.match(r"^\s*(?P\d+)\s+(?P\S+)", f) + if m is None: + continue + uid = m.group(1) + user = m.group(2) + print_err("reloading user units for {user}...".format(user=user)) + + subprocess.call(["@su@", "-s", "@shell@", "-l", user, + "-c", "XDG_RUNTIME_DIR=/run/user/{uid} @systemd@/bin/systemctl --user daemon-reload".format(uid=uid)]) + subprocess.call(["@su@", "-s", "@shell@", "-l", user, + "-c", "XDG_RUNTIME_DIR=/run/user/{uid} @systemd@/bin/systemctl --user start nixos-activation.service".format(uid=uid)]) + + +def main() -> None: + if len(sys.argv) < 1: + die_with_usage() + + action = sys.argv[1] + valid_modes = ["switch", "boot", "test", "dry-activate"] + if action not in valid_modes: + die_with_usage() + + if not is_nixos(): + die("This is not a NixOS installation!") + + openlog("nixos") + + # Install or update the bootloader. + if action == "switch" or action == "boot": + subprocess.check_call(["@installBootLoader@", out]) + + # Just in case the new configuration hangs the system, do a sync now. + if os.environ.get('NIXOS_NO_SYNC') != "1": + subprocess.check_call(["@coreutils@/bin/sync", "-f", "/nix/store"]) + + if action == "boot": + sys.exit(0) + + # Check if we can activate the new configuration. + old_version = read_if_exists(Path("/run/current-system/init-interface-version"), otherwise="") + new_version = (out / "init-interface-version").read_text() + + if new_version != old_version: + print_err(""" + Warning: the new NixOS configuration has an ‘init’ that is + incompatible with the current configuration. The new configuration + won\'t take effect until you reboot the system. + """) + sys.exit(100) + + # Ignore SIGHUP so that we're not killed if we're running on (say) + # virtual console 1 and we restart the "tty1" unit. + signal.signal(signal.SIGPIPE, signal.SIG_IGN) + + active_prev = get_active_units() + transitions = determine_unit_transitions(dry_run=action == 'dry-activate') + + # Should we have systemd re-exec itself? + prev_systemd = Path("/unknown") + pid1exe = Path("/proc/1/exe") + if pid1exe.exists(): + prev_systemd = pid1exe.resolve() + + new_systemd = Path("@systemd@/lib/systemd/systemd").resolve() + restart_systemd = prev_systemd != new_systemd + + units_to_stop = transitions.units_to_stop() + units_to_start = transitions.units_to_start() + units_to_restart = transitions.units_to_restart() + units_to_skip = transitions.units_to_skip() + + # Show dry-run actions. + if action == "dry-activate": + print_err(transitions.summary()) + if restart_systemd: + print_err("would restart systemd") + sys.exit() + + syslog(LOG_NOTICE, "switching to system configuration {out}".format(out=out)) + + if len(transitions.units_to_stop()) > 0: + if len(transitions.filtered_units_to_stop()) > 0: + print_err("stopping the following units: ", + ', '.join(transitions.filtered_units_to_stop())) + # Use current version of systemctl binary before daemon is reexeced. + subprocess.check_call(["/run/current-system/sw/bin/systemctl", "stop", "--"] + + sorted(transitions.units_to_stop())) # FIXME: ignore errors? + + if len(transitions.units_to_skip()) > 0: + print_err("NOT restarting the following changed units: ", + ', '.join(sorted(transitions.units_to_skip()))) + + # Activate the new configuration (i.e., update /etc, make accounts, + # and so on). + print_err("activating the configuration...") + res = 0 + if subprocess.call([out / "activate", out]) != 0: + res = 2 + + # Restart systemd if necessary. + if restart_systemd: + print_err("restarting systemd...") + if subprocess.call(["@systemd@/bin/systemctl", "daemon-reexec"]) != 0: + res = 2 + + # Forget about previously failed services. + subprocess.call(["@systemd@/bin/systemctl", "reset-failed"]) + + # Make systemd reload its units. + if subprocess.call(["@systemd@/bin/systemctl", "daemon-reload"]) != 0: + res = 3 + + reload_user_units() + + # Set the new tmpfiles + print_err("setting up tmpfiles") + if subprocess.call(["@systemd@/bin/systemd-tmpfiles", "--create", "--remove", + "--exclude-prefix=/dev"]) != 0: + res = 3 + + # Reload units that need it. This includes remounting changed mount + # units. + if len(transitions.units_to_reload()) > 0: + units = sorted(transitions.units_to_reload()) + print_err("reloading the following units: ", ", ".join(units)) + if subprocess.call(["@systemd@/bin/systemctl", "reload", "--"] + + cast(List[str], units)) != 0: + res = 4 + os.unlink(RELOAD_UNIT_FILE) + + # Restart changed services (those that have to be restarted rather + # than stopped and started). + if len(transitions.units_to_restart()) > 0: + units = sorted(transitions.units_to_restart()) + print_err("restarting the following units: ", ", ".join(units)) + if subprocess.call(["@systemd@/bin/systemctl", "restart", "--"] + + cast(List[str], units)) != 0: + res = 4 + os.unlink(RESTART_UNIT_FILE) + + # Start all active targets, as well as changed units we stopped above. + # The latter is necessary because some may not be dependencies of the + # targets (i.e., they were manually started). FIXME: detect units + # that are symlinks to other units. We shouldn't start both at the + # same time because we'll get a "Failed to add path to set" error from + # systemd. + if len(transitions.filtered_units_to_start()) > 0: + print("starting the following units: ", + ", ".join(transitions.filtered_units_to_start())) + if subprocess.call(["@systemd@/bin/systemctl", "start", "--"] + + sorted(transitions.units_to_start())) != 0: + res = 4 + os.unlink(START_UNIT_FILE) + + # Print failed and new units. + failed = [] + new = [] + active_new = get_active_units() + for unit, state in active_new.items(): + if state.state == 'failed': + failed.append(unit) + elif state.state == 'auto-restart': + # A unit in auto-restart state is a failure *if* it previously failed to start + lines = subprocess.check_output(['systemd@/bin/systemctl', 'show', unit]) + info = parse_key_values(lines.split('\n')) + + if info['ExecMainStatus'] != '0': + failed.append(unit) + elif state.state != 'failed' and unit not in active_prev: + new.append(unit) + + if len(new) > 0: + print_err("the following new units were started: ", ", ".join(sorted(new))) + + if len(failed) > 0: + print_err("warning: the following units failed: ", ", ".join(sorted(failed))) + for unit in failed: + print_err('') + cmd = "COLUMNS=1000 @systemd@/bin/systemctl status --no-pager '{unit}' >&2".format(unit=unit) + subprocess.check_call(cmd, shell=True) + res = 4 + + if res == 0: + syslog(LOG_NOTICE, "finished switching to system configuration {out}".format(out=out)) + else: + syslog(LOG_ERR, "switching to system configuration {out} failed (status {res})". + format(out=out, res=res)) + + sys.exit(res) + +if __name__ == '__main__': + main() diff --git a/nixos/modules/system/activation/top-level.nix b/nixos/modules/system/activation/top-level.nix index a560af5ce96da..557281428884c 100644 --- a/nixos/modules/system/activation/top-level.nix +++ b/nixos/modules/system/activation/top-level.nix @@ -85,7 +85,7 @@ let mkdir $out/bin export localeArchive="${config.i18n.glibcLocales}/lib/locale/locale-archive" - substituteAll ${./switch-to-configuration.pl} $out/bin/switch-to-configuration + substituteAll ${./switch-to-configuration.py} $out/bin/switch-to-configuration chmod +x $out/bin/switch-to-configuration echo -n "${toString config.system.extraDependencies}" > $out/extra-dependencies @@ -122,8 +122,9 @@ let configurationName = config.boot.loader.grub.configurationName; # Needed by switch-to-configuration. - - perl = "${pkgs.perl}/bin/perl " + (concatMapStringsSep " " (lib: "-I${lib}/${pkgs.perl.libPrefix}") (with pkgs.perlPackages; [ FileSlurp NetDBus XMLParser XMLTwig ])); + python = + let env = pkgs.python3.withPackages (pypkgs: [ pypkgs.dbus-python ]); + in "${env}/bin/python3"; }; # Handle assertions and warnings diff --git a/nixos/modules/system/etc/etc.nix b/nixos/modules/system/etc/etc.nix index 7d43ba07ca57f..6dfd339e860fe 100644 --- a/nixos/modules/system/etc/etc.nix +++ b/nixos/modules/system/etc/etc.nix @@ -154,7 +154,7 @@ in '' # Set up the statically computed bits of /etc. echo "setting up /etc..." - ${pkgs.perl}/bin/perl -I${pkgs.perlPackages.FileSlurp}/lib/perl5/site_perl ${./setup-etc.pl} ${etc}/etc + ${pkgs.python3}/bin/python3 ${./setup-etc.py} ${etc}/etc ''; }; diff --git a/nixos/modules/system/etc/setup-etc.pl b/nixos/modules/system/etc/setup-etc.pl deleted file mode 100644 index eed20065087fa..0000000000000 --- a/nixos/modules/system/etc/setup-etc.pl +++ /dev/null @@ -1,140 +0,0 @@ -use strict; -use File::Find; -use File::Copy; -use File::Path; -use File::Basename; -use File::Slurp; - -my $etc = $ARGV[0] or die; -my $static = "/etc/static"; - -sub atomicSymlink { - my ($source, $target) = @_; - my $tmp = "$target.tmp"; - unlink $tmp; - symlink $source, $tmp or return 0; - rename $tmp, $target or return 0; - return 1; -} - - -# Atomically update /etc/static to point at the etc files of the -# current configuration. -atomicSymlink $etc, $static or die; - -# Returns 1 if the argument points to the files in /etc/static. That -# means either argument is a symlink to a file in /etc/static or a -# directory with all children being static. -sub isStatic { - my $path = shift; - - if (-l $path) { - my $target = readlink $path; - return substr($target, 0, length "/etc/static/") eq "/etc/static/"; - } - - if (-d $path) { - opendir DIR, "$path" or return 0; - my @names = readdir DIR or die; - closedir DIR; - - foreach my $name (@names) { - next if $name eq "." || $name eq ".."; - unless (isStatic("$path/$name")) { - return 0; - } - } - return 1; - } - - return 0; -} - -# Remove dangling symlinks that point to /etc/static. These are -# configuration files that existed in a previous configuration but not -# in the current one. For efficiency, don't look under /etc/nixos -# (where all the NixOS sources live). -sub cleanup { - if ($File::Find::name eq "/etc/nixos") { - $File::Find::prune = 1; - return; - } - if (-l $_) { - my $target = readlink $_; - if (substr($target, 0, length $static) eq $static) { - my $x = "/etc/static/" . substr($File::Find::name, length "/etc/"); - unless (-l $x) { - print STDERR "removing obsolete symlink ‘$File::Find::name’...\n"; - unlink "$_"; - } - } - } -} - -find(\&cleanup, "/etc"); - - -# Use /etc/.clean to keep track of copied files. -my @oldCopied = read_file("/etc/.clean", chomp => 1, err_mode => 'quiet'); -open CLEAN, ">>/etc/.clean"; - - -# For every file in the etc tree, create a corresponding symlink in -# /etc to /etc/static. The indirection through /etc/static is to make -# switching to a new configuration somewhat more atomic. -my %created; -my @copied; - -sub link { - my $fn = substr $File::Find::name, length($etc) + 1 or next; - my $target = "/etc/$fn"; - File::Path::make_path(dirname $target); - $created{$fn} = 1; - - # Rename doesn't work if target is directory. - if (-l $_ && -d $target) { - if (isStatic $target) { - rmtree $target or warn; - } else { - warn "$target directory contains user files. Symlinking may fail."; - } - } - - if (-e "$_.mode") { - my $mode = read_file("$_.mode"); chomp $mode; - if ($mode eq "direct-symlink") { - atomicSymlink readlink("$static/$fn"), $target or warn; - } else { - my $uid = read_file("$_.uid"); chomp $uid; - my $gid = read_file("$_.gid"); chomp $gid; - copy "$static/$fn", "$target.tmp" or warn; - $uid = getpwnam $uid unless $uid =~ /^\+/; - $gid = getgrnam $gid unless $gid =~ /^\+/; - chown int($uid), int($gid), "$target.tmp" or warn; - chmod oct($mode), "$target.tmp" or warn; - rename "$target.tmp", $target or warn; - } - push @copied, $fn; - print CLEAN "$fn\n"; - } elsif (-l "$_") { - atomicSymlink "$static/$fn", $target or warn; - } -} - -find(\&link, $etc); - - -# Delete files that were copied in a previous version but not in the -# current. -foreach my $fn (@oldCopied) { - if (!defined $created{$fn}) { - $fn = "/etc/$fn"; - print STDERR "removing obsolete file ‘$fn’...\n"; - unlink "$fn"; - } -} - - -# Rewrite /etc/.clean. -close CLEAN; -write_file("/etc/.clean", map { "$_\n" } @copied); diff --git a/nixos/modules/system/etc/setup-etc.py b/nixos/modules/system/etc/setup-etc.py new file mode 100644 index 0000000000000..da4a088f59ed0 --- /dev/null +++ b/nixos/modules/system/etc/setup-etc.py @@ -0,0 +1,178 @@ +import argparse +import os +import os.path +from pathlib import Path +from pwd import getpwnam +from grp import getgrnam +import shutil +import sys +from typing import List, Tuple, Set + +STATIC = Path("/etc/static") +CLEAN_FILE = Path("/etc/.clean") + +def warn(msg: str) -> None: + print('warning:', msg, file=sys.stderr) + +def atomic_symlink(source: Path, target: Path) -> None: + tmp = Path("{target}.tmp".format(target=target)) + tmp.unlink() + tmp.symlink_to(source) + tmp.rename(target) + +def atomic_symlink_or_warn(source: Path, target: Path) -> None: + try: + atomic_symlink(source, target) + except IOError as exc: + warn("error creating link '{target}': {exc}". \ + format(target=target, exc=exc)) + +def is_static(path: Path) -> bool: + """ + Returns True if the argument points to the files in /etc/static. That means + either argument is a symlink to a file in /etc/static or a directory with + all children being static. + """ + + if path.is_symlink(): + target = path.resolve() + res = os.path.commonprefix([target, STATIC]) == STATIC # type: bool + return res + + if path.is_dir(): + return all(is_static(ent) for ent in path.iterdir()) + + return False + +def cleanup() -> None: + """ + Remove dangling symlinks that point to /etc/static. These are + configuration files that existed in a previous configuration but not + in the current one. For efficiency, don't look under /etc/nixos + (where all the NixOS sources live). + """ + for root, dirs, files in os.walk('/etc'): + if root == '/etc': + dirs.remove('nixos') + + for f in files: + path = Path(root) / Path(f) + if path.is_symlink(): + target = path.resolve() + if os.path.commonprefix([target, STATIC]) == STATIC: + x = STATIC / path.relative_to('/etc') + if x.is_symlink(): + print("removing obsolete symlink '{path}'".format(path=path), + file=sys.stderr) + path.unlink() + +def link_statics(etc: Path) -> Tuple[Set[Path], Set[Path]]: + """ + For every file in the etc tree, create a corresponding symlink in + /etc to /etc/static. The indirection through /etc/static is to make + switching to a new configuration somewhat more atomic. + """ + + clean = open(CLEAN_FILE, 'a') + created = set() + copied = set() + for root, dirs, files in os.walk(etc): + for f in files: + path = Path(root) / Path(f) + if os.path.commonprefix([etc, path]) != etc: + continue + fn = path.relative_to(etc) + target = Path("/etc") / f + target.parent.mkdir(parents=True) + created.add(fn) + + # Rename doesn't work if target is directory. + if Path(f).is_symlink() and target.is_dir(): + if is_static(target): + try: + shutil.rmtree(target) + except IOError as exc: + warn("error trying to remove '{target}': {exc}". \ + format(target=target, exc=exc)) + else: + warn("$target directory contains user files. Symlinking may fail.") + + meta_f = Path("{f}.mode".format(f=f)) + if meta_f.exists(): + mode = open(meta_f, 'r').read().strip() + if mode == 'direct-symlink': + src = (STATIC / fn).resolve() + atomic_symlink_or_warn(src, target) + else: + user = open("{f}.uid".format(f=f)).read().strip() + grp = open("{f}.gid".format(f=f)).read().strip() + uid = getpwnam(user).pw_uid + gid = getgrnam(grp).gr_gid + + src = STATIC / fn + tmp = Path("{target}.tmp".format(target=target)) + try: + shutil.copyfile(src, tmp) + except IOError as exc: + warn("error copying from '{src}' to '{tmp}': {exc}". \ + format(src=src, tmp=tmp, exc=exc)) + + try: + os.chown(tmp, uid, gid) + except IOError as exc: + warn("error setting owner of '{tmp}': {exc}". \ + format(tmp=tmp, exc=exc)) + + try: + os.chmod(tmp, int(mode, 8)) + except IOError as exc: + warn("error setting mode of '{tmp}': {exc}". \ + format(tmp=tmp, exc=exc)) + + try: + tmp.rename(target) + except IOError as exc: + warn("error renaming '{tmp}' to '{target}': {exc}". \ + format(tmp=tmp, target=target, exc=exc)) + + copied.add(fn) + print(fn, file=clean) + + elif Path(f).is_symlink(): + atomic_symlink_or_warn(STATIC / fn, target) + + return (created, copied) + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument('etc') + args = parser.parse_args() + etc = args.etc + + # Atomically update /etc/static to point at the etc files of the + # current configuration. + atomic_symlink(etc, STATIC) + + cleanup() + + # Use /etc/.clean to keep track of copied files. + old_copied = [] # type: List[str] + if CLEAN_FILE.is_file(): + old_copied += [ line.strip() for line in CLEAN_FILE.read_text().split('\n') ] + + created, copied = link_statics(etc) + + # Delete files that were copied in a previous version but not in the + # current. + for f in old_copied: + if f not in created: + fn = Path("/etc") / f + print("removing obsolete file '{f}'...".format(f=f), + file=sys.stderr) + fn.unlink() + + # Rewrite /etc/.clean + CLEAN_FILE.write_text('\n'.join(str(p) for p in copied)) + +if __name__ == "__main__": + main() diff --git a/pkgs/data/misc/shared-mime-info/default.nix b/pkgs/data/misc/shared-mime-info/default.nix index f1df81f2db418..c66517fd72cef 100644 --- a/pkgs/data/misc/shared-mime-info/default.nix +++ b/pkgs/data/misc/shared-mime-info/default.nix @@ -11,8 +11,9 @@ stdenv.mkDerivation rec { }; nativeBuildInputs = [ - pkgconfig gettext intltool perl perlXMLParser libxml2 glib + pkgconfig gettext intltool perl perlXMLParser ]; + buildInputs = [ libxml2 glib ]; meta = with stdenv.lib; { inherit version; diff --git a/pkgs/development/libraries/dbus-glib/default.nix b/pkgs/development/libraries/dbus-glib/default.nix index c8bc96f80fded..a850100d79c6f 100644 --- a/pkgs/development/libraries/dbus-glib/default.nix +++ b/pkgs/development/libraries/dbus-glib/default.nix @@ -11,7 +11,10 @@ stdenv.mkDerivation rec { outputs = [ "out" "dev" "devdoc" ]; outputBin = "dev"; - nativeBuildInputs = [ pkgconfig gettext ]; + nativeBuildInputs = [ + pkgconfig gettext + glib # for glib-marshal + ]; buildInputs = [ expat libiconv ]; diff --git a/pkgs/development/tools/build-managers/meson/default.nix b/pkgs/development/tools/build-managers/meson/default.nix index 35ae59af617cf..d07be33c0713a 100644 --- a/pkgs/development/tools/build-managers/meson/default.nix +++ b/pkgs/development/tools/build-managers/meson/default.nix @@ -47,10 +47,10 @@ python3Packages.buildPythonApplication rec { crossFile = writeTextDir "cross-file.conf" '' [binaries] - c = '${stdenv.cc.targetPrefix}cc' - cpp = '${stdenv.cc.targetPrefix}c++' - ar = '${stdenv.cc.bintools.targetPrefix}ar' - strip = '${stdenv.cc.bintools.targetPrefix}strip' + c = '${stdenv.targetPlatform.config}-cc' + cpp = '${stdenv.targetPlatform.config}-c++' + ar = '${stdenv.targetPlatform.config}-ar' + strip = '${stdenv.targetPlatform.config}-strip' pkgconfig = 'pkg-config' [properties] @@ -70,7 +70,7 @@ python3Packages.buildPythonApplication rec { inherit (stdenv) cc; - isCross = stdenv.buildPlatform != stdenv.hostPlatform; + isCross = stdenv.targetPlatform != stdenv.hostPlatform; meta = with lib; { homepage = http://mesonbuild.com;