#! /usr/bin/perl # ex:ts=8 sw=4: # $OpenBSD: proot,v 1.74 2023/05/06 05:21:15 espie Exp $ # # Copyright (c) 2016 Marc Espie # # Permission to use, copy, modify, and distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. use v5.36; my $ports1; use FindBin; BEGIN { $ports1 = $ENV{PORTSDIR} || '/usr/ports'; } use lib ("$ports1/infrastructure/lib", "$FindBin::Bin/../lib"); use OpenBSD::Getopt; use OpenBSD::Paths; use OpenBSD::State; use OpenBSD::ProgressMeter; use DPB::User; package MyUser; our @ISA = qw(DPB::User); use File::Basename; use File::Find; sub new($class, @r) { return $class->SUPER::new(@r)->enforce_local; } sub mkpath($self, $dir) { my $p = dirname($dir); if (! -d $p) { $self->mkpath($p); } $self->mkdir($dir); } sub mkRpath($self, $dir, @except) { my %dont = map {($_, 1)} @except; if (-d $dir) { find(sub() { if ($dont{$_}) { $File::Find::prune = 1; return; } $self->own($_); }, $dir); } else { $self->mkpath($dir); } } sub own($self, $path) { chown $self->{uid}, $self->{gid}, $path; } sub mkdir($self, $dir) { mkdir($dir); $self->own($dir); } package MyState; our @ISA = (qw(OpenBSD::State)); use File::Copy; use File::Spec; use File::Basename; use File::Find; use File::Path qw(remove_tree); sub set_chroot($state) { for my $item (keys %{$state->{dontrm}}) { $state->add_chroot_preserved($item); } } sub fatal_error($self, @r) { $self->errsay(@r); $self->{error} = 1; } sub add_chroot_preserved($state, $item) { $state->{chroot_dont}{$state->chroot($item)} = 1; } sub add_preserved($state, $item) { $state->{dontrm}{$item} = 1; # XXX chicken&egg issue: add_chroot_preserved requires # the chroot to be set, but we might mix preserve/chroot # options, so set_chroot will fix up things after the fact, # based on dontrm keys # the exists test prevents early preserve options to fail # miserably. if (exists $state->{chroot_dont}) { $state->add_chroot_preserved($item); } } sub canonical_dir($state, $path) { $path = File::Spec->canonpath($path); $path =~ s,/+$,,; return $path; } sub do_parm($state, $k, $v) { my $opts = { chroot => sub() { $state->{chroot} = File::Spec->canonpath($v); }, srcroot => sub() { $state->{srcroot} = File::Spec->canonpath($v); }, PORT_USER => sub() { $state->{portuser} = MyUser->new($v); }, LOG_USER => sub() { $state->{loguser} = MyUser->new($v); }, FETCH_USER => sub() { $state->{fetchuser} = MyUser->new($v); $state->{FETCH_USER} = $state->{fetchuser}->user; }, BUILD_USER => sub() { $state->{builduser} = MyUser->new($v); $state->{BUILD_USER} = $state->{builduser}->user; }, PORTSDIR => sub() { $state->{PORTSDIR} = File::Spec->canonpath($v); }, DISTDIR => sub() { $state->{DISTDIR} = File::Spec->canonpath($v); }, PACKAGE_REPOSITORY => sub() { $state->{PACKAGE_REPOSITORY} = File::Spec->canonpath($v); }, PLIST_REPOSITORY => sub() { $state->{PLIST_REPOSITORY} = File::Spec->canonpath($v); }, NFSDIR => sub() { $state->{DISTDIR} = File::Spec->canonpath("$v/distfiles"); $state->{PACKAGE_REPOSITORY} = File::Spec->canonpath("$v/packages"); $state->{PLIST_REPOSITORY} = File::Spec->canonpath("$v/plist"); }, LOCALDIR => sub() { $state->{WRKOBJDIR} = File::Spec->canonpath("$v/pobj"); $state->{LOCKDIR} = File::Spec->canonpath("$v/locks"); }, preserve => sub() { $state->add_preserved($v); }, portscvs => sub() { $state->{portscvs} = $v; }, ignorecvs => sub() { $state->{ignorecvs} = 1; }, chown_all => sub() { $state->{chown_all} = $v; }, WRKOBJDIR => sub() { $state->{WRKOBJDIR} = File::Spec->canonpath($v); }, DISTDIR => sub() { $state->{DISTDIR} = File::Spec->canonpath($v); }, LOCKDIR => sub() { $state->{LOCKDIR} = File::Spec->canonpath($v); }, LOGDIR => sub() { $state->{LOGDIR} = File::Spec->canonpath($v); }, snapshot => sub() { $state->{snapshot} = $v; }, trusted_snapshot => sub() { $state->{snapshot} = $v; $state->{trusted} = 1; }, actions => sub() { if ($v eq 'none') { delete $state->{actions}; } if ($v =~ m/^-(.*)/) { $state->{actions}{$1} = 0; } else { $state->{actions}{$v} = 1; } }, sets => sub() { if ($v =~ m/^-(.*)/) { $state->{sets}{$1} = 0; } else { $state->{sets}{$v} = 1; } }, extra => sub() { $state->{known}{$v} = 1; }, mkconf_tail => sub() { $state->{mkconf_tail} = $v; }, mkconf_lines => sub() { push(@{$state->{mkconf_lines}}, $v); } }; if (!defined $k) { $state->fatal_error("Can't specify option value without a key", $v); } if (defined $opts->{$k}) { &{$opts->{$k}}(); } else { $state->fatal_error("Unknown option #1=#2", $k, $v); } } sub read_configfile($state, $name) { open(my $f, '<', $name) or die "Can't open config $name: $!"; my ($k, $v); while (<$f>) { chomp; my $line = $_; s/\s*\#.*//; next if /^$/; if (m/^(.*?)\s*\=\s*(.*)$/) { ($k, $v) = ($1, $2); } elsif (m/^\s+(.*)$/) { $v = $1; } else { $state->fatal_error("Error in config file #1: #2", $., $line); next; } $state->do_parm($k, $v); } close($f); } sub handle_options($state) { # default shit $state->{actions} = { check_mount => 1, devs => 1, ldconfig => 1, ports_subdirs => 1, unpopulate_light => 1, resolv => 1, write_mk => 1, }; $state->{sets} = { }; $state->{error} = 0; $state->{no_exports} = 1; $state->{opt} = { B => sub($opt) { $state->{fchroot} = File::Spec->canonpath($opt); }, S => sub($opt) { $state->{fsrcroot} = File::Spec->canonpath($opt); }, c => sub($opt) { $state->read_configfile($opt); }, }; $state->SUPER::handle_options("B:S:c:mx", "[-B chroot] [-c configfile] [-S chroot] [arg=value...]"); # command-line trump configuration file my ($k, $v); for my $arg (@ARGV) { if ($arg =~ m/^(.*?)\s*\=\s*(.*)$/) { ($k, $v) = ($1, $2); } else { $v = $arg; } $state->do_parm($k, $v); } if (defined $state->{fchroot}) { $state->{chroot} = $state->{fchroot}; } $state->set_chroot; if (defined $state->{fsrcroot}) { $state->{srcroot} = $state->{fsrcroot}; } # more basic option defaults if (!defined $state->{actions}{snapshot} && !defined $state->{actions}{locate} && !defined $state->{actions}{none}) { if ($state->{snapshot}) { $state->{actions}{snapshot} = 1; } else { $state->{actions}{locate} = 1; } } if (defined $state->{portscvs} && !defined $state->{actions}{checkout_ports}) { $state->{actions}{checkout_ports} = 1; } if ($state->{actions}{unpopulate}) { delete $state->{actions}{unpopulate_light}; } if (!defined $state->{chroot}) { $state->usage("need a chroot base"); } if (defined $state->{actions}{checkout_ports} && !defined $state->{portuser}) { $state->usage("can't do action checkout_ports without a PORT_USER"); } if (defined $state->{snapshot} && $state->{snapshot} =~ m/^\Q$state->{chroot}\E(.*)/) { $state->add_preserved($1); } $state->{progressmeter} = OpenBSD::ProgressMeter->new; $state->{progressmeter}->setup($state->opt('x'), $state->opt('m'), $state); if ($< != 0) { $state->fatal("Must be root"); } for my $i (qw(PORTSDIR DISTDIR WRKOBJDIR PACKAGE_REPOSITORY PLIST_REPOSITORY LOCKDIR LOGDIR FETCH_USER BUILD_USER)) { if (defined $state->{$i}) { $state->{write}{$i} = 1; } } $state->{PORTSDIR} //= "/usr/ports"; $state->{DISTDIR} //= join('/', $state->{PORTSDIR}, 'distfiles'); $state->{WRKOBJDIR} //= join('/', $state->{PORTSDIR}, 'pobj'); $state->{LOCKDIR} //= join('/', $state->{WRKOBJDIR}, 'locks'); $state->{LOGDIR} //= join('/', $state->{PORTSDIR}, 'logs'); $state->{fetchuser} //= MyUser->new('_pfetch'); $state->{builduser} //= MyUser->new('_pbuild'); $state->{loguser} //= $state->{builduser}; $state->{PACKAGE_REPOSITORY} //= join('/', $state->{PORTSDIR}, 'packages'); $state->{PLIST_REPOSITORY} //= join('/', $state->{PORTSDIR}, 'plist'); $state->{sysdir} //= '/usr/src/sys'; for my $dir (qw(DISTDIR WRKOBJDIR LOGDIR PACKAGE_REPOSITORY PLIST_REPOSITORY LOCKDIR)) { $state->{$dir} = $state->canonical_dir($state->{$dir}); $state->add_preserved($state->{$dir}); } $state->{PORTSDIR} = $state->canonical_dir($state->{PORTSDIR}); if (!$state->{actions}{copy_ports}) { $state->add_preserved($state->{PORTSDIR}); } for my $i (qw(portuser loguser fetchuser builduser)) { if (defined $state->{$i}) { $state->say("#1: #2", $i, $state->{$i}{user}); } } for my $i (qw(chroot srcroot PORTSDIR DISTDIR WRKOBJDIR LOCKDIR LOGDIR PACKAGE_REPOSITORY PLIST_REPOSITORY)) { if (defined $state->{$i}) { $state->say("#1=#2", $i, $state->{$i}); } } } sub chroot($state, @components) { if (defined $state->{chroot}) { unshift @components, $state->{chroot}; } return File::Spec->canonpath(join('/', @components)); } sub srcroot($state, @components) { if (defined $state->{srcroot}) { unshift @components, $state->{srcroot}; } return File::Spec->canonpath(join('/', @components)); } sub chdir($state, $dir) { CORE::chdir($dir) or $state->fatal("Can't change to #1: #2", $dir, $!); } sub banner($self, $text, $sub) { $self->{progressmeter}->set_header($text); &$sub(); $self->{progressmeter}->next; } sub run_chroot($self, $text, $sub) { $self->{progressmeter}->set_header($text); my $r = fork(); if (!defined $r) { $self->fatal("Couldn't fork"); } if ($r == 0) { CORE::chroot($self->chroot) or $self->fatal("Can't chroot into #1: #2", $self->chroot, $!); CORE::chdir('/') or $self->fatal("Can't chdir after chroot: #1", $!); &$sub(); exit(0); } waitpid($r, 0); if ($? != 0) { $self->fatal("Running task failed"); } # XXX fork'd so we don't know what we showed eval { $self->{progressmeter}->forked; }; $self->{progressmeter}->next; } sub sync_display($self) { $self->{progressmeter}->clear if defined $self->{progressmeter}; } sub check_mountpoint($state) { open(my $cmd, "-|", OpenBSD::Paths->mount); my ($dev, $nosuid, $wx); while (<$cmd>) { chomp; if (m/^(\S+)\s+on\s+(\S+)\s+type\s+(\S+)\s+\((.+)\)$/) { my ($dir, $type, $options) = ($2, $3, $4); my %opts = map {($_, 1)} split(/,\s+/, $options); if ($opts{nodev}) { $dev->{$dir} = 0; } else { $dev->{$dir} = 1; } if ($opts{nosuid}) { $nosuid->{$dir} = 1; } else { $nosuid->{$dir} = 0; } if ($opts{wxallowed}) { $wx->{$dir} = 1; } else { $wx->{$dir} = 0; } my $devno = (stat $dir)[0]; if ($type eq 'nfs') { $state->{nfs}{$devno} = $dir; } } } close($cmd); my $root = $state->{chroot}; if (-l $root) { $root = readlink $root; } for my $dir (keys %$dev) { if ($dir =~ m/^\Q$root\E(\/.*)/) { $state->say("Preserve #1", $1); $state->add_preserved($1); my $devno = (stat $dir)[0]; $state->{dontrm_dev}{$devno} = 1; } } my $mnt = $root; while (1) { if (!defined $dev->{$mnt}) { my $up = dirname($mnt); if ($mnt eq $up) { last; } else { $mnt = $up; next; } } $state->errsay("#1 is under #2 which is nodev", $root, $mnt) if $dev->{$mnt} == 0; $state->errsay("#1 is under #2 which does not have nosuid", $root, $mnt) if $nosuid->{$mnt} == 0; $state->errsay("#1 is under #2 which does not have wxallowed", $root, $mnt) if $wx->{$mnt} == 0; return; } $state->fatal_error("Couldn't find mountpoint for #1 ???", $root); } sub special_data($state) { $state->{known}{'/etc/resolv.conf'} = 1; $state->{known}{'/etc/hosts'} = 1; } sub read_locates($state) { $state->banner("Running locate", sub { open(my $cmd, '-|', 'locate', '-d', $state->srcroot(OpenBSD::Paths->srclocatedb), '-d', $state->srcroot(OpenBSD::Paths->xlocatedb), ':'); while (<$cmd>) { chomp; my ($set, $path) = split(':', $_, 2); $set =~ s/\d+//; # next if $path =~ m/\.ph$/; unless (exists $state->{sets}{$set} && $state->{sets}{$set} == 0) { $state->{known}{$path} = 1; } $state->{progressmeter}->working(1000); } close($cmd); }); } sub library_sanity_check($state) { # create a quick repo containing the best libs. require OpenBSD::LibSpec; my $repo = {}; for my $libbase (OpenBSD::Paths->library_dirs) { my $libdir = "$libbase/lib"; opendir(my $dir, $libdir) or $state->fatal_error("can't read libdir #1"); while (my $e = readdir $dir) { next unless $e =~ m,^lib.*\.so\.\d+\.\d+$,; my $path = "$libdir/$e"; my $lib = OpenBSD::LibSpec->from_string($path); if (!$lib->is_valid) { $state->fatal_error("Couldn parse #1", $e); } my $k = $lib->key; my $current = $repo->{$k}; if (!defined $current || $current->compare($lib) < 0) { $repo->{$k} = $lib; } } } for my $path (keys %{$state->{known}}) { next unless $path =~ m,^/usr(?:/X11R6)?/lib/lib.*\.so\.\d+\.\d+$,; my $lib = OpenBSD::LibSpec->from_string($path); if (!$lib->is_valid) { $state->fatal_error("Couldn parse #1", $path); } my $k = $lib->key; my $current = $repo->{$k}; if (!defined $current) { $state->fatal_error("can't find library #1", $path); } if ($current->compare($lib) != 0) { my $l = $path =~ m/X11R6/ ? $state->srcroot(OpenBSD::Paths->xlocatedb) : $state->srcroot(OpenBSD::Paths->srclocatedb); $state->fatal_error("locate database #1 contains old library #2 vs actual system #3", $l, $path, $current->to_string); } } if ($state->{error}) { exit(1); } } # THIS IS THE WORK HORSE sub simple_copy($state, $path, $cpath, $user = undef) { $state->{accounted}{$path} = 1; if (-l $path) { return if $state->{chroot_dont}{$cpath}; remove_tree($cpath); my $target = readlink $path; if (!defined $target) { $state->fatal_error("Can't read link to #1: #2", $path, $!); } else { symlink($target, $cpath) or $state->fatal_error("Can't symlink #1 -> #2", $target, $cpath); } return; } else { my ($dev, $ino, $mode, $uid, $gid, $sz, $atime, $mtime) = (stat $path)[0, 1, 2, 4, 5, 7, 8, 9]; if (-d $path) { if (!-d $cpath && -e _) { return if $state->{chroot_dont}{$cpath}; remove_tree($cpath); } mkdir $cpath, $mode; if (!-d $cpath) { $state->fatal_error("Can't mkdir #1", $cpath); } if (defined $user) { $user->own($cpath); } else { chown $uid, $gid, $cpath; } chmod $mode, $cpath; utime $atime, $mtime, $cpath; return; } elsif (-f $path) { my ($dev2, $ino2, $sz2, $mtime2) = (stat $cpath)[0, 1, 7, 9]; if (defined $dev2 && $dev2 == $dev && $ino2 == $ino) { return; } my $key = "$dev/$ino"; if (exists $state->{copied}{$key}) { return if $state->{chroot_dont}{$cpath}; remove_tree($cpath); link($state->{copied}{$key}, $cpath); return; } # avoid the copy if same filesystem if (link($path, $cpath)) { return; } my $okay = 0; if (defined $sz2 && $sz2 == $sz && $mtime2 >= $mtime) { $okay = 1; } else { return if $state->{chroot_dont}{$cpath}; remove_tree($cpath); $okay = copy($path, $cpath); } if ($okay) { if (defined $user) { $user->own($cpath); } else { chown $uid, $gid, $cpath; } chmod $mode, $cpath; utime $atime, $mtime, $cpath; $state->{copied}{$key} = $cpath; return; } } } $state->fatal_error("Can't copy #1: #2", $path, $!); } sub recursive_copy($state, $path) { my $d = dirname($path); if ($d ne $path) { if (!-d $state->chroot($d)) { $state->recursive_copy($d); } } my $spath = $state->srcroot($path); if (!-e $spath) { $state->errsay("#1 does not exist", $spath); return; } $state->simple_copy($spath, $state->chroot($path)); } sub copy_sync($state) { # farting all over the place $state->banner("Copying stuff over", sub { my $old = umask; umask 0; for my $path (sort keys %{$state->{known}}) { $state->recursive_copy($path); $state->{progressmeter}->message($path); } umask $old; }); } sub best_user($state, $path) { local $_ = $path; if (m/^\Q$state->{LOGDIR}\E/) { return $state->{loguser}; } if (m/^\Q$state->{DISTDIR}\E\/build-stats/) { return $state->{loguser}; } if (m/^\Q$state->{PLIST_REPOSITORY}\E/) { return $state->{builduser}; } if (m/^\Q$state->{PACKAGE_REPOSITORY}\E/) { return $state->{builduser}; } if (m/^\Q$state->{DISTDIR}\E/) { return $state->{fetchuser}; } if (m/^\Q$state->{DISTDIR}\E/) { return $state->{fetchuser}; } return $state->{portuser}; } sub grab_file($state, $snapdir, $file) { my $baseurl = $state->{snapshot}; unless (-f $file) { if ($state->system('/usr/bin/ftp' , "-C", "-o", "$snapdir/$file.part", "$baseurl/$file") == 0) { rename("$snapdir/$file.part", "$snapdir/$file"); } else { $state->fatal("fetch #1 failed", $file); } } } sub extract_archive($state, $dir, $archive) { require IO::Uncompress::AnyUncompress; my $in = IO::Uncompress::AnyUncompress->new("$dir/$archive", MultiStream => 1); if (!$in) { $state->fatal("Couldn't open #1", "$dir/$archive"); } require OpenBSD::Ustar; my $arc = OpenBSD::Ustar->new($in, $state, $state->chroot); while (my $o = $arc->next) { $o->{name} =~ s,^\./,/,; if (defined $o->{linkname}) { $o->{linkname} =~ s,^\./,/,; } unlink($state->chroot($o->{name})); # $state->say("$o->{name}"); $state->{progressmeter}->message("$archive: $o->{name}"); $state->{accounted}{$o->{name}} = 1; $o->create; } $arc->close; $in->close; } sub find_snapshot_version($state, $snapdir, $check) { open my $f, '<', "$snapdir/$check" or $state->fatal("no $check"); my $v; while (<$f>) { if (m/^SHA256\s+\((.*\D)(\d+)\.tgz\)\s+\=/) { $state->{sets}{$1} //= 1; $v = $2; } } if (!defined $v) { $state->fatal("Unrecognized snapshot: no version in $check"); } if ($state->{sets}{base}) { $state->{sets}{etc} //= 1; } if ($state->{sets}{xbase}) { $state->{sets}{xetc} //= 1; } return $v; } sub get_snapshot($state) { $state->banner("Grabbing snapshot", sub { my ($snapdir, $grab); if ($state->{snapshot} =~ m/^(https?|ftp):/) { $snapdir = $state->chroot('/tmp'); File::Path::make_path($snapdir); $grab = sub { $state->grab_file($snapdir, shift); } } else { $snapdir = $state->{snapshot}; $grab = sub {}; } my $check = $state->{trusted} ? "SHA256" : "SHA256.sig"; &$grab($check); my $v = $state->find_snapshot_version($snapdir, $check); my (@files, @later); for my $set (sort keys %{$state->{sets}}) { if ($set =~ m/etc/) { push(@later, "$set.tgz"); } else { my $file = "$set$v.tgz"; &$grab($file); push(@files, $file); } } if (!$state->{trusted} && $state->system( sub { $state->chdir($snapdir); }, '/usr/bin/signify', '-C', '-p', "/etc/signify/openbsd-$v-base.pub", '-x', $check, @files) != 0) { $state->fatal("Checksum error"); } for my $archive (@files) { $state->extract_archive($snapdir, $archive); } for my $archive (@later) { $state->extract_archive($state->chroot("var/sysmerge"), $archive); } if ($state->{snapshot} =~ m/^(https?|ftp):/) { for my $file (@files) { unlink("$snapdir/$file"); } } }); } sub copy_ports($state) { $state->banner("Copying ports", sub { my $old = umask; umask 0; my $portsdir = $state->{PORTSDIR}; my $srcports = $state->srcroot($portsdir); if (-l $srcports) { $srcports = readlink $srcports; } my $destports = $state->chroot($portsdir); find(sub { if ($state->{ignorecvs} && $_ eq 'CVS' && -d $_) { $File::Find::prune = 1; return; } my $realname = $File::Find::name; $realname =~ s,^\Q$srcports\E\b,,; my $r2 = $realname; $r2 =~ s,^,$portsdir,; if ($r2 eq $state->{WRKOBJDIR}) { $File::Find::prune = 1; return; } $realname =~ s,^/,,; $state->{progressmeter}->message($realname); $state->simple_copy($File::Find::name, $state->chroot($r2), $state->best_user($r2)); }, $srcports); umask $old; }); } sub copy_sys($state) { $state->banner("Copying sys includes", sub { my $old = umask; umask 0; my $srcsys = $state->srcroot($state->{sysdir}); my $destsys = $state->chroot($state->{sysdir}); find(sub { # XXX don't bother copying stuff that's NOT .h return if -f $_ && !m/\.h$/ && $_ ne 'Makefile'; if (-d _ && $_ eq 'obj' || $_ eq 'CVS') { $File::Find::prune = 1; return; } my $destname = $File::Find::name; $destname =~ s,^\Q$srcsys\E\b,$destsys,; $state->{progressmeter}->message($File::Find::name); $state->simple_copy($File::Find::name, $destname); }, $srcsys); umask $old; }); } sub regen_devs($state) { $state->banner("Generating devices", sub { $state->system( sub { $state->chdir($state->chroot("/dev")); }, '/bin/sh', './MAKEDEV', 'all'); $state->add_preserved('/dev'); }); $state->run_chroot("Generating devices db", sub { $state->system("/usr/sbin/dev_mkdb"); }); } sub run_ldconfig($state) { $state->banner("Running ldconfig", sub { $state->system( sub { CORE::chroot($state->chroot) or exit 1; }, 'ldconfig', '/usr/lib', '/usr/X11R6/lib', '/usr/local/lib'); $state->{accounted}{'/var/run/ld.so.hints'} = 1; }); } sub checkout_ports($state) { $state->banner("Checking out ports tree", sub { my @cvs = ("/usr/bin/cvs", "-z3", "-d", $state->{portscvs}); my $dir = $state->chroot($state->{PORTSDIR}); if (-d "$dir/CVS") { $state->{portuser}->run_as(sub { $state->system( sub { $state->chdir($dir); }, @cvs, "update", "-dP", "-A"); }); } else { # okay, so stupid cvs creates the dir, work-around that $state->{portuser}->mkpath("$dir.tmp"); $state->{portuser}->run_as(sub { $state->system( sub { $state->chdir("$dir.tmp"); }, @cvs, "co", "-P", "-A", "ports"); }); rename("$dir.tmp/ports", "$dir"); rmdir("$dir.tmp"); } }); } sub unpopulate_chroot($state) { $state->run_chroot("Cleaning up files and directories", sub { my @dirs; find( sub { my $devno = (lstat $_)[0]; if (!defined $devno || $state->{dontrm_dev}{$devno} || $state->{dontrm}{$File::Find::name}) { $File::Find::prune = 1 if -d _; return; } if ($state->{accounted}{$File::Find::name}) { return; } $state->{progressmeter}->message($File::Find::name); if (-l _ || ! -d _) { unlink($_); } else { push(@dirs, $File::Find::name); } }, '/'); for my $dir (reverse @dirs) { #$state->say("rmdir #1", $dir); rmdir $dir; } }); } sub verbose_shit($state) { $state->run_chroot("Showing up stragglers", sub { find( sub { if (-d $_ && $state->{dontrm}{$File::Find::name}) { $File::Find::prune = 1; return; } if ($state->{dontrm}{$File::Find::name}) { return; } if ($state->{accounted}{$File::Find::name} || $File::Find::name eq '/') { return; } $state->say("#1", $File::Find::name); }, '/'); }); } sub show_absolute_symlinks($state) { my $wrapper = '/usr/sbin/mailwrapper'; my $expected = { '/etc/localtime' => undef, '/etc/rmt' => '/usr/sbin/rmt', '/etc/termcap' => '/usr/share/misc/termcap', '/usr/bin/hoststat' => $wrapper, '/usr/bin/mailq' => $wrapper, '/usr/bin/newaliases' => $wrapper, '/usr/bin/purgestat' => $wrapper, '/usr/bin/rcs2log' => '/usr/libexec/cvs/contrib/rcs2log', '/usr/local/lib/X11/app-defaults' => '/etc/X11/app-defaults', '/usr/sbin/makemap' => $wrapper, '/usr/sbin/sendmail' => $wrapper, }; $state->run_chroot("Showing absolute symlinks", sub { find( sub { return unless -l $_; my $dest = readlink $_; return unless $dest =~ m/^\//; my $ok = 0; my $n = $File::Find::name; if (exists $expected->{$n}) { $ok = 1; if (defined $expected->{$n} and $expected->{$n} ne $dest) { $ok = 0; } } if (!$ok) { $state->errsay("#1 points to #2", $n, $dest); } }, '/'); }); } sub is_under_nfs($state, $dir) { my $devno = (stat $dir)[0]; if (defined $devno && exists $state->{nfs}{$devno}) { return 1; } else { return 0; } } use constant { WHINE => 1, MKPATH => 2 }; sub build_dir($state, $opts, $username, $dirname, @r) { $state->{progressmeter}->message($dirname); my $dir = join('/', $state->{$dirname}, @r); $state->{$username}->mkpath($dir); my $nfs = $state->is_under_nfs($dir); if (($opts & WHINE) && $nfs) { $state->errsay("#1 (#2) is under nfs", $dirname, $dir); } if ($state->{chown_all} && !$nfs && !($opts & MKPATH)) { $state->{$username}->mkRpath($dir); } } sub make_ports_subdirs($state) { $state->run_chroot("Adjusting ports directories", sub { $state->build_dir(0, "fetchuser", "DISTDIR"); if (defined $state->{loguser}) { $state->build_dir(0, "loguser", "LOGDIR"); $state->build_dir(0, "loguser", "DISTDIR", "build-stats"); } $state->build_dir(WHINE|MKPATH , "builduser", "WRKOBJDIR"); $state->build_dir(0, "builduser", "PACKAGE_REPOSITORY"); $state->build_dir(0, "builduser", "PLIST_REPOSITORY"); $state->build_dir(WHINE, "builduser", "LOCKDIR"); }); } sub write_mk_conf($state) { $state->banner("Writing mk.conf", sub { open(my $f, ">", $state->chroot("/etc/mk.conf")); print $f "# Automatically generated by $state->{cmd}\n\n"; for my $i (sort keys %{$state->{write}}) { print $f "$i = $state->{$i}\n"; } print $f "PORTS_PRIVSEP = Yes\n"; print $f "SUDO = doas\n"; if (exists $state->{mkconf_lines}) { print $f "\n"; for my $l (@{$state->{mkconf_lines}}) { print $f $l, "\n"; } } if (exists $state->{mkconf_tail}) { open(my $g, "<", $state->{mkconf_tail}) or $state->fatal_error("Couldn't read tail #1: #2", $state->{mkconf_tail}, $!); print $f "# Copied from $state->{mkconf_tail}\n"; while(<$g>) { print $f $_; } close $g; } close($f); }); } package main; my $state = MyState->new; $state->handle_options; if ($state->{actions}{check_mount}) { $state->check_mountpoint; } if ($state->{actions}{unpopulate}) { $state->unpopulate_chroot; } if ($state->{actions}{locate}) { $state->read_locates; $state->library_sanity_check; } if ($state->{actions}{snapshot}) { $state->get_snapshot; } if ($state->{actions}{resolv}) { $state->special_data; } if (defined $state->{known}) { $state->copy_sync; } if ($state->{actions}{copy_ports}) { $state->copy_ports; } if ($state->{actions}{copy_sys}) { $state->copy_sys; } if ($state->{actions}{unpopulate_light}) { $state->unpopulate_chroot; } if ($state->{actions}{ldconfig}) { $state->run_ldconfig; } if ($state->{actions}{devs}) { $state->regen_devs; } if ($state->{actions}{checkout_ports}) { $state->checkout_ports; } if ($state->{actions}{ports_subdirs}) { $state->make_ports_subdirs; } if ($state->{actions}{stragglers}) { $state->verbose_shit; } if ($state->{actions}{write_mk}) { $state->write_mk_conf; } if ($state->{actions}{check_symlinks}) { $state->show_absolute_symlinks; } exit($state->{error});