# ex:ts=8 sw=4: # $OpenBSD: Config.pm,v 1.98 2023/09/29 09:12:15 naddy Exp $ # # Copyright (c) 2010-2013 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; # all the code responsible for handling command line options and the # config file. package DPB::Config; use DPB::User; use DPB::PortInfo; sub setup_users($class, $state) { for my $u (qw(build_user log_user fetch_user port_user)) { my $U = uc($u); if ($state->defines($U)) { $state->{$u} = DPB::User->new($state->defines($U)); } if (defined $state->{$u}) { if ($state->defines("DIRMODE")) { $state->{$u}{dirmode} = oct($state->defines("DIRMODE")); } if ($state->defines("DROPPRIV")) { $state->{$u}{droppriv} = $state->defines("DROPPRIV"); } } } my $u = DPB::User->new('_dpb'); if (defined $u->{uid}) { $state->{unpriv_user} = $u; } else { $state->fatal("No _dpb user"); } $state->{unpriv_user}->enforce_local; $> = $state->{unpriv_user}{uid}; $) = $state->{unpriv_user}{grouplist}; } sub parse_command_line($class, $state) { $state->{dontclean} = {}; $state->{opt} = { A => sub($opt) { $state->{arch} = $opt; }, L => sub($opt) { $state->{flogdir} = $opt; }, l => sub($opt) { $state->{flockdir} = $opt; }, r => sub() { $state->{random} = 1; $state->heuristics->random; }, P => sub($opt) { push(@{$state->{paths}}, $opt); }, I => sub($opt) { push(@{$state->{ipaths}}, $opt); }, C => sub($opt) { push(@{$state->{cpaths}}, $opt); }, X => sub($opt) { push(@{$state->{xpaths}}, $opt); }, b => sub($opt) { push(@{$state->{build_files}}, $opt); }, h => sub($opt) { push(@{$state->{config_files}}, $opt); }, }; $state->SUPER_handle_options('acemNqrRstuUvh:S:xX:A:B:C:f:F:I:j:J:M:p:P:b:l:L:', "[-acemNqrRsuUvx] [-A arch] [-B chroot] [-C plist] [-f m] [-F m]", "[-I pathlist] [-J p] [-j n] [-p parallel] [-P pathlist] [-h hosts]", "[-L logdir] [-l lockdir] [-b log] [-M threshold] [-X pathlist]", "[pathlist ...]"); for my $l (qw(j f F)) { my $o = $state->{opt}{$l}; if (defined $o && $o !~ m/^\d+$/) { $state->usage("-$l takes an integer argument, not $o"); } } $state->{roach} = $state->opt('N'); # for "new" $state->{chroot} = $state->opt('B'); $state->{base_user} = DPB::User->from_uid($<); if (!defined $state->{base_user}) { $state->usage("Can't figure out who I am"); } if ($state->{base_user}{uid} == 0) { $state->{noportsprivsep} = 1; } else { $state->errsay("Warning: dpb started as #1", $state->{base_user}->user); $state->errsay("Warning: running dpb as root with a build_user is the preferred setup"); } $class->setup_users($state); ($state->{ports}, $state->{localarch}, $state->{distdir}) = DPB::Vars->get(DPB::Host::Localhost->getshell($state), $state, "PORTSDIR", "MACHINE_ARCH", "DISTDIR"); if (!defined $state->{ports}) { $state->usage("Can't obtain vital information from the ports tree"); } $state->{arch} //= $state->{localarch}; if (defined $state->{opt}{F}) { if (defined $state->{opt}{j} || defined $state->{opt}{f}) { $state->usage("Can't use -F with -f or -j"); } $state->{fetch_only} = 1; $state->{opt}{f} = $state->{opt}{F}; } if (defined $state->opt('j')) { if ($state->localarch ne $state->arch) { $state->usage( "Can't use -j if -A arch is not local architecture"); } } $state->{realports} = $state->anchor($state->{ports}); $state->{realdistdir} = $state->anchor($state->{distdir}); if (defined $state->{config_files}) { for my $f (@{$state->{config_files}}) { $f = $state->expand_path($f); } } # keep cmdline subst values my %cmdline = %{$state->{subst}}; $class->parse_config_files($state); # ... as those must override the config files contents while (my ($k, $v) = each %cmdline) { $state->{subst}->{$k} = $v; } $class->setup_users($state); $state->{build_user} //= $state->{default_prop}{build_user}; if ($state->{base_user}{uid} == 0) { $state->{fetch_user} //= DPB::User->new('_pfetch'); } if (!defined $state->{port_user}) { my ($uid, $gid) = (stat $state->{realports})[4,5]; $state->{port_user} = DPB::User->from_uid($uid, $gid); } if (!defined $state->{build_user}) { $state->{build_user} = $state->{base_user}; } $state->{log_user} //= $state->{build_user}; $state->{fetch_user} //= $state->{build_user}; $state->{log_user}->enforce_local; $state->{chroot} = $state->{default_prop}{chroot}; # reparse things properly now that we can chroot my $backup; ($state->{ports}, $state->{portspath}, $state->{repo}, $state->{localarch}, $state->{distdir}, $state->{localbase}, $backup, $state->{fetch_cmd}) = DPB::Vars->get(DPB::Host::Localhost->getshell($state), $state, "PORTSDIR", "PORTSDIR_PATH", "PACKAGE_REPOSITORY", "MACHINE_ARCH", "DISTDIR", "LOCALBASE", "SITE_BACKUP", "FETCH_CMD"); if (!defined $state->{portspath}) { $state->usage("Can't obtain vital information from the ports tree"); } $state->{backup_sites} = [AddList->make_list($backup)]; $state->{portspath} = [ map {$state->anchor($_)} split(/:/, $state->{portspath}) ]; $state->{realports} = $state->anchor($state->{ports}); $state->{realdistdir} = $state->anchor($state->{distdir}); if (!defined $state->{port_user}) { my ($uid, $gid) = (stat $state->{realports})[4,5]; $state->{port_user} = DPB::User->from_uid($uid, $gid); } $state->say("Started as: #1", $state->{base_user}->user); $state->say("Port user: #1", $state->{port_user}->user); $state->say("Build user: #1", $state->{build_user}->user); $state->say("Fetch user: #1", $state->{fetch_user}->user); $state->say("Log user: #1", $state->{log_user}->user); $state->say("Unpriv user#1: #2", $state->{base_user}{uid} == 0 ? "" : "(unused)", $state->{unpriv_user}->user); $state->{logdir} = $state->{flogdir} // $ENV{LOGDIR} // '%p/logs/%a'; $state->{lockdir} //= $state->{flockdir} // "%L/locks"; $state->{logdir} = $state->expand_path($state->{logdir}); $state->{size_log} = "%f/build-stats/%a-size"; if ($state->define_present('LOGDIR')) { $state->{logdir} = $state->{subst}->value('LOGDIR'); } if ($state->define_present('CONTROL')) { if ($state->{subst}->value('CONTROL') ne '') { require DPB::External; $state->{external} = DPB::External->server($state); die if !defined $state->{external}; } } else { require DPB::External; $state->{subst}->add('CONTROL', '%L/control-%h-%$'); $state->{external} = DPB::External->server($state); } if ($state->define_present('LISTING_HOST')) { $state->{listing_host} = $state->{subst}->value('LISTING_HOST'); } $state->{external} //= DPB::ExternalStub->new; if ($state->{opt}{s}) { $state->{wantsize} = 1; } elsif ($state->define_present('WANTSIZE')) { $state->{wantsize} = $state->{subst}->value('WANTSIZE'); } elsif (DPB::HostProperties->has_mem) { $state->{wantsize} = 1; } if ($state->define_present('COLOR')) { $state->{color} = $state->{subst}->value('COLOR'); } if ($state->define_present('NO_CURSOR')) { $state->{nocursor} = $state->{subst}->value('NO_CURSOR'); } if (DPB::HostProperties->has_mem || $state->{wantsize}) { require DPB::Heuristics::Size; $state->{sizer} = DPB::Heuristics::Size->new($state); } else { require DPB::Heuristics::Nosize; $state->{sizer} = DPB::Heuristics::Nosize->new($state); } if ($state->define_present('FETCH_JOBS') && !defined $state->{opt}{f}) { $state->{opt}{f} = $state->{subst}->value('FETCH_JOBS'); } if ($state->define_present('LISTING_EXTRA') && !defined $state->{opt}{e}) { $state->{opt}{e} = $state->{subst}->value('LISTING_EXTRA'); } if ($state->define_present('ROACH') && !defined $state->{opt}{N}) { $state->{roach} = $state->{subst}->value('ROACH'); } if ($state->define_present('LOCKDIR')) { $state->{lockdir} = $state->{subst}->value('LOCKDIR'); } if ($state->define_present('TESTS')) { $state->{tests} = $state->{subst}->value('TESTS'); } if ($state->{subst}->value('NEVER_CLEAN')) { $state->{never_clean} = 1; } if ($state->define_present('FETCH_CMD')) { $state->{fetch_cmd} = $state->{subst}->value('FETCH_CMD'); } if ($state->{flogdir}) { $state->{logdir} = $state->{flogdir}; } if ($state->{flockdir}) { $state->{lockdir} = $state->{flockdir}; } if ($state->{opt}{t}) { $state->{tests} = 1; } if ($state->define_present('STATS_BACKLOG')) { $state->{stats_backlog} = $state->{subst}->value('STATS_BACKLOG'); } $state->{stats_backlog} //= 25; if ($state->define_present('STATS_USED')) { $state->{stats_used} = $state->{subst}->value('STATS_USED'); } $state->{stats_used} //= 10; $state->{opt}{f} //= 2; if ($state->opt('f')) { $state->{want_fetchinfo} = 1; } my $k = "STARTUP"; if ($state->define_present($k)) { $state->{startup_script} = $state->expand_chrooted_path($state->{subst}->value($k)); } # redo this in case config files changed it $state->{logdir} = $state->expand_path($state->{logdir}); if ($state->define_present("RECORD")) { $state->{record} = $state->{subst}->value("RECORD"); } $state->{record} //= "%L/term-report.log"; $state->{record} = $state->expand_path($state->{record}); $state->{size_log} = $state->expand_path($state->{size_log}); $state->{lockdir} = $state->expand_path($state->{lockdir}); for my $cat (qw(build_files paths ipaths cpaths xpaths)) { next unless defined $state->{$cat}; for my $f (@{$state->{$cat}}) { $f = $state->expand_path($f); } } if (!$state->{subst}->value("NO_BUILD_STATS")) { $state->{permanent_log} = $state->expand_path("%f/build-stats/%a"); push(@{$state->{build_files}}, $state->{permanent_log}); } $state->{dependencies_log} = $state->expand_path("%f/build-stats/%a-dependencies"); $state->{display_timeout} = $state->{subst}->value('DISPLAY_TIMEOUT') // 10; if ($state->defines("DONT_BUILD_ONCE")) { $state->{build_once} = 0; } if ($state->define_present('MIRROR')) { $state->{mirror} = $state->{subst}->value('MIRROR'); } else { $state->{mirror} = $state->{fetch_only}; } $state->{fullrepo} = join("/", $state->{repo}, $state->arch, "all"); } sub command_line_overrides($class, $state) { my $override_prop = DPB::HostProperties->new; if (defined $state->{base_user}) { $override_prop->{base_user} = $state->{base_user}; } if (defined $state->{port_user}) { $override_prop->{port_user} = $state->{port_user}; } if (defined $state->{build_user}) { $override_prop->{build_user} = $state->{build_user}; } if (!$state->{subst}->empty('HISTORY_ONLY')) { $state->{want_fetchinfo} = 1; $state->{opt}{f} = 0; $state->{opt}{j} = 1; $state->{opt}{e} = 1; $state->{all} = 1; $state->{scan_only} = 1; # XXX not really random, but no need to use dependencies $state->{random} = 1; } if ($state->opt('j')) { $override_prop->{jobs} = $state->opt('j'); } if ($state->opt('p')) { $override_prop->{parallel} = $state->opt('p'); } if ($state->opt('B')) { $override_prop->{chroot} = $state->opt('B'); } if ($state->define_present('STUCK_TIMEOUT')) { $override_prop->{stuck} = $state->{subst}->value('STUCK_TIMEOUT'); } if ($state->define_present('FETCH_TIMEOUT')) { $override_prop->{fetch_timeout} = $state->{subst}->value('FETCH_TIMEOUT'); } if ($state->define_present('SMALL_TIME')) { $override_prop->{small} = $state->{subst}->value('SMALL_TIME'); } if ($state->define_present('CONNECTION_TIMEOUT')) { $override_prop->{timeout} = $state->{subst}->value('CONNECTION_TIMEOUT'); } if ($state->opt('J')) { $override_prop->{junk} = $state->opt('J'); } if ($state->defines("ALWAYS_CLEAN")) { $override_prop->{always_clean} = 1; } if ($state->defines("FETCH_CMD")) { $override_prop->{FETCH_CMD} = $state->{subst}->value('FETCH_CMD'); } if ($state->opt('M')) { $override_prop->{mem} = $state->opt('M'); } if ($state->define_present('SYSLOG')) { require Sys::Syslog; Sys::Syslog::openlog('dpb', "nofatal"); $override_prop->{syslog} = 1; } return $override_prop; } sub parse_config_files($class, $state) { my $override_prop = $class->command_line_overrides($state); my $default_prop = { junk => 150, parallel => '/2', small => 120, repair => 1, nochecksum => 1, master_pid => $state->{master_pid}, }; if ($state->{config_files}) { for my $config (@{$state->{config_files}}) { $class->parse_hosts_file($config, $state, \$default_prop, $override_prop); } } my $prop = DPB::HostProperties->new($default_prop); $prop->finalize_with_overrides($override_prop); if (!$state->{config_files}) { DPB::Core::Init->new(DPB::Host->new('localhost', $prop)); } $state->{default_prop} = $prop; $state->{override_prop} = $override_prop; } sub parse_hosts_file($class, $filename, $state, $rdefault, $override) { open my $fh, '<', $filename or $state->fatal("Can't read hosts file #1: #2", $filename, $!); my $cores = {}; while (<$fh>) { chomp; s/\s*\#.*$//; next if m/^$/; if (m/^([A-Z_]+)\=\s*(.*)\s*$/) { $state->{subst}->add($1, $2); next; } if (defined $state->{build_user}) { $$rdefault->{build_user} //= $state->{build_user}; } # copy default properties my $prop = DPB::HostProperties->new($$rdefault); my ($host, @properties) = split(/\s+/, $_); for my $arg (@properties) { if ($arg =~ m/^(.*?)=(.*)$/) { $prop->{$1} = $2; } } if (defined $prop->{arch} && $prop->{arch} ne $state->arch) { next; } if ($host eq 'DEFAULT') { $$rdefault = { %$prop }; next; } $prop->finalize_with_overrides($override); DPB::Core::Init->new(DPB::Host->new($host, $prop)); if (defined $prop->{build_user} && !defined $state->{build_user} && !$state->defines("BUILD_USER")) { $state->{build_user} = $prop->{build_user}; } } } sub read_exceptions_file($class, $state, $filename, $default = 'build') { my $properties = {}; open my $fh, '<', $filename or $state->fatal("Can't read exceptions file #1: #2", $filename, $!); $state->{adjuncts} = {}; my @defaults = $default; while(<$fh>) { chomp; s/\#.*//; next if m/^\s*$/; my @paths; my @properties; for my $field (split(/\s+/, $_)) { if ($field =~ m/\//) { push(@paths, DPB::PkgPath->new($field)); } elsif (defined $properties->{$field}) { push(@properties, $field); } else { $state->fatal( "Unknown property in file #1 at #2: #3", $filename, $., $field); } } if (@properties == 0) { @properties = @defaults; } else { @defaults = @properties; } if (@paths == 0) { $state->fatal("No path in file #1 at #2: #3", $filename, $., $_); } for my $p (@properties) { for my $v (@paths) { &{$properties->{$p}}($v); } } } } sub add_host($class, $state, $host, @properties) { my $prop = DPB::HostProperties->new($state->{default_prop}); for my $arg (@properties) { if ($arg =~ m/^(.*?)=(.*)$/) { $prop->{$1} = $2; } } $prop->finalize_with_overrides($state->{override_prop}); DPB::Core::Init->init_core( DPB::Core::Init->new(DPB::Host->new($host, $prop)), $state); } package DPB::ExternalStub; sub new($class) { bless {}, $class; } sub receive_commands($) { } sub cleanup($) { } 1;