/snap/core22/2411/usr/lib/usrmerge
Edit: /snap/core22/2411/usr/lib/usrmerge/convert-usrmerge (14442B)
#!/usr/bin/perl
# vim: shiftwidth=4 tabstop=4
#
# Copyright 2014 by Marco d'Itri
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
use lib "/usr/lib/usrmerge/lib";
use warnings;
use strict;
use autodie;
use v5.16;
use File::Find::Rule;
use Cwd qw(abs_path);
use Errno;
my $Debug = 0;
my $Program_RC = 0;
check_free_space();
go_faster();
# print the long error message if something fails due to autodie
$SIG{__DIE__} = sub { fatal($_[0]); };
{
foreach my $name (early_conversion_files()) {
next if $name =~ m#^/usr/#; # already converted
convert_file($name);
}
my @dirs = directories_to_merge();
# create any directory which is in / but not in /usr
foreach my $dir (@dirs) {
mkdir("/usr$dir") if not -e "/usr$dir";
}
my @later;
my $rule = File::Find::Rule->mindepth(1)->maxdepth(1)->start(@dirs);
while (defined (my $name = $rule->match)) {
convert_file($name, \@later);
}
# symlinks must be converted after the rest to avoid races, because
# they may point to binaries which have not been converted yet
convert_file($_) foreach @later;
verify_links_only($_) foreach @dirs;
convert_directory($_) foreach @dirs;
exit($Program_RC) if $Program_RC;
print "The system has been successfully converted.\n";
exit;
}
##############################################################################
sub convert_file {
my ($n, $later) = @_;
print "==== $n ====\n" if $Debug;
# the source is a broken link
if (-l $n and not -e $n and -e "/usr$n") {
warn "WARNING: $n is a broken symlink and has been renamed!\n";
rename($n, "$n.usrmerge-broken");
return;
}
# the destination is a broken link
if (-l "/usr$n" and not -e "/usr$n") {
warn "WARNING: /usr$n is a broken symlink and has been renamed!\n";
rename("/usr$n", "/usr$n.usrmerge-broken");
# continue and move the source as usual
}
# is a directory and the destination does not exist
if (-d $n and not -e "/usr$n") {
mv($n, "/usr$n"); # XXX race
symlink("/usr$n", $n);
return;
}
# is a link and the destination does not exist, but defer the conversion
if (-l $n and not -e "/usr$n" and $later) {
push(@$later, $n);
return;
}
# is a file or link and the destination does not exist
if (not -e "/usr$n") {
cp($n, "/usr$n");
symlink("/usr$n", "$n~~tmp~usrmerge~~");
rename("$n~~tmp~usrmerge~~", $n);
# XXX alternative implementation:
#mv($n, "/usr$n"); # XXX race
#symlink("/usr$n", $n);
return;
}
# both source and dest are directories
if (-d $n and -d "/usr$n") {
# so they have to be merged recursively
my $rule = File::Find::Rule->mindepth(1)->maxdepth(1)->start($n);
while (defined (my $name = $rule->match)) {
convert_file($name, $later);
}
return;
}
# The other cases are more complex and there are some corner cases that
# we do not try to resolve automatically.
# both source and dest are links
if (-l $n and -l "/usr$n") {
my $l1 = readlink($n);
my $l2 = readlink("/usr$n");
my ($basedir) = $n =~ m#^(.+)/[^/]+$#;
return if $l1 eq $l2; # and they point to the same file
return if "$basedir/$l1" eq $l2; # same (the / link is relative)
return if $l1 eq "/usr$basedir/$l2";# same (the /usr link is relative)
return if $l1 eq "/usr$n"; # and the / link points to the /usr link
if ($l2 eq $n) { # and the /usr link points to the / link
# the target of the new link will be an absolute path, so it
# may be different from the original one even if both point to
# the same file
symlink(abs_path($n), "/usr$n~~tmp~usrmerge~~");
rename("/usr$n~~tmp~usrmerge~~", "/usr$n");
# convert the /bin link too to make the program idempotent
symlink(abs_path($n), "$n~~tmp~usrmerge~~");
rename("$n~~tmp~usrmerge~~", "$n");
return;
}
fatal("Both $n and /usr$n exist");
}
# the source is a link
if (-l $n) {
my $l = readlink($n);
return if $l eq "/usr$n"; # and it points to dest
fatal("Both $n and /usr$n exist");
}
# the destination is a link
if (-l "/usr$n") {
my $l = readlink("/usr$n");
if ($l eq $n) { # and it points to source
cp($n, "/usr$n~~tmp~usrmerge~~");
rename("/usr$n~~tmp~usrmerge~~", "/usr$n");
symlink("/usr$n", "$n~~tmp~usrmerge~~");
rename("$n~~tmp~usrmerge~~", $n);
# XXX alternative implementation:
#mv($n, "/usr$n"); # XXX race
#symlink("/usr$n", $n);
return;
}
fatal("Both $n and /usr$n exist");
}
# generated files
# if it was already re-generated in the new place, clear the legacy location
# Origin of generated files:
# /lib/udev/hwdb.bin -> 'systemd-hwdb --usr update'
my @generated_files = qw(
/lib/udev/hwdb.bin
);
if (-e "/usr$n" && $n ~~ @generated_files) {
rm('-f', "$n");
return;
}
fatal("$n is a directory and /usr$n is not")
if -d $n and -e "/usr$n";
fatal("/usr$n is a directory and $n is not")
if -d "/usr$n";
fatal("Both $n and /usr$n exist")
if -e "/usr$n";
fatal("The status of $n and /usr$n is really unexpected");
}
##############################################################################
# To prevent a failure later, the regular files of the libraries used by
# cp and mv must be converted before of the symlinks that point to them.
sub early_conversion_files {
open(my $fh, '-|', 'ldd /bin/cp');
my @ldd = <$fh>;
close $fh;
# the libraries
my @list = grep { $_ } map { /^\s+\S+ => (\S+) / and $1 } @ldd;
# the dynamic linker
push(@list, grep { $_ } map { /^\s+(\/\S+) \(/ and $1 } @ldd);
# this is the equivalent of readlink --canonicalize
my @newlist;
foreach my $name (@list) {
my $newname = -l $name ? readlink($name) : $name;
if ($newname !~ m#^/#) {
my $dir = $name;
$dir =~ s#[^/]+$##;
$newname = $dir . $newname;
}
my $topdir = $newname;
$topdir =~ s#^(/[^/]+).*#$1#;
# this is needed to be idempotent after a complete conversion
next if -l $topdir;
push(@newlist, $newname);
}
return @newlist;
}
##############################################################################
# Safety check: verify that there are no regular files in the directories
# that will be deleted by the final pass.
sub verify_links_only {
my ($dir) = @_;
my $link_or_dir = File::Find::Rule->or(
File::Find::Rule->symlink,
File::Find::Rule->directory,
);
my $rule = File::Find::Rule->mindepth(1)->not($link_or_dir)->start($dir);
while (defined (my $name = $rule->match)) {
print STDERR "$name is not a symlink!\n";
print STDERR "\nSafety check: the conversion has failed!\n";
exit 1;
}
}
sub convert_directory {
my ($dir) = @_;
return if -l $dir;
if (not -d $dir) {
symlink("usr$dir", $dir);
restore_context($dir);
return;
}
no autodie;
if (not rename($dir, "$dir~~delete~usrmerge~~")) { # XXX race
if ($!{EBUSY}) {
handle_ebusy($dir);
return;
}
die "Can't rename $dir: $!";
}
use autodie;
symlink("usr$dir", $dir);
restore_context($dir);
rm('-rf', "$dir~~delete~usrmerge~~");
}
sub restore_context {
my ($file) = @_;
return if not -x '/sbin/restorecon';
safe_system('restorecon', $file);
}
sub handle_ebusy {
my ($dir) = @_;
print STDERR <;
die "stat / failed" if not defined $root_id;
chomp $root_id;
# beware: df(1) reports the value of %a, not of %f
open($fh, '-|',
'stat --dereference --file-system --format="%f %S %i" /usr/');
my $stat_output = <$fh>;
die "stat /usr failed" if not defined $stat_output;
chomp $stat_output;
my ($free_blocks, $bs, $usr_id) = split(/ /, $stat_output);
return if $root_id eq $usr_id;
my $free = $free_blocks * ($bs / 1024);
my @dirs = grep { -e $_ } directories_to_merge();
open($fh, '-|',
"du --summarize --no-dereference --total --block-size=1K @dirs");
my $needed;
while (<$fh>) {
($needed) = /^(\d+)\s+total$/;
}
close $fh;
die "df @dirs failed" if not defined $needed;
say "Free space in /usr: $free KB." if $Debug;
say "The origin directories (@dirs) require $needed KB." if $Debug;
die "$free KB in /usr but $needed KB are required!\n" if $needed > $free;
}
##############################################################################
# Try to avoid the inherent races by being as fast as possible.
sub go_faster {
system('/usr/bin/ionice', '--class=realtime', "--pid=$$")
if -x '/usr/bin/ionice';
system('/usr/bin/chrt', '--rr', '-p', '99', '--pid', $$)
if -x '/usr/bin/chrt';
}
# We use cp from coreutils because it preserves extended attributes and
# handles sparse files.
sub cp {
my ($source, $dest) = @_;
safe_system('cp', '--no-dereference', '--preserve=all',
'--reflink=auto', '--sparse=always', $source, $dest);
}
# We use mv from coreutils because it supports moving directories across
# filesystems, which would be inconvenient to implement here.
sub mv {
my ($source, $dest) = @_;
safe_system('mv', '--no-clobber', $source, $dest);
}
# We use rm from coreutils because I cannot be bothered to implement -r.
sub rm {
safe_system('rm', @_);
}
sub safe_system {
my (@cmd) = @_;
my $rc = system(@cmd);
die "Failed to execute @cmd: $!\n" if $rc == -1;
die "@cmd: rc=" . ($? >> 8) . "\n" if $rc;
}
sub fatal {
my ($msg) = @_;
$msg .= ".\n" if $msg !~ /\n$/;
print STDERR < is / and I is /usr.
Types of files:
=over 4
=item F: file
=item D: directory
=item L: symbolic link
=item B: broken symbolic link
=back
Actions:
=over 4
=item X: unresolvable conflict
=item D: recurse and compare the content of the directories
=item K: keep the source (if the link matches the destination)
=item S: swap source and destination (if the link matches the destination)
=item R: rename (source or destination)
=back
=head1 BUGS
Replacing a directory with a symlink is racy unless we do a complex dance
of bind mounts. We should decide if this is really needed.
Conflicting relative symbolic links are not handled automatically.
The libc6-i386 and libc6-x32 packages require to convert the /lib32 and
/libx32 directories as well, otherwise they would only contain the
symlink to the dynamic linker specified by the architecture ABI.
=head1 AUTHOR
The program and this man page have been written by Marco d'Itri
and are licensed under the terms of the GNU General Public License,
version 2 or higher.