# Copyright (C) 2005 Mandriva # Olivier Blin # Copyright (C) 2017 Mageia # Martin Whitaker # # 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, or (at your option) # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA. # SYNOPSIS # -------- # This package provides functions to create and customise the root filesystem # for a Live system. package MGA::DrakISO::BuildRoot; use strict; use MDK::Common; use common; use File::Temp qw(tmpnam); use Try::Tiny; use urpm; use urpm::media; use urpm::select; use MGA::DrakISO::LiveBuild; use MGA::DrakISO::Loopback; use MGA::DrakISO::Utils; use Exporter; our @ISA = qw(Exporter); our @EXPORT = qw(install_live_system customise_live_system); my $local_repo_dir = '/var/local/mga_rpms/'; ############################################################################### # System Installation ############################################################################### # This is the top-level function called to create the basic root filesystem. It # uses stage2 of the Mageia installer to do the actual work. It is independent # of any other preparatory step. # # This function is largely derived from drakx_in_chroot. # sub install_live_system { my ($build) = @_; print "Installing Live system\n" if $::verbose; my $arch = $build->{settings}{arch}; my $base_repository = $build->{settings}{repository}; my $arch_repository = $base_repository . '/' . $arch; my $remote_method = $arch_repository =~ m!^(ftp|http)://! && $1; my $chroot = $build->get_chroot_dir; my $rooted_stage2 = '/tmp/stage2'; my $chroot_stage2 = $chroot . $rooted_stage2; my $live_root = $build->get_live_root; if (-e $live_root) { # We want a clean start... umount_all_in_root($live_root); run_as_root('rm', '-rf', $live_root); } mkdir_p($live_root); my $Xserver_pid; my $error_message; try { # Mount the directory where we want to install the Live system. mount($chroot . '/mnt', $live_root, '--bind'); # Mount the standard system pseudo-filesystems, so that the installer # has a proper environment to run in. mount_system_fs($chroot); mount($chroot . '/sys/kernel/debug', 'none', '-t', 'debugfs'); # Mount the stage2 installer filesystem. if ($remote_method) { my $local_mdkinst = $chroot . '/tmp/mdkinst.sqfs'; run_('curl', '--silent', '-o', $local_mdkinst, $arch_repository . '/install/stage2/mdkinst.sqfs') or die "ERROR: failed to download mdkinst.sqfs from remote repository\n"; mount($chroot_stage2, $local_mdkinst, '-t', 'squashfs', '-o', 'loop,ro'); } elsif (-d $arch_repository . '/install/stage2/live') { mount($chroot_stage2, $arch_repository . '/install/stage2/live', '--bind', '-o', 'ro'); } elsif (-f $arch_repository . '/install/stage2/mdkinst.sqfs') { mount($chroot_stage2, $arch_repository . '/install/stage2/mdkinst.sqfs', '-t', 'squashfs', '-o', 'loop,ro'); } else { die "ERROR: failed to find installer stage2\n"; } # The stage2 installer expects to find the full repository in this # location... mount($chroot . '/tmp/media', $base_repository, '--bind', '-o', 'ro') if !$remote_method; # and the arch-specific repository in this location. symlinkf('media/' . $arch, $chroot . '/tmp/image'); # Because the installer uses the above symlink, relative paths in # the urpmi configuration don't work unless we add this extra link. symlinkf('media/i586', $chroot . '/tmp/i586') if $arch eq 'x86_64'; # If the user has provided an auto-install configuration file, use it, # otherwise construct one. my $rooted_auto_inst = '/tmp/auto_inst.cfg.pl'; my $system_auto_inst = $build->get_absolute_path($build->{system}{auto_install}); my $interactive; if (defined $system_auto_inst) { -f $system_auto_inst or die "ERROR: can't find the auto-install configuration file $system_auto_inst\n"; cp_f($system_auto_inst, $chroot . $rooted_auto_inst); } else { write_auto_inst_cfg($build, $chroot . $rooted_auto_inst, \$interactive); } # Add a few more things to complete the installer's working environment. my $etc = $chroot . '/etc'; mkdir_p($etc); output($etc . '/hosts', "127.0.0.1 localhost\n"); my $var = $chroot . '/var'; mkdir_p($var); create_initial_symlinks($chroot, $rooted_stage2); chomp(my $kernel_version = `uname -r`); my $modules_dir = '/modules/' . $kernel_version; output_p($chroot . $modules_dir . $_, "\n") foreach "/lib/$modules_dir/modules.dep", "/lib/$modules_dir/modules.alias"; # If some of the installer steps will be interactive, we need to start a # nested X server to display the GUI. my $DISPLAY = ''; if ($interactive) { my $Xserver = whereis_binary('Xephyr') || whereis_binary('Xnest') or die "ERROR: Xephyr or Xnest not found - cannot run installer GUI\n"; my $screen_size = $Xserver =~ /Xephyr/ ? '-screen' : '-geometry'; $DISPLAY = ':8'; if ($Xserver_pid = fork()) { } else { close(STDOUT); open(STDOUT, '>', '/dev/null'); close(STDERR); open(STDERR, '>', '/dev/null'); exec($Xserver, $DISPLAY, '-ac', $screen_size, '1024x768'); } } # Run the installer. The chroot command sets up a new environment, # so we need to set the variables we want after we've entered the # chroot. my $env = join(' ', "PATH=/usr/sbin:/mnt/usr/sbin:/usr/bin:/mnt/usr/bin", "LD_LIBRARY_PATH=/usr/lib:/mnt/usr/lib:/usr/lib64:/mnt/usr/lib64", if_($remote_method, "URLPREFIX=$arch_repository"), "DISPLAY=$DISPLAY", "TERM=linux", "HOME=/", ); my $cmd = "/usr/bin/runinstall2 --local_install --auto_install $rooted_auto_inst"; $cmd .= "--method $remote_method" if $remote_method; run_in_root($chroot, $arch, 'sh', '-c', "$env $cmd") or die "ERROR: failed to install base system\n"; # Add a local repository if the user has requested it. build_local_repo($build); } catch { $error_message = $_; } finally { # During package installation, a dbus daemon may get automatically # launched in the Live system root. We need to kill it before we # can unmount the root filesystem. run_as_root('fuser', '-s', '-k', "$chroot/mnt"); # Allow a bit of time for the processes to die. sleep(1); # Now we can unmount everything. The order is important. umount_all_in_root($live_root); umount_all_in_root($chroot); # And finally kill off our nested X server. kill(15, $Xserver_pid) if $Xserver_pid; }; defined $error_message && die $error_message; } sub write_auto_inst_cfg { my ($build, $file, $interactive) = @_; my $region = $build->{settings}{region}; my $enabled_media = $build->{system}{enabled_media}; my $rpmsrate_flags = $build->{system}{rpmsrate_flags}; my $rpmsrate_level = $build->{system}{rpmsrate_level} || 5; my $include_packages = $build->{system}{include_packages}; my $exclude_packages = $build->{system}{exclude_packages}; my $preferred_packages = $build->{system}{preferred_packages}; my $local_repo_packages = $build->{system}{local_repo_packages}; my @desktops = split(/\|/, $build->{settings}{desktop}); my $default_user = $build->{settings}{default_user}; my $post_install_nr = $build->{system}{post_install_nr}; my $post_install = $build->{system}{post_install}; $$interactive = !$region || !$enabled_media || !$rpmsrate_flags || !$default_user; my @text; push @text, ( "\$o = {", " security => 1,", " authentication => {", " shadow => 1,", " local => 1,", " blowfish => 1,", " },", ); push @text, ( " interactiveSteps => [", if_(!$region, " 'selectLanguage',", " 'selectKeyboard',", ), if_(!$enabled_media, " 'chooseMedia',", ), if_(!$rpmsrate_flags, " 'choosePackages',", ), if_(!$default_user, " 'addUser',", ), " ],", ) if $$interactive; push @text, ( " media => [", " {", " type => 'media_cfg',", " url => 'drakx://media',", " selected_names => '" . join(', ', @$enabled_media) . "',", " },", " ],", " # temporary (?) fix for mga#12299", " enabled_media => [ " . join(', ', map { "'$_'" } @$enabled_media) . " ],", ) if $enabled_media; push @text, ( " rpmsrate_flags_chosen => {", (map { " $_ => 1," } @$rpmsrate_flags), " },", " compssListLevel => $rpmsrate_level,", ) if $rpmsrate_flags; push @text, ( " default_packages => [", (map { " '$_'," } @$include_packages), " ],", ) if $include_packages; push @text, ( " skipped_packages => [", (map { " '$_'," } (@$exclude_packages, @$local_repo_packages)), " ],", ) if $exclude_packages; push @text, ( " preferred_packages => '" . join(', ', @$preferred_packages) . "',", ) if $preferred_packages; push @text, ( " meta_class => 'desktop',", " desktop => '$desktops[0]',", ) if @desktops; push @text, ( " autologin => '$default_user',", " users => [", " {", " realname => '',", " name => '$default_user',", " shell => '/bin/bash',", " icon => 'default',", " groups => [],", " gid => '',", " uid => '',", " },", " ],", " superuser => {", " pw => '',", " realname => 'root',", " shell => '/bin/bash',", " home => '/root',", " gid => '0',", " uid => '0',", " },", ) if $default_user; push @text, ( " locale => {", " lang => 'en_US',", " country => 'US',", " IM => undef,", if_($region ne 'all', " langs => { " . join(', ', map { "$_ => 1" } $build->get_langs) . " },", ), if_($region eq 'all', " langs => { all => 1 },", ), " utf8 => 1,", " },", " keyboard => {", " KEYBOARD => 'us',", " KEYTABLE => 'us',", " KBCHARSET => 'C',", " GRP_TOGGLE => '',", " },", " timezone => {", " ntp => undef,", " timezone => 'America/New_York',", " UTC => 1,", " },", ) if $region; push @text, ( " postInstallNonRooted => \"$post_install_nr\"," ) if $post_install_nr; push @text, ( " postInstall => \"$post_install\"," ) if $post_install; push @text, ( " X => { disabled => 1 },", " keep_unrequested_dependencies => 0,", " match_all_hardware => 1,", " autoExitInstall => 1,", "};", ); output($file, map { "$_\n" } @text); } sub create_initial_symlinks { my ($chroot, $rooted_stage2) = @_; my $chroot_stage2 = $chroot . $rooted_stage2; foreach my $line (cat_or_die($chroot_stage2 . '/usr/share/symlinks')) { my ($from, $to_) = split(' ', $line); my $to = $chroot . ($to_ || $from); $from = $rooted_stage2 . $from if !$to_; symlinkf($from, $to) or die "ERROR: symlinking $to failed\n"; } my $from = $rooted_stage2 . '/usr'; my $to = $chroot . '/usr'; symlinkf $from, $to or die "ERROR: symlinking $to failed\n"; foreach my $dir ('/bin', '/sbin', '/lib', '/lib64') { $from = 'usr' . $dir; $to = $chroot . $dir; symlinkf($from, $to) or die "ERROR: symlinking $to failed\n"; } } sub build_local_repo { my ($build) = @_; my $requested_packages = $build->{system}{local_repo_packages} || []; return if !@$requested_packages; print "Building local repository\n" if $::verbose > 1; my $root = $build->get_live_root; my $arch = $build->{settings}{arch}; # We aren't going to run in the chroot, so we need to adjust the paths # in the urpmi configuration. my $base_repository = $build->{settings}{repository}; my $arch_repository = $base_repository . '/' . $arch; my $i586_repository = $base_repository . '/' . 'i586'; run_as_root('sed', '-i', "s!/tmp/image!$arch_repository!", $root . '/etc/urpmi/urpmi.cfg'); run_as_root('sed', '-i', "s!/tmp/i586!$i586_repository!", $root . '/etc/urpmi/urpmi.cfg') if $arch eq 'x86_64'; # When we pass a chroot directory to urpmi, it wants a /dev/null in # that root. mk_dev_null($root); # Initialise a urpm object to let us query the database. my $urpm = urpm->new; urpm::set_files($urpm, $root); urpm::get_global_options($urpm); $urpm->{info} = sub { }; $urpm->{log} = sub { }; urpm::media::configure($urpm, cmdline_skiplist => join(',', @{$build->{system}{exclude_packages} || []})); print "..selecting RPMs\n" if $::verbose > 1; # For each requested package in turn, find any dependencies that aren't # already installed and add them to our hash of selected packages. Do # this for each package separately, because some of the requested packages # may conflict. my $error; my %selected; foreach my $name (@$requested_packages) { my (%requested, %state); urpm::select::search_packages($urpm, \%requested, [ $name ], no_substring => 1); if (!%requested) { print "ERROR: couldn't find '$name' package in selected media\n"; $error = 1; next; } urpm::select::resolve_dependencies($urpm, \%state, \%requested, auto_select => 1, no_recommends => 1); my @unselected = urpm::select::unselected_packages(\%state); if (@unselected) { print "ERROR: couldn't add '$name' package to local repository\n"; print urpm::select::translate_why_unselected($urpm, \%state, @unselected); $error = 1; next; } $selected{$_} = 1 foreach keys %{$state{selected}}; } print "..copying RPMs\n" if $::verbose > 1; # Find the URLs for the selected RPMs and copy them to the appropriate # media directory. Note that in the local repository there is only a # single medium for each class, so we ignore the source media type. # Record the classes we find so we know what hdlists we need. my %classes; my @selected_packages = @{$urpm->{depslist}}[keys %selected]; foreach my $pkg (@selected_packages) { my @medium = grep { $pkg->id >= $_->{start} && $pkg->id <= $_->{end} } @{$urpm->{media}}; my $src_path = $medium[0]->{url} . '/' . $pkg->filename; my @path_parts = split('/', $src_path); my $class = $path_parts[-3]; my $name = $path_parts[-1]; my $dst_dir = $local_repo_dir . $class; mkdir_in_root($root, $dst_dir) if ! -d $root . $dst_dir; # TODO: support remote repositories copy_to_root($root, $dst_dir, undef, $src_path); $classes{$class} = 1; } print "..generating media info\n" if $::verbose > 1; foreach my $class (keys %classes) { run_in_root($root, $arch, 'genhdlist2', if_($::verbose < 2, '-q'), $local_repo_dir . $class) or die "ERROR: failed to generate hdlists for '$class' local repository\n"; } $error and die "ERROR: some requested packages have not been added to the local repository\n"; } ############################################################################### # System Customisation ############################################################################### # This is the top-level function called to customise the root filesystem. It # allows the standard Mageia installation to be fine-tuned for use as a Live # system. The basic root filesystem must have been prepared before calling # this function. # sub customise_live_system { my ($build) = @_; print "Customising Live system\n" if $::verbose; my $arch = $build->{settings}{arch}; my $root = $build->get_live_root; # Workaround buggy installation of directories that are not owned by any # packages. my $previous_umask = umask; umask 022; my $error_message; try { mount_system_fs($root); # Copy resolv.conf for name resolution to work when adding media. copy_to_root($root, '/etc/', undef, '/etc/resolv.conf'); # Remove urpmi media added by drakx-in-chroot, they're unusable. run_in_root($root, undef, 'urpmi.removemedia', if_($::verbose < 3, '-q'), '-a'); print "..adding additional media\n" if $::verbose > 1; # Add additional media for installing RPMs not in the main repository. foreach (@{$build->{system}{additional_media}}) { run_in_root($root, undef, 'urpmi.addmedia', if_($::verbose < 2, '-q'), if_($_->{distrib}, '--distrib'), $_->{name}, $_->{path}) or die "ERROR: unable to add media from $_->{path}\n"; @{$_->{packages} || []} or next; run_in_root($root, $arch, 'urpmi', if_($::verbose < 3, '-q'), '--auto', '--no-verify-rpm', if_(!$_->{distrib}, '--searchmedia', $_->{name}), @{$_->{packages}}) or die "ERROR: unable to install packages from $_->{path}\n"; } print "..installing additional packages\n" if $::verbose > 1; # Additional rpms may have dependencies in additional media. if (@{$build->{system}{rpms} || []}) { my $rpm_tmp_dir = '/tmp/draklive_rpms/'; mkdir_in_root($root, $rpm_tmp_dir); my @rpm_files = map { $build->get_absolute_path($_) } @{$build->{system}{rpms}}; copy_to_root($root, $rpm_tmp_dir, undef, @rpm_files); @rpm_files = map { $rpm_tmp_dir . basename($_) } @{$build->{system}{rpms}}; run_in_root($root, $arch, 'urpmi', if_($::verbose < 3, '-q'), '--auto', '--no-verify-rpm', @rpm_files) or die "ERROR: unable to install additional system rpms\n"; rm_in_root($root, $rpm_tmp_dir); } # Remove any additional media. if (@{$build->{system}{additional_media}}) { run_in_root($root, undef, 'urpmi.removemedia', if_($::verbose < 3, '-q'),'-a'); } print "..adding local repository media\n" if $::verbose > 1; # Add local repository media foreach my $path (glob($root . $local_repo_dir . '*')) { my $name = basename($path); run_in_root($root, undef, 'urpmi.addmedia', if_($::verbose < 3, '-q'), 'Live ' . ucfirst($name), $local_repo_dir . $name) or die "ERROR: unable to add $name medium from local repository\n"; } print "..removing unwanted packages\n" if $::verbose > 1; # Remove any packages as requested by the user. my @erase = @{$build->{system}{erase_rpms} || []}; run_in_root($root, $arch, 'rpm', '-e', @erase) if @erase; print "..disabling unwanted services\n" if $::verbose > 1; # Disable services as requested by the user. foreach (@{$build->{system}{disable_services}}) { run_in_root($root, undef, 'systemctl', if_($::verbose < 3, '-q'), 'disable', "$_.service"); } foreach (@{$build->{system}{disable_timers}}) { run_in_root($root, undef, 'systemctl', if_($::verbose < 3, '-q'), 'disable', "$_.timer"); } print "..adjusting system files\n" if $::verbose > 1; # Make sure harddrake is run: # if previous HW config file is empty, we assume DrakX has just completed the installation # (do it in chroot, or else Storable from the build system may write an incompatible config file) run_in_root($root, undef, 'perl', '-MStorable', '-e', qq(Storable::store({ UNKNOWN => {} }, '/etc/sysconfig/harddrake2/previous_hw'))); # Remove some build-machine specific configuration. foreach (qw(/etc/shorewall/interfaces /etc/mdadm.conf)) { clean_system_conf_file($root . $_); } # Create fstab. my $mount_options = $build->get_media_setting('mount_options') || 'defaults'; my $fstab_entry; if ($build->{mount}{overlay}) { $fstab_entry = "none / $build->{mount}{overlay} $mount_options 0 0"; } else { $fstab_entry = $build->get_media_setting('source') . " / " . $build->get_media_setting('fs') . " $mount_options 1 1"; } output_to_root($root, '/etc/fstab', 0644, $fstab_entry); # Interactive mode can lead to race in initscripts. run_as_root('sed', '-i', 's/^PROMPT=.*/PROMPT=no/', $root . '/etc/sysconfig/init'); configure_draklive_resize($build); print "..copying additional files\n" if $::verbose > 1; # Copy extra files as requested by the user. foreach (@{$build->{system}{files}}) { my ($source, $dest, $o_opts) = @$_; mkdir_in_root($root, $dest =~ m|/$| ? $dest : dirname($dest)); my @sources = glob__($build->get_absolute_path($source)); my $mode = $o_opts && $o_opts->{mode}; copy_to_root($root, $dest, $mode, @sources); } my @no_install_files = map { $_->[1] } grep { $_->[2] && $_->[2]{no_install} } @{$build->{system}{files}}; my $installer_file = '/etc/draklive-install.d/remove.d/draklive'; mkdir_in_root($root, dirname($installer_file)); output_to_root($root, $installer_file, undef, @no_install_files); print "..removing unwanted files\n" if $::verbose > 1; # Remove any files as requested by the user. my @remove = @{$build->{system}{remove_files} || []}; rm_in_root($root, @remove) if @remove; print "..applying patches\n" if $::verbose > 1; # Apply patches as requested by the user. foreach (@{$build->{system}{patches}}) { my $patch_file = $build->get_absolute_path($_); my @patch_cmd = ( 'patch', '-p0', '-d', $root, '-i', $patch_file, if_($::verbose < 3, '-s') ); run_as_root(join(' ', @patch_cmd, '--dry-run -f -R > /dev/null')) || run_as_root(@patch_cmd) or die "ERROR: unable to apply patch $patch_file\n"; } print "..performing final fixes\n" if $::verbose > 1; # Perform any final fixes as requested by the user. if ($build->{system}{final_fixes}) { run_in_root($root, $arch, 'bash', '-c', $build->{system}{final_fixes}); } # Final cleanup. clean_system_conf_file($root . '/etc/resolv.conf'); write_dist_lists($build); } catch { $error_message = $_; } finally { umount_all_in_root($root); umask $previous_umask; }; defined $error_message && die $error_message; } sub configure_draklive_resize { my ($build) = @_; my $resizable_loopback = find { $_->{min_size} } @{$build->{mount}{dirs} || []}; if ($resizable_loopback) { my $ext = $loop_types{$resizable_loopback->{type}}{extension}; my @text = ( "DRAKLIVE_RESIZE=yes", "LOOPBACK=/live/media/loopbacks$resizable_loopback->{path}$ext", "TYPE=$resizable_loopback->{fs}", "MIN_SIZE=$resizable_loopback->{min_size}", "MOUNT=/live$resizable_loopback->{mountpoint}_resized", "OLD_MOUNT=/live$resizable_loopback->{mountpoint}", "UNION=/", ); output_to_root($build->get_live_root, '/etc/sysconfig/draklive-resize', undef, @text); } } sub clean_system_conf_file { my ($file) = @_; run_as_root('sed', '-i', '/^[^#]/d', $file) if -f $file; } sub write_dist_lists { my ($build) = @_; my $lists_dir = $build->get_build_dir('dist'); mkdir_p($lists_dir); my $root = $build->get_live_root; run_in_root($root, undef, "rpm -qa | sort > " . $lists_dir . '/' . $build->get_name . '.lst'); run_in_root($root, undef, "rpm -qa --qf '%{name}\n' | sort > " . $lists_dir . '/' . $build->get_name . '.lst.names'); run_in_root($root, undef, qq( sh -c "rpm -qa --qf '[%{NAME} %{FILESIZES} %{FILESTATES}\n]' | awk '{if(\\\$3==0) {s[\\\$1]+=\\\$2}} END{for (p in s){print s[p],p}}' | sort -n" > ) . $lists_dir . '/' . $build->get_name . '.lst.full'); run_in_root($root, undef, qq( sh -c "urpmi_rpm-find-leaves | xargs rpm -q --qf '[%{NAME} %{FILESIZES} %{FILESTATES}\n]' | awk '{if(\\\$3==0) {s[\\\$1]+=\\\$2}} END{for (p in s){print s[p],p}}' | sort -n" > ) . $lists_dir . '/' . $build->get_name . '.lst.leaves'); require lang; my @live_langs = $build->get_langs; my @langs = grep { member($_, @live_langs) || member(lang::locale_to_main_locale($_), @live_langs) } lang::list_langs(); my $langs_file = $lists_dir . '/' . $build->get_name . '.langs'; output_p($langs_file, map { lang::l2name($_) . " (" . $_ . ")\n" } sort(@langs)); } ############################################################################### # Helper Functions ############################################################################### sub mkdir_in_root { my ($root, $dir) = @_; run_as_root('mkdir', '-p', $root . $dir) or die "ERROR: failed to make directory $dir in Live system root\n"; } sub copy_to_root { my ($root, $dest, $mode, @files) = @_; run_as_root('cp', '-af', @files, $root . $dest) or die "ERROR: failed to copy file to $dest in Live system root\n"; return if !defined $mode; run_as_root('chmod', sprintf("%o", $mode), $root . $dest) or die "ERROR: failed to change mode of $dest in Live system root\n"; } sub output_to_root { my ($root, $file, $mode, @text) = @_; my $temp_file = tmpnam(); output($temp_file, map { "$_\n" } @text); chmod($mode, $temp_file) if $mode; run_as_root('mv', $temp_file, $root . $file) or die "ERROR: failed to write $file in Live system root\n"; } sub rm_in_root { my ($root, @targets) = @_; run_as_root('rm', '-rf', map { $root . $_ } @targets) or die "ERROR: failed to remove files in Live system root\n"; } 1;