#!/usr/bin/perl # Copyright (C) 2005 Mandriva # Olivier Blin # Copyright (C) 2017 Mageia # # 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. use lib qw(/usr/lib/libDrakX); use strict; use MDK::Common; use common; use fs; use modules; use run_program; use Getopt::Long; use Pod::Usage; use File::Temp; use File::Copy qw(mv); use IO::Handle; #- autoflush use MDV::Draklive::Utils; use MDV::Draklive::Live; use MDV::Draklive::Loopback; use MDV::Draklive::Config; use MDV::Draklive::Storage; ############################################################################### # Common Helper Functions ############################################################################### sub get_langs { my ($live) = @_; uniq( (ref $live->{regions} ? @{$live->{regions}{$live->{settings}{region}}} : ()), @{$live->{system}{langs_always}} ); } sub mount_system_fs { my ($live) = @_; run_('mount', '-t', 'devtmpfs', '/dev', $live->get_system_root . '/dev'); run_('mount', '-t', 'proc', '/proc', $live->get_system_root . '/proc'); run_('mount', '-t', 'sysfs', '/sys', $live->get_system_root . '/sys'); run_('mount', '-t', 'debugfs', '/sys/kernel/debug/usb', $live->get_system_root . '/sys/kernel/debug/usb'); } sub umount_system_fs { my ($live) = @_; eval { fs::mount::umount($live->get_system_root . $_) } foreach qw(/dev /proc /run /sys/kernel/debug/usb /sys); } sub umount_external_fs { my ($live) = @_; eval { fs::mount::umount($live->get_system_root . $_) } foreach map { "/mnt/$_" } all($live->get_system_root . "/mnt"); } sub get_absolute_path { my ($live, $path) = @_; if (defined $path && substr($path, 0, 1) ne '/') { $live->{settings}{config_root} . '/' . $path; } else { $path; } } ############################################################################### # Install Phase ############################################################################### sub install_system { my ($live) = @_; my $repository = $live->{settings}{repository} . '/' . $live->{settings}{arch}; my $drakx_in_chroot = $repository . '/misc/drakx-in-chroot'; my $remote_repository = $repository =~ m!^(ftp|http)://! && $1; if ($remote_repository) { my $local_drakx_in_chroot = $live->get_builddir . $live->{prefix}{build}{scripts} . '/drakx-in-chroot'; mkdir_p(dirname($local_drakx_in_chroot)); run_('curl', '--silent', '-o', $local_drakx_in_chroot, $drakx_in_chroot) or die "unable to get drakx-in-chroot from remote repository\n"; $drakx_in_chroot = $local_drakx_in_chroot; } local %ENV = ( %ENV, (map { "DRAKLIVE_" . uc($_->[0]) => $_->[1] } group_by2(%{$live->{settings}})), %{$live->{system}{install_env}}, ); $ENV{DRAKLIVE_LANGS} = join(':', get_langs($live)); run_({ targetarch => $live->{settings}{arch} }, 'perl', $drakx_in_chroot, $repository, $live->get_system_root, if_($live->{system}{auto_install}, '--auto_install', $live->{settings}{config_root} . '/' . $live->{system}{auto_install}), if_($live->{system}{patch_install}, '--defcfg', $live->{settings}{config_root} . '/' . $live->{system}{patch_install}), if_($live->{system}{rpmsrate}, '--rpmsrate', $live->{settings}{config_root} . '/' . $live->{system}{rpmsrate}), ($live->{system}{stage2_updates} ? (map { ('--stage2-update', $live->{settings}{config_root} . '/' . $_->[0], $_->[1]) } @{$live->{system}{stage2_updates}}) : ()), ) or die "unable to install system chroot\n"; post_install_system($live); } sub post_install_system { my ($live) = @_; my $previous_umask = umask; #- workaround buggy installation of directories that are not owned by any packages umask 022; mount_system_fs($live); #- copy resolv.conf for name resolution to work when adding media cp_f("/etc/resolv.conf", $live->get_system_root . "/etc/"); #- remove previous draklive leftovers if needed run_({ root => $live->get_system_root }, 'urpmi.removemedia', '-a'); foreach (@{$live->{system}{additional_media}}) { run_({ root => $live->get_system_root }, 'urpmi.addmedia', if_($_->{distrib}, '--distrib'), $_->{name}, $_->{path}) or die "unable to add media from $_->{path}\n"; @{$_->{packages} || []} or next; run_({ root => $live->get_system_root, targetarch => $live->{settings}{arch} }, 'urpmi', '--auto', '--no-verify-rpm', if_(!$_->{distrib}, '--searchmedia', $_->{name}), @{$_->{packages}}) or die "unable to install packages from $_->{path}\n"; } #- additional rpms may have dependencies in additional media if (@{$live->{system}{rpms} || []}) { my $rpm_tmp_dir = '/tmp/draklive_rpms'; mkdir_p($live->get_system_root . $rpm_tmp_dir); cp_f((map { $live->{settings}{config_root} . '/' . $_ } @{$live->{system}{rpms}}), $live->get_system_root . $rpm_tmp_dir); run_({ root => $live->get_system_root, targetarch => $live->{settings}{arch} }, 'urpmi', '--auto', '--no-verify-rpm', map { $rpm_tmp_dir . '/' . basename($_) } @{$live->{system}{rpms}}) or die "unable to install additional system rpms\n"; rm_rf($live->get_system_root . $rpm_tmp_dir); } #- remove urpmi media added by drakx-in-chroot and additional media, they're unusable run_({ root => $live->get_system_root }, 'urpmi.removemedia', '-a'); my $erase = join(' ', @{$live->{system}{erase_rpms} || []}); run_({ root => $live->get_system_root, targetarch => $live->{settings}{arch} }, 'sh', '-c', "rpm -qa $erase | xargs rpm -e ") if $erase; run_({ root => $live->get_system_root }, 'systemctl', 'disable', $_ . '.service') foreach @{$live->{system}{disable_services}}; run_({ root => $live->get_system_root }, 'systemctl', 'disable', $_ . '.timer') foreach @{$live->{system}{disable_timers}}; #- make sure harddrake is run: #- if previous HW config file is empty, we assumes DrakX has just completed the installation #- (do it in chroot, or else Storable from the build box may write an incompatible config file) run_({ root => $live->get_system_root }, 'perl', '-MStorable', '-e', qq(Storable::store({ UNKNOWN => {} }, '/etc/sysconfig/harddrake2/previous_hw'))); #- remove some build-machine specific configuration clean_system_conf_file($live, $_) foreach qw(/etc/mtab /etc/iftab /etc/shorewall/interfaces /etc/mdadm.conf), if_(!$live->{system}{skip_fstab}, '/etc/fstab'); unlink($_) foreach map { glob($live->get_system_root . $_) } @{$live->{system}{remove_files} || []}; if ($live->{system}{modules_conf}) { local $::prefix = $live->get_system_root; local *modules::write_preload_conf = sub {}; #- FIXME, make this an option my $modules_conf = modules::any_conf->vnew; put_in_hash($modules_conf, $live->{system}{modules_conf}); $modules_conf->write; } my $mount_options = $live->{media}->get_media_setting('mount_options') || "defaults"; output_with_perm($live->get_system_root . '/etc/fstab', 0644, $live->{mount}{overlay} ? "none / $live->{mount}{overlay} $mount_options 0 0\n" : $live->{media}->get_media_setting('source') . " / " . $live->{media}->get_media_setting('fs') . " $mount_options 1 1\n" ) unless $live->{system}{skip_fstab}; #- interactive mode can lead to race in initscripts #- (don't use addVarsInSh from MDK::Common, it breaks shell escapes) substInFile { s/^PROMPT=.*/PROMPT=no/ } $live->get_system_root . '/etc/sysconfig/init'; configure_draklive_resize($live); if ($live->{system}{preselect_kdm_user}) { #- preselect specified user in kdm my $kdm_cfg = $live->get_system_root . '/etc/kde/kdm/kdmrc'; update_gnomekderc($kdm_cfg, 'X-:0-Greeter' => (PreselectUser => 'Default', DefaultUser => $live->{system}{preselect_kdm_user})) if -f $kdm_cfg; } #- apply patches and install files after the configuration is cleaned #- to allow special configuration files (especially modprobe.preload) foreach (@{$live->{system}{patches}}) { my $patch = $live->{settings}{config_root} . '/' . $_; my @args = ('-p0', '-d', $live->get_system_root, '-i', $patch); run_program::run('patch', '>', '/dev/null', '--dry-run', '-f', '-R', @args) || run_('patch', @args) or die "unable to apply patch " . $patch . "\n"; } copy_files_to($live, $live->{system}{files}, $live->get_system_root); my @no_install_files = map { $_->[1] } grep { $_->[2] && $_->[2]{no_install} } @{$live->{system}{files}}; output_p($live->get_system_root . '/etc/draklive-install.d/remove.d/draklive', map { "$_\n" } @no_install_files); eval { rm_rf($live->get_builddir . $live->{prefix}{build}{files}) }; mkdir_p($live->get_builddir . $live->{prefix}{build}{files}); if ($live->{media}{files}) { copy_files_to($live, $live->{media}{files}, $live->get_builddir . $live->{prefix}{build}{files}); } remove_files_from($live->{media}{remove_files}, $live->get_builddir . $live->{prefix}{build}{files}); run_({ targetarch => $live->{settings}{arch} }, "chroot", $live->get_system_root, "bash", "-c", $live->{system}{postInstall}) if $live->{system}{postInstall}; clean_system_conf_file($live, "/etc/resolv.conf"); write_dist_lists($live); umount_system_fs($live); umask $previous_umask; } sub configure_draklive_resize { my ($live) = @_; my $resizable_loopback = find { $_->{min_size} } @{$live->{mount}{dirs} || []}; if ($resizable_loopback) { my $media_loopbacks = $live->get_media_prefix('loopbacks'); my $ext = $loop_types{$resizable_loopback->{type}}{extension}; output($live->get_system_root . '/etc/sysconfig/draklive-resize', <{prefix}{live}{mnt}$live->{prefix}{media}{mnt}${media_loopbacks}$resizable_loopback->{path}$ext TYPE=$resizable_loopback->{fs} MIN_SIZE=$resizable_loopback->{min_size} MOUNT=$live->{prefix}{live}{mnt}$resizable_loopback->{mountpoint}_resized OLD_MOUNT=$live->{prefix}{live}{mnt}$resizable_loopback->{mountpoint} UNION=/ EOF } } sub copy_files_to { my ($live, $files, $root) = @_; foreach (@$files) { my ($source, $dest, $o_opts) = @$_; $dest = $root . '/' . $dest; mkdir_p($dest =~ m|/$| ? $dest : dirname($dest)); my @sources = MDV::Draklive::Utils::glob__($live->{settings}{config_root} . '/' . $source); print STDERR "copying @sources to $dest\n"; cp_af(@sources, $dest); my $o_perm = $o_opts && $o_opts->{mode}; chmod $o_perm, $dest if $o_perm; } } sub remove_files_from { my ($files, $root) = @_; run_('find', $root, '(', join_lists('-o', map { [ '-name', $_ ] } @$files), ')', '-exec', 'rm', '-r', '{}', ';') if $files && @$files; } sub join_lists { my ($separator, $head, @lists) = @_; @{$head || []}, map { $separator, @$_ } @lists; } sub clean_system_conf_file { my ($live, $file) = @_; substInFile { undef $_ if /^[^#]/ } $live->get_system_root . $file; } sub write_dist_lists { my ($live) = @_; my $lists_dir = $live->get_builddir . $live->{prefix}{build}{dist}; mkdir_p($lists_dir); run_("chroot " . $live->get_system_root . " rpm -qa | sort > " . $lists_dir . '/' . $live->get_name . '.lst'); run_("chroot " . $live->get_system_root . " rpm -qa --qf '%{name}\n' | sort > " . $lists_dir . '/' . $live->get_name . '.lst.names'); run_("chroot " . $live->get_system_root . 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 . '/' . $live->get_name . '.lst.full'); run_("chroot " . $live->get_system_root . 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 . '/' . $live->get_name . '.lst.leaves'); require lang; my @live_langs = get_langs($live); my @langs = grep { member($_, @live_langs) || member(lang::locale_to_main_locale($_), @live_langs) } lang::list_langs(); my $langs_file = $lists_dir . '/' . $live->get_name . '.langs'; output_p($langs_file, map { lang::l2name($_) . " (" . $_ . ")\n" } sort(@langs)); } ############################################################################### # Loop Phase ############################################################################### sub create_loopback_files { my ($live) = @_; # umount filesystem in the live before creating the loopback umount_external_fs($live); umount_system_fs($live); my @excluded_files = expand_file_list($live, @{$live->{loopbacks}{exclude}{files} || []}); my @modules_files = expand_file_list($live, @{$live->{loopbacks}{modules} || []}); foreach (grep { exists $loop_types{$_->{type}}{build} } @{$live->{mount}{dirs} || []}) { local $_->{exclude} = [ @excluded_files, @modules_files ]; $loop_types{$_->{type}}{build}->($live, $_); } foreach my $module (list_loopback_modules($live)) { my $copy_tree = $live->get_system_root . "/tmp/draklive/loop/$module->{name}"; eval { rm_rf($copy_tree) }; hardlink_filtered($live->get_system_root, $copy_tree, $module->{files}); my $loop = $loop_types{$module->{type}}; $loop->{build}->($live, { path => "$live->{prefix}{build}{modules}/$module->{name}", root => $copy_tree, exclude => \@excluded_files }); eval { rm_rf($copy_tree) }; } if (@excluded_files) { my $excluded_tree = $live->get_system_root . "/tmp/draklive/excluded/all"; eval { rm_rf($excluded_tree) }; hardlink_filtered($live->get_system_root, $excluded_tree, \@excluded_files); foreach my $module (list_loopback_modules($live)) { my $copy_tree = $live->get_system_root . "/tmp/draklive/excluded/$module->{name}"; eval { rm_rf($copy_tree) }; hardlink_filtered($excluded_tree, $copy_tree, $module->{files}); my $loop = $loop_types{$module->{type}}; $loop->{build}->($live, { path => "$live->{prefix}{build}{modules}/excluded-$module->{name}", root => $copy_tree }); eval { rm_rf($copy_tree) }; } my $loop = $loop_types{$live->{loopbacks}{exclude}{type}}; $loop->{build}->($live, { path => "/excluded", root => $excluded_tree, exclude => \@modules_files }); eval { rm_rf($excluded_tree) }; } } sub expand_file_list { my ($live, @files) = @_; map { $_->{path} ? $_->{path} : chomp_(cat_(glob(($_->{rooted} && $live->get_system_root) . $_->{source}))); } @files; } #- hardlink recursively file list to a directory sub hardlink_filtered { my ($src, $dest, $files) = @_; mkdir_p($dest); my $pwd = $ENV{PWD}; chdir($src); my $list_file = tmpnam(); output_p($list_file, map { "$_\n" } grep { -e $src . $_ } @$files); #- cpio -pldm won't copy recursively, use rsync -r instead system('rsync', '-ar', '--files-from=' . $list_file, '--link-dest=' . $src, $src, $dest); unlink $list_file; chdir($pwd); } sub list_loopback_modules { my ($live) = @_; map { my $l = $_; map { my $list = $_; my $name = basename($list); $name =~ s/\.[^.]+$//; { type => $l->{type}, name => $name, files => [ expand_file_list($live, { source => $list }) ] }; } glob(($_->{rooted} && $live->get_system_root) . $_->{source}); } @{$live->{loopbacks}{modules}}; } sub list_selected_loopbacks { my ($live) = @_; my @pack = $live->{settings}{pack} ? @{$live->{packs}{$live->{settings}{pack}} || []} : (); my @pack_modules = grep { member($_->{name}, @pack) } list_loopback_modules($live); (map { $loop_types{$_->{type}}{is_loopback} && $_->{path} ? $_->{path} . $loop_types{$_->{type}}{extension} : () } @{$live->{mount}{dirs} || []}), (map { $live->{prefix}{build}{modules} . '/' . $_->{name} . $loop_types{$_->{type}}{extension} } @pack_modules); } ############################################################################### # Boot Phase ############################################################################### sub prepare_bootloader { my ($live) = @_; create_initrd($live); create_bootloader($live); } sub create_initrd { my ($live) = @_; my $root = $live->get_system_root; my $kernel = $live->find_kernel; print "using kernel $kernel->{version}\n"; my $initrd = $root . '/boot/' . $live->get_initrd_name; unlink($initrd); mount_system_fs($live); { my $bootloader = {}; local $::prefix = $root; bootloader::add_kernel($bootloader, $kernel, { label => 'linux', vga => $live->{system}{vga_mode} }, '', $live->{system}{no_initrd}); } umount_system_fs($live); my $boot_dir = $live->get_builddir . $live->{prefix}{build}{boot}; mkdir_p($boot_dir); mv($initrd, $boot_dir . '/initrd.gz') or die "cannot move initrd: $!\n"; } sub create_bootloader { my ($live) = @_; my $boot_dir = $live->get_builddir . $live->{prefix}{build}{boot}; mkdir_p($boot_dir); my $root = $live->get_system_root; my $kernel = $live->find_kernel->{version}; my $vmlinuz = $root . '/boot/vmlinuz-' . $kernel; -e $vmlinuz or die "cannot find kernel $kernel\n"; cp_f($vmlinuz, $boot_dir . '/vmlinuz'); my $grub2_dir = $boot_dir . '/grub2'; mkdir_p($grub2_dir); my $fonts_dir = $grub2_dir . '/fonts'; mkdir_p($fonts_dir); my $font = $root . '/usr/share/grub/unicode.pf2'; cp_f($font, $fonts_dir) if -e $font; my $themes_dir = $grub2_dir . '/themes'; mkdir_p($themes_dir); my $theme_name = $live->{media}{bootloader_theme}; my @theme_fonts; if (defined $theme_name) { my $theme = $root . '/boot/grub2/themes/' . $theme_name; -e $theme or die "cannot find grub2 $theme_name theme\n"; cp_f($theme, $themes_dir); @theme_fonts = map { basename($_) } glob("$theme/*.pf2"); } my $add_lang_menu = defined $live->{media}{bootloader_langs}; if ($add_lang_menu) { my $lang_names = get_absolute_path($live, $live->{media}{bootloader_langs}); -e $lang_names or die "cannot find language name file $lang_names\n"; my @langs = eval(cat_($lang_names)) or die "error in language name file $lang_names\n"; MDK::Common::File::output_utf8($grub2_dir . '/lang-menu.cfg', build_lang_menu_cfg(@langs)); my $locale_dir = $grub2_dir . '/locale'; mkdir_p($locale_dir); my $messages = get_absolute_path($live, $live->{media}{bootloader_messages}); -d $messages or die "cannot find translated messages directory $messages\n"; cp_f(glob($messages . '/*.mo'), $locale_dir); } my $eltorito_img = get_absolute_path($live, $live->{media}{eltorito_img}); if (defined $eltorito_img) { -e $eltorito_img or die "cannot find grub2 Eltorito boot image $eltorito_img\n"; cp_f($eltorito_img, $grub2_dir . '/eltorito.img'); } else { build_grub2_eltorito_img($live, $grub2_dir); } my $label = $live->{media}->get_media_label; my $grub2_cfg = $grub2_dir . '/grub.cfg'; if (defined $live->{media}{grub2_cfg}) { my $grub_cfg_template = get_absolute_path($live, $live->{media}{grub2_cfg}); -e $grub_cfg_template or die "cannot find grub2 config file $grub_cfg_template\n"; cp_f($grub_cfg_template, $grub2_cfg); run_("sed", "-i", "s/VOLUME_LABEL/$label/g", $grub2_cfg); } else { output($grub2_cfg, build_grub2_cfg($live, $theme_name, \@theme_fonts, $add_lang_menu)); } my $title = $label; $title =~ s/-/ /gr; my $base_theme_txt = $grub2_dir . "/themes/$theme_name/theme.txt"; if (defined $theme_name && -e $base_theme_txt) { run_('sed', '-i', qq(s/title-text:.*/title-text: "$title"/), $base_theme_txt); } return if $live->{settings}{arch} ne 'x86_64'; my $efi_root_dir = $live->get_builddir . $live->{prefix}{build}{EFI}; my $efi_boot_dir = $efi_root_dir . '/BOOT'; mkdir_p($efi_boot_dir); my $bootx64_efi = get_absolute_path($live, $live->{media}{bootx64_efi}); if (defined $bootx64_efi) { -e $bootx64_efi or die "cannot find grub2 EFI boot image $bootx64_efi\n"; cp_f($bootx64_efi, $efi_boot_dir . '/bootx64.efi'); } else { build_grub2_bootx64_efi($live, $efi_boot_dir); } output($efi_boot_dir . '/grub.cfg', build_uefi_grub2_cfg($live)); if (defined $theme_name && -e $base_theme_txt) { my $uefi_theme_txt = $grub2_dir . "/themes/$theme_name/theme-uefi.txt"; cp_f($base_theme_txt, $uefi_theme_txt); run_('sed', '-i', qq(s/title-text:.*/title-text: "$title (UEFI)"/), $uefi_theme_txt); } my $images_dir = $live->get_builddir . $live->{prefix}{build}{images}; mkdir_p($images_dir); my $esp_image = $images_dir . '/esp.img'; eval { rm_rf($esp_image) }; run_("/sbin/mkdosfs", "-F12", "-C", $esp_image, "4096"); run_("mcopy", "-s", "-i", $esp_image, $efi_root_dir, "::"); } sub build_grub2_eltorito_img { my ($live, $dest_dir) = @_; # temporary hack to workaround grub2/grub2-efi conflict run_({ root => $live->get_system_root }, 'urpmi', '-q', '--force', 'grub2'); my @modules = qw(biosdisk iso9660 fat part_msdos all_video font png gfxterm gfxmenu linux configfile echo gettext ls search test); my $output = '/boot/grub2/eltorito.img'; run_({ root => $live->get_system_root }, 'grub2-mkimage', '--output', $output, '--prefix', $live->get_media_prefix('boot') . '/grub2', '--format', 'i386-pc-eltorito', @modules ); mv($live->get_system_root . $output, $dest_dir) or die "cannot move eltorito.img: $!\n"; # temporary hack to workaround grub2/grub2-efi conflict run_({ root => $live->get_system_root }, 'rpm', '-e', '--nodeps', 'grub2'); } sub build_grub2_bootx64_efi { my ($live, $dest_dir) = @_; # temporary hack to workaround grub2/grub2-efi conflict run_({ root => $live->get_system_root }, 'urpmi', '-q', '--force', 'grub2-efi'); my @modules = qw(iso9660 fat part_msdos all_video font png gfxterm gfxmenu linux configfile echo gettext ls search test); my $output = '/boot/grub2/bootx64.efi'; run_({ root => $live->get_system_root }, 'grub2-mkimage', '--output', $output, '--prefix', $live->get_media_prefix('EFI') . '/BOOT', '--format', 'x86_64-efi', @modules ); mv($live->get_system_root . $output, $dest_dir) or die "cannot move bootx64.efi: $!\n"; # temporary hack to workaround grub2/grub2-efi conflict run_({ root => $live->get_system_root }, 'rpm', '-e', '--nodeps', 'grub2-efi'); } sub build_grub2_cfg { my ($live, $theme_name, $theme_fonts, $add_lang_menu) = @_; my @loadfonts; if (defined $theme_name) { @loadfonts = map { "loadfont \$prefix/themes/$theme_name/$_" } @$theme_fonts; } my @langs = get_langs($live); my $default_lang = $langs[0] || 'en_US'; my $gettext = $add_lang_menu ? '$' : ''; my $boot_dir = $live->get_media_prefix('boot'); join("\n", "search --no-floppy --set=root -l '" . $live->{media}->get_media_label . "'", "set prefix=(\$root)" . $live->get_media_prefix('boot') . "/grub2", "", "if loadfont \$prefix/fonts/unicode.pf2 ; then", " set gfxmode=1024x768,800x600,auto", " set gfxpayload=keep", " terminal_output gfxterm", "fi", "", if_($theme_name, "if [ x\$uefi == 'xtrue' ] ; then", " set theme=\$prefix/themes/$theme_name/theme-uefi.txt", "else", " set theme=\$prefix/themes/$theme_name/theme.txt", "fi", "export theme", @loadfonts, ), "", "set default=" . get_bootloader_default($live), "set timeout=" . get_bootloader_timeout($live), "", if_($add_lang_menu, "if [ -z \$lang ] ; then", " set lang=$default_lang", "fi", "export lang", "", "submenu $gettext\"Language [\$lang]\" {", " source \$prefix/lang-menu.cfg", "}", "", ), (map { my ($name, $cmdline) = @$_; join("\n", "menuentry $gettext\"$name\" {", " linux $boot_dir/vmlinuz " . get_default_append($live) . if_($cmdline, " $cmdline"), " initrd $boot_dir/initrd.gz", "}" ); } group_by2(@{$live->{media}{bootloader_entries}})), "", ); } sub get_bootloader_default { my ($live) = @_; defined $live->{media}{bootloader_default} ? $live->{media}{bootloader_default} : 0; } sub get_bootloader_timeout { my ($live) = @_; defined $live->{media}{bootloader_timeout} ? $live->{media}{bootloader_timeout} : 4; } sub get_default_append { my ($live) = @_; my $append = $live->{system}{append}; join(" ", "root=mgalive:LABEL=" . $live->{media}->get_media_label, if_($append, $append), if_($live->{system}{vga_mode} && $append !~ /\bvga=\b/, "vga=" . $live->{system}{vga_mode}), ); } sub build_lang_menu_cfg { my ($langs) = @_; join("\n", "function set_language {", " set lang=\$1", " configfile \$prefix/grub.cfg", "}", "", "set default=\$lang", "set timeout=-1", "", (map { my ($id, $name) = @$_; "menuentry '$name' --id $id { set_language $id }"; } group_by2(@$langs)), "", ); } sub build_uefi_grub2_cfg { my ($live) = @_; join("\n", "search --no-floppy --set=root -l '" . $live->{media}->get_media_label . "'", "set prefix=(\$root)" . $live->get_media_prefix('boot') . "/grub2", "", "set uefi=true", "export uefi", "", "configfile \$prefix/grub.cfg", "", ); } ############################################################################### # Master Phase ############################################################################### sub create_master { my ($live) = @_; if (my $create = $live->{media}->get_storage_setting('create')) { $create->($live); } else { warn "not implemented yet\n"; } } sub create_iso_master { my ($live) = @_; my $label = $live->{media}->get_media_label or die "the source device must be described by a label\n"; my $mbr_image = get_absolute_path($live, $live->{media}{mbr_boot_img}); if (!defined $mbr_image) { $mbr_image = $live->get_system_root . '/usr/lib/grub/i386-pc/boot_hybrid.img'; # temporary hack to workaround grub2/grub2-efi conflict my $images_dir = $live->get_builddir . $live->{prefix}{build}{images}; mkdir_p($images_dir); run_({ root => $live->get_system_root }, 'urpmi', '-q', '--force', 'grub2'); cp_f($mbr_image, $images_dir . '/mbr_boot.img'); run_({ root => $live->get_system_root }, 'rpm', '-e', '--nodeps', 'grub2'); $mbr_image = $images_dir . '/mbr_boot.img'; } my $esp_image = $live->get_builddir . $live->{prefix}{build}{images} . '/esp.img'; my $dest = $live->get_builddir . $live->{prefix}{build}{dist} . '/' . $live->get_name . '.iso'; mkdir_p(dirname($dest)); build_iso_image( $live, $dest, $label, $mbr_image, $esp_image, $live->get_media_prefix('boot') . '=' . $live->get_builddir . $live->{prefix}{build}{boot}, if_($live->{settings}{arch} eq 'x86_64', $live->get_media_prefix('EFI') . '=' . $live->get_builddir . $live->{prefix}{build}{EFI}, ), ( map { $live->get_media_prefix('loopbacks') . $_ . '=' . $live->get_builddir . $live->{prefix}{build}{loopbacks} . $_; } list_selected_loopbacks($live) ), if_($live->{media}{files}, map { $_ . '=' . $live->get_builddir . $live->{prefix}{build}{files} . '/' . $_; } all($live->get_builddir . $live->{prefix}{build}{files}) ), ); } sub build_iso_image { my ($live, $dest, $label, $mbr_image, $esp_image, @opts) = @_; my $boot = $live->get_media_prefix('boot'); my $EFI = $live->get_media_prefix('EFI'); run_('xorrisofs', '-pad', '-l', '-R', '-J', '-V', $label, '-graft-points', '-hide-rr-moved', '--sort-weight', 0, '/', '--sort-weight', 1, $boot, # for hybrid MBR boot '--grub2-mbr', $mbr_image, '-b', "$boot/grub2/eltorito.img", '-no-emul-boot', '-boot-load-size', 4, '-boot-info-table', '--grub2-boot-info', if_($live->{settings}{arch} eq 'x86_64', # for DVD UEFI boot '-eltorito-alt-boot', '-e', "$EFI/BOOT/bootx64.efi", '-no-emul-boot', # for USB UEFI boot '-part_like_isohybrid', '-iso_mbr_part_type', '0x00', '-append_partition', 2, '0xef', $esp_image, ), if_($dest, '-o', $dest), @opts, ) or die "unable to run xorrisofs\n"; if ($dest) { my $dir = dirname($dest); my $filename = basename($dest); run_('mgaiso-addmd5', '>', '/dev/null', '2>', '/dev/null', $dest); run_({ chdir => $dir }, 'md5sum', '>', $dest . '.md5', $filename); run_({ chdir => $dir }, 'sha1sum', '>', $dest . '.sha1', $filename); run_({ chdir => $dir }, 'sha512sum', '>', $dest . '.sha512', $filename); run_({ chdir => $dir }, 'date', '>', $dir . '/DATE.txt'); if (my $suffix = $live->get_set_suffix) { if (my ($prefix, $ext) = $dest =~ /(.*)(\.[^.]+)$/) { my $link = $prefix . $suffix . $ext; linkf($dest, $link); print "linked as $link\n"; } } } } ############################################################################### # Clean Phase ############################################################################### sub clean { my ($live) = @_; # umount filesystem in the live before cleaning umount_external_fs($live); umount_system_fs($live); rm_rf($_) foreach grep { -e $_ } $live->get_builddir, $live->get_system_root; } ############################################################################### # Main Program ############################################################################### my @actions = ( { name => 'dump-config', do => \&MDV::Draklive::Config::dump_config }, { name => 'clean', do => \&clean }, { name => 'install', do => \&install_system }, { name => 'post-install', do => \&post_install_system }, { name => 'initrd', do => \&create_initrd }, { name => 'boot', do => \&prepare_bootloader }, { name => 'loop', do => \&create_loopback_files }, { name => 'master', do => \&create_master }, ); my @all = qw(install boot loop master); die "you must be root to run this program\n" if $>; my $live_object = 'MDV::Draklive::Live'->new; my $config_root = $MDV::Draklive::Config::default_config_root; my $config_path = $MDV::Draklive::Config::default_config_path; my $settings_path = $MDV::Draklive::Config::default_settings_path; GetOptions( "help" => sub { Pod::Usage::pod2usage('-verbose' => 1) }, "all" => sub { $_->{to_run} = 1 foreach grep { member($_->{name}, @all) } @actions }, (map { $_->{name} => \$_->{to_run} } @actions), "all-regions" => sub { $live_object->{all_regions} = 1 }, "config-root=s" => \$config_root, "config=s" => \$config_path, "settings=s" => \$settings_path, "define=s" => \%{$live_object->{settings}}, ) or Pod::Usage::pod2usage(); require standalone; every { !$_->{to_run} } @actions and Pod::Usage::pod2usage(); MDV::Draklive::Config::read_config($live_object, $config_root, $config_path, $settings_path); MDV::Draklive::Config::check_config($live_object); MDV::Draklive::Config::complete_config($live_object); foreach my $region ($live_object->{all_regions} ? sort(keys %{$live_object->{regions}}) : $live_object->{settings}{region}) { $region and print qq(=== proceeding with region "$region"\n); $live_object->{settings}{region} = $region; foreach (grep { $_->{to_run} } @actions) { print qq(* entering step "$_->{name}"\n); $_->{do}->($live_object); print qq(* step "$_->{name}" done\n); } } __END__ =head1 NAME draklive - A live distribution mastering tool =head1 SYNOPSIS draklive [options] Options: --help long help message --install install selected distribution in chroot --boot prepare initrd and bootloader files --loop build compressed loopback files --master build master image --all run all steps, from installation to mastering --clean clean installation chroot and work directory --initrd build initrd only --post-install run post install only (rpms and patches installation) --config-root root directory containing config files and additionnal files defaults to current directory if it contains a configuration file else, "/etc/draklive" is used --config use this configuration file as live description defaults to "config/live.cfg" --settings use this file as live settings (key=value format) defaults to "config/settings.cfg" --define key=value set setting "key" to "value" takes precedence over values from a settings file --all-regions proceed with all configured regions Examples: draklive --clean draklive --all draklive --config config/live.cfg --install =head1 OPTIONS =over 8 =item B<--config> Makes draklive use the next argument as a configuration file. This file should contain an hash describing the live distribution, meaning the system (chroot and boot), media (usb, cdrom, nfs), and mount type (simple R/W union, union with squash files). Here's a configuration sample: { settings { repository => '/mnt/ken/2006.0', root => '/chroot/live-move', }, system => { kernel => '2.6.12-12mdk-i586-up-1GB', auto_install => 'config/auto_inst.cfg.pl', patch_install => 'config/patch-2006-live.pl', rpmsrate => 'config/rpmsrate', rpms => [ 'rpms/unionfs-kernel-2.6.12-12mdk-i586-up-1GB-1.1.1.1.20051124.1mdk-1mdk.i586.rpm' ], patches => [ 'patches/halt.loopfs.patch', ], vga_mode => 788, no_initrd => 0, }, media => { storage => 'iso', }, mount => $predefined{mounts}{squash_union} }; =item B<--settings> Makes draklive load the next argument as a file in key=value format into the $live->{settings} hash ($live being the global live configuration hash). Built-in keys: arch: build architecture builddir: directory hosting build files (initrd, loopbacks, images) chroot: directory hosting chrooted installations region: use the matching set of langs from the regions configuration hash repository: path to the Mandriva distribution repository (ftp/http/local) Example keys: desktop media product =back =head1 DESCRIPTION B builds a live distribution according to a configuration file, creates a master image, and optionally installs it on a device. See L =head1 AUTHOR Olivier Blin =cut