#!/usr/bin/perl # draklive $Id$ # Copyright (C) 2005 Mandriva # Olivier Blin # # 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 MDK::Common; use common; use list_modules; use modules; use detect_devices; use run_program; use POSIX qw(strftime); use Cwd 'abs_path'; use Getopt::Long; use Pod::Usage; my $dir_distrib_sqfs = { mountpoint => '/distrib', type => 'squashfs', source => 'distrib.sqfs', build_from => '/', }; my $dir_memory = { mountpoint => '/memory', type => 'tmpfs', }; # this is not unused (it can be used from config file): my %predefined = ( mounts => { simple_union => { root => '/union', overlay => 'unionfs', dirs => [ $dir_memory, { mountpoint => '/media' }, ], }, squash_union => { root => '/union', overlay => 'unionfs', dirs => [ { mountpoint => '/system', type => 'loopfs', pre_allocate => '100k', source => 'system.loop' }, $dir_distrib_sqfs, ], }, volatile_squash_union => { root => '/union', overlay => 'unionfs', dirs => [ $dir_memory, $dir_distrib_sqfs, ], }, multi_squash_union => { root => '/union', overlay => 'unionfs', dirs => [ $dir_memory, { mountpoint => '/system', type => 'squashfs', source => 'system.sqfs' }, $dir_distrib_sqfs, ], }, }, ); # this is not unused (it can be used from config file): my %custom = ( media => { nfs => sub { my ($module, $client, $source) = @_; { extra_modules => [ $module ], fs => 'nfs', pre => "ifconfig eth0 $client up", source => $source, }; }, }, ); my %storage = ( cdrom => { modules => 'bus/usb disk/cdrom|hardware_raid|ide|raw|sata|scsi|usb', fs => 'iso9660', source => 'LABEL=MDVCDROOT', sleep => 15, read_only => 1, detect => \&detect_devices::burners, create => \&create_cdrom_master, format => \&format_cdrom_device, record => \&record_cdrom_master, }, usb => { modules => 'bus/usb disk/raw|usb', fs => 'vfat', source => 'LABEL=MDVUSBROOT', sleep => 15, detect => sub { grep { detect_devices::isKeyUsb($_) } detect_devices::get() }, create => undef, format => \&format_usb_device, record => \&record_usb_master, }, ); sub nls_modules { my ($live) = @_; if_(get_media_setting($live->{media}, 'fs') eq 'vfat', 'nls_cp437'), #- default FAT codepage map { "nls_$_" } (map { "iso8859-$_" } 1..7, 9, 13..15), 'utf8'; } sub progress_start { my ($total, $time, $o_exp_divide) = @_; { total => $total, current => 0, start_time => $time, exp_divide => $o_exp_divide, maxl => length($total) - $o_exp_divide, }; } sub progress_show_incr { my ($progress, $incr, $time) = @_; $progress->{current} += $incr; my $elapsed_time = $time - $progress->{start_time}; my $eta = int($elapsed_time*$progress->{total}/$progress->{current}); printf("\r%3d%% (%$progress->{maxl}s/%-$progress->{maxl}s), %8s/%8s (ETA)", int(100*$progress->{current}/$progress->{total}), (map { substr($_, 0, length($_)-$progress->{exp_divide}) } $progress->{current}, $progress->{total}), (map { POSIX::strftime("%H:%M:%S", gmtime($_)) } $elapsed_time, $eta)); } sub progress_end() { print "\n" } my $loop_number = 0; my %loop = ( squashfs => { read_only => 1, modules => [ qw(loop squashfs) ], build => sub { my ($dest, $root) = @_; my $total = first(split /\s/, `du -sb $root`); print "have to process " . int($total/1000000) . " MB\n"; my $progress = progress_start($total, time(), 6); open(my $OUTPUT, '-|', 'mksquashfs', $root, $dest, '-info'); { local $_; #- avoid outside $_ to be overwritten while (<$OUTPUT>) { if (/^mksquashfs: file .*, uncompressed size (\d+) bytes, (?:DUPLICATE)?$/) { progress_show_incr($progress, $1, time()); } } } progress_end(); }, mount => sub { my ($live, $dir) = @_; my @mnt = ( "/bin/losetup -r /dev/loop$loop_number $live->{prefix}{live_mnt}$live->{prefix}{media_mnt}$live->{prefix}{loopbacks}/$dir->{source}", "mount -o ro -t squashfs /dev/loop$loop_number $live->{prefix}{live_mnt}$dir->{mountpoint}"); $loop_number++; @mnt; }, }, loopfs => { modules => [], create => sub { my ($dest, $size) = @_; run_('dd', "of=$dest", 'count=0', "seek=$size", 'bs=1k'); run_('mke2fs', '-F', $dest); }, mount => sub { my ($live, $dir) = @_; my @mnt = ( "losetup /dev/loop$loop_number $live->{prefix}{live_mnt}$live->{prefix}{media_mnt}$live->{prefix}{loopbacks}/$dir->{source}", "mount -t ext2 /dev/loop$loop_number $live->{prefix}{live_mnt}$dir->{mountpoint}"); $loop_number++; @mnt; }, }, tmpfs => { mount => sub { my ($live, $dir) = @_; "mount -t tmpfs none $live->{prefix}{live_mnt}$dir->{mountpoint}"; }, }, ); my %overlay = ( unionfs => { modules => [ qw(unionfs) ], mount => sub { my ($live) = @_; #- build dirs list: "dir1=ro:dir2:ro:dir3=rw" my $dirs = join(':', map { "$live->{prefix}{live_mnt}$_->{mountpoint}=" . ($_->{type} && !$loop{$_->{type}}{read_only} ? 'rw' : 'ro'); } @{$live->{mount}{dirs} || []}); "mount -o dirs=$dirs -t unionfs unionfs $live->{prefix}{live_mnt}$live->{mount}{root}"; }, }, ); my %moddeps; sub load_moddeps { my ($root, $kernel_path) = @_; my $get_modname = sub { first($_[0] =~ m!^$kernel_path/kernel/(?:.*/|)(.*?)\.k?o!) }; %moddeps = (map { my ($f, $deps) = split ':'; my $modname = $get_modname->($f); $modname => { full => $f, deps => [ map { $get_modname->($_) } split ' ', $deps ] }; } cat_($root . $kernel_path . '/modules.dep')); } sub moddeps_closure { my ($module) = @_; my @deps = @{$moddeps{$module}{deps}}; (map { moddeps_closure($_) } @deps), @deps; } sub run_ { my $options = ref $_[0] eq 'HASH' ? shift @_ : {}; print STDERR "running " . (exists $options->{root} && "(in chroot) ") . join(' ', @_) . "\n"; run_program::raw($options, @_); } sub get_region_suffix { my ($live) = @_; defined $live->{system}{region} ? '/' . $live->{system}{region} : ref $live->{regions} ? '/noregion' : ''; } sub get_workdir { my ($live) = @_; $live->{workdir} . get_region_suffix($live); } sub get_system_root { my ($live) = @_; $live->{system}{root} . get_region_suffix($live); } sub get_initrd_path { my ($live, $media) = @_; $live->{prefix}{boot} . '/' . $media->{storage} . '/initrd.gz'; } sub get_syslinux_path { my ($live, $media, $o_boot_only) = @_; $live->{prefix}{boot} . '/' . $media->{storage} . '/syslinux' . ($o_boot_only && $media->{boot} && '-boot') . '.cfg'; } sub create_initrd { my ($live) = @_; foreach ($live->{media}, @{$live->{extra_media}}) { create_initrd_for_media($live, $_); } $live->{copy_initrd} and cp_f(get_workdir($live) . get_initrd_path($live, $live->{media}), $live->{copy_initrd}); } sub create_initrd_for_media { my ($live, $media) = @_; my $initrd_tree = get_workdir($live) . $live->{prefix}{initrd} . '/' . $media->{storage}; rm_rf($initrd_tree) if -e $initrd_tree; mkdir_p($initrd_tree . $_) foreach qw(/bin /dev /lib /proc /sys), map { $live->{prefix}{live_mnt} . $_ } $live->{prefix}{media_mnt}, $live->{mount}{root}, map { $_->{mountpoint} } @{$live->{mount}{dirs} || []}; # cp_f(get_system_root($live) . '/sbin/nash', $initrd_tree . '/bin/'); #- use nash from cooker for now, label support cp_f('/sbin/nash', $initrd_tree . '/bin/'); #- needed to mount loopbacks read-only and move mountpoints cp_f('/bin/mount', '/sbin/losetup', $initrd_tree . '/bin/'); cp_f('/lib/ld-linux.so.2', '/lib/tls/libc.so.6', $initrd_tree . '/lib/'); if (get_media_setting($media, 'fs') eq 'nfs') { cp_f('/sbin/ifconfig', $initrd_tree . '/bin/'); if ($live->{debug}) { cp_f('/bin/ping', $initrd_tree . '/bin/'); cp_f('/lib/libresolv.so.2', $initrd_tree . '/lib/'); } } if ($live->{debug}) { cp_f('/usr/bin/strace', $initrd_tree . '/bin/'); cp_f('/usr/bin/busybox', $initrd_tree . '/bin'); my @l = map { /functions:/ .. /^$/ ? do { s/\s//g; split /,/ } : () } `busybox`; shift @l; symlink('busybox', $initrd_tree . "/bin/$_") foreach @l; } require devices; devices::make($initrd_tree . "/dev/$_") foreach qw(console initrd null ram systty), (map { "tty$_" } 0..5), (map { "loop$_" } 0..7); load_moddeps(get_system_root($live), "/lib/modules/" . $live->{system}{kernel}); my ($modules, $skipped) = partition { exists $moddeps{$_} } uniq(map { modules::cond_mapping_24_26($_) } category2modules(get_media_setting($media, 'modules'))); my ($extra_modules, $missing) = partition { exists $moddeps{$_} } nls_modules($live), get_media_fs_module($media), @{get_media_setting($media, 'extra_modules') || []}, (map { @{$loop{$_}{modules} || []} } uniq(map { $_->{type} } grep { $_->{type} } @{$live->{mount}{dirs} || []})), ($live->{mount}{overlay} ? @{$overlay{$live->{mount}{overlay}}{modules} || []} : ()); @$missing and die "missing mandatory modules:" . join("\n", '', sort(@$missing)); push @$modules, @$extra_modules; my @module_deps = uniq(map { moddeps_closure($_) } @$modules); run_('gzip', '>', $initrd_tree . "/lib/$_.ko", '-dc', get_system_root($live) . $moddeps{$_}{full}) foreach @module_deps, @$modules; @$skipped and warn "skipped modules:" . join("\n", '', sort(@$skipped)); #- move ide-generic to end of loaded modules, so that it registers unhandled drives only #- this avoid it to take-over drives which would have been managed by SATA modules my ($head, $tail) = partition { $_ ne 'ide-generic' } @$modules; @$modules = (@$head, @$tail); create_initrd_linuxrc($live, $media, @module_deps, @$modules); compress_initrd_tree($live, $media); add_splash($live, $media); } sub create_initrd_linuxrc { my ($live, $media, @modules) = @_; my $target = $live->{prefix}{live_mnt} . ($live->{mount}{root} || $live->{prefix}{media_mnt}); my $sleep = get_media_setting($media, 'sleep'); my $pre = get_media_setting($media, 'pre'); my $fs = get_media_setting($media, 'fs'); output_with_perm(get_workdir($live) . $live->{prefix}{initrd} . '/' . $media->{storage} . '/linuxrc', 0755, join("\n", "#!/bin/nash", (map { "insmod /lib/$_.ko" } @modules), if_($sleep, "sleep $sleep"), #- required for labels "mount -t proc none /proc", #- required for cdrom labels "mount -t sysfs none /sys", if_($live->{debug}, "/bin/sh"), if_($pre, deref_array($pre)), ($fs eq 'nfs' ? '/bin/mount -n -o ro,nolock' : 'mount') . (get_media_setting($media, 'read_only') && " -o ro") . " -t $fs " . get_media_setting($media, 'source') . " $live->{prefix}{live_mnt}$live->{prefix}{media_mnt}", (map { $loop{$_->{type}}{mount}->($live, $_) } grep { $_->{type} } @{$live->{mount}{dirs} || []}), ($live->{mount}{overlay} ? $overlay{$live->{mount}{overlay}}{mount}->($live) : ()), "echo 0x0100 > /proc/sys/kernel/real-root-dev", "umount /sys", "umount /proc", "pivot_root $target $target/initrd", "/bin/sh -c 'rmdir /initrd$target; cd /initrd$live->{prefix}{live_mnt}; for i in `/bin/ls -1`; do mkdir -p $live->{prefix}{live_mnt}/\$i; mount -n --move \$i $live->{prefix}{live_mnt}/\$i; done'", if_($live->{post}, deref_array($live->{post})), "")); } sub compress_initrd_tree { my ($live, $media) = @_; my $initrd_tree = get_workdir($live) . $live->{prefix}{initrd} . '/' . $media->{storage}; my $size = run_program::get_stdout("du -ks $initrd_tree | awk '{print \$1}'") + 250; my $inodes = run_program::get_stdout("find $initrd_tree | wc -l") + 1250; $size = int($size + $inodes / 10) + 1; #- 10 inodes needs 1K my $initrd = get_workdir($live) . get_initrd_path($live, $media); $initrd =~ s/.gz$//; mkdir_p(dirname($initrd)); run_('dd', 'if=/dev/zero', "of=$initrd", 'bs=1k', "count=$size"); run_('mke2fs', '-q', '-m', 0, '-F', '-N', $inodes, '-s', 1, $initrd); mkdir_p($live->{mnt}); run_('mount', '-o', 'loop', '-t', 'ext2', $initrd, $live->{mnt}); cp_af(glob("$initrd_tree/*"), $live->{mnt}); rm_rf($live->{mnt} . "/lost+found"); run_('umount', $live->{mnt}); run_('gzip', '-f', '-9', $initrd); } sub add_splash { my ($live, $media) = @_; if ($live->{system}{vga_mode} && $live->{system}{splash} ne 'no') { require bootloader; my $initrd = get_workdir($live) . get_initrd_path($live, $media); my $tmp_initrd = '/tmp/initrd.gz'; cp_f($initrd, get_system_root($live) . $tmp_initrd); { local $::prefix = get_system_root($live); bootloader::add_boot_splash($tmp_initrd, $live->{system}{vga_mode}); } cp_f(get_system_root($live) . $tmp_initrd, $initrd); unlink(get_system_root($live) . $tmp_initrd); } } sub build_syslinux_cfg { my ($live, $media, $opts) = @_; #- fastboot is needed to avoid fsck my $append = "fastboot splash=silent vga=$live->{system}{vga_mode}"; #- syslinux wants files at root (used with vfat fs) my $to_root = get_boot_setting($media, 'fs', $opts->{boot_only}) eq 'vfat'; my ($initrd, $kernel, $display, $help) = map { $to_root ? basename($_) : $_ } get_initrd_path($live, $media), map { $live->{prefix}{boot} . '/' . $_ } qw(vmlinuz live.msg help.msg); qq(default live prompt 1 timeout 40 display $display F1 $help label live kernel $kernel append initrd=$initrd $append ); } sub install_system { my ($live) = @_; run_({ timeout => 3600 }, 'drakx-in-chroot', $live->{system}{repository}, get_system_root($live), if_($live->{system}{region}, '--langs', join(':', @{$live->{regions}{$live->{system}{region}}})), if_($live->{system}{auto_install}, '--auto_install', Cwd::abs_path($live->{system}{auto_install})), if_($live->{system}{patch_install}, '--defcfg', Cwd::abs_path($live->{system}{patch_install})), if_($live->{system}{rpmsrate}, '--rpmsrate', Cwd::abs_path($live->{system}{rpmsrate}))) or die "unable to install system chroot"; #- remove urpmi media added by drakx-in-chroot, they're unusable run_({ root => get_system_root($live) }, 'urpmi.removemedia', '-a'); post_install_system($live); } sub post_install_system { my ($live) = @_; if (@{$live->{system}{rpms}}) { my $rpm_tmp_dir = '/tmp/draklive_rpms'; mkdir_p(get_system_root($live) . $rpm_tmp_dir); cp_f(@{$live->{system}{rpms}}, get_system_root($live) . $rpm_tmp_dir); run_({ root => get_system_root($live) }, 'urpmi', '--auto', map { $rpm_tmp_dir . '/' . basename($_) } @{$live->{system}{rpms}}); rm_rf(get_system_root($live) . $rpm_tmp_dir); } my $erase = join(' ', @{$live->{system}{erase_rpms}}); run_({ root => get_system_root($live) }, 'sh', '-c', "rpm -qa $erase | xargs rpm -e ") if $erase; run_({ root => get_system_root($live) }, 'chkconfig', '--del', $_) foreach @{$live->{system}{disable_services}}; my @patches = map { Cwd::abs_path($_) } @{$live->{system}{patches}}; each_index { !defined $_ and die "unable to find file " . $live->{system}{patches}[$::i] } @patches; run_('patch', '-p0', '-s', '-N', '-F', 0, '-d', get_system_root($live), '-r', '/tmp', '-i', $_) foreach @patches; foreach (@{$live->{system}{files}}) { my $dest = pop @$_; cp_af(@$_, get_system_root($live) . $dest); } #- 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 => get_system_root($live) }, 'perl', '-MStorable', '-e', qq(Storable::store({ UNKNOWN => {} }, '/etc/sysconfig/harddrake2/previous_hw'))); #- remove some build-machine specific configuration substInFile { undef $_ if /^[^#]/ } get_system_root($live) . $_ foreach qw(/etc/fstab /etc/mtab /etc/modprobe.conf /etc/modprobe.preload /etc/iftab /etc/shorewall/interfaces); #- run harddrake because a crappy snd-usb-audio workaround may do something at shutdown #- (do it after the modprobe files are cleaned) run_({ root => get_system_root($live) }, '/usr/share/harddrake/service_harddrake', 'stop'); #- interactive mode can lead to race in initscripts #- (don't use addVarsInSh from MDK::Common, it breaks shell escapes) substInFile { s/^PROMPT=.*/PROMPT=no/ } get_system_root($live) . '/etc/sysconfig/init'; #- disable first boot wizard output(get_system_root($live) . '/etc/sysconfig/firstboot', 'FIRSTBOOT=no'); #- enable drakx-finish-install output(get_system_root($live) . '/etc/sysconfig/finish-install', qq( FINISH_INSTALL=yes NETWORK=no AUTHENTICATION=no USERS=no )); #- preselect guest user in kdm my $kdm_cfg = '/etc/kde/kdm/kdmrc'; update_gnomekderc(get_system_root($live) . $kdm_cfg, 'X-:0-Greeter' => (PreselectUser => 'Default', DefaultUser => 'guest')) if -f $kdm_cfg; #- don't make kbluetoothdrc display an annoying and useless popup window my $kbluetoothd_cfg = '/etc/kde/kbluetoothdrc'; update_gnomekderc(get_system_root($live) . $kbluetoothd_cfg, 'Notification Messages', 'kbluepinLocInfo-usr-lib-kdebluetooth' => 'false'); chmod 0644, get_system_root($live) . $kbluetoothd_cfg; } sub create_loopback_files { my ($live) = @_; mkdir_p(get_workdir($live) . $live->{prefix}{loopbacks}); foreach (grep { $_->{build_from} } @{$live->{mount}{dirs} || []}) { my $tree = get_system_root($live) . $_->{build_from}; my $dest = get_workdir($live) . $live->{prefix}{loopbacks} . '/' . $_->{source}; unlink($dest); $loop{$_->{type}}{build}->($dest, $tree); } foreach (grep { $_->{pre_allocate} } @{$live->{mount}{dirs} || []}) { my $dest = get_workdir($live) . $live->{prefix}{loopbacks} . '/' . $_->{source}; unlink($dest); $loop{$_->{type}}{create}->($dest, $_->{pre_allocate}); } } #- mainly for storage-specific subroutines sub get_storage_setting { my ($media, $setting) = @_; $storage{$media->{storage}}{$setting}; } #- for actions that support an optionnal boot storage type sub get_boot_setting { my ($media, $setting, $boot_only) = @_; $boot_only && $media->{boot} ? $storage{$media->{boot}}{$setting} : get_storage_setting($media, $setting); } #- for user-customisable media setting, that can override storage setting sub get_media_setting { my ($media, $setting) = @_; $media->{$setting} || get_storage_setting($media, $setting); } sub get_media_fs_module { my ($media) = @_; my $fs = get_media_setting($media, 'fs'); $fs eq 'iso9660' ? 'isofs' : $fs; } sub get_media_label { my ($media) = @_; first(get_media_setting($media, 'source') =~ /^LABEL=(.*)$/); } sub get_media_device { my ($media) = @_; return $media->{device} if $media->{device}; my $label = get_media_label($media) or return get_media_setting($media, 'source'); my $device = chomp_(`readlink -f /dev/disk/by-label/$label`) or die "unable to find device for /dev/disk/by-label/$label"; $device; } sub prepare_bootloader { my ($live, $opts) = @_; create_initrd($live); cp_f(get_system_root($live) . '/boot/vmlinuz-' . $live->{system}{kernel}, get_workdir($live) . $live->{prefix}{boot} . '/vmlinuz'); require bootsplash; my $theme = do { local $::prefix = get_system_root($live); bootsplash::themes_read_sysconfig('800x600'); }; my $msg = get_system_root($live) . "/usr/share/bootsplash/themes/$theme->{name}/lilo/syslinux"; if (-f $msg) { print "using $msg as syslinux splash image\n"; cp_f($msg, get_workdir($live) . $live->{prefix}{boot} . '/live.msg'); } else { warn "unable to find syslinux splash ($msg)"; } output(get_workdir($live) . $live->{prefix}{boot} . '/help.msg', pack("C*", 0x0E, 0x80, 0x03, 0x00, 0xC) . qq( Welcome to Mandriva live! The command line can be used to specify kernel options. live )); foreach ($live->{media}, @{$live->{extra_media}}) { output(get_workdir($live) . get_syslinux_path($live, $_, $opts->{boot_only}), build_syslinux_cfg($live, $_, $opts)); } } sub get_cdrom_master_path { my ($live, $opts) = @_; get_workdir($live) . $live->{prefix}{images} . '/' . ($opts->{boot_only} ? 'boot' : 'live') . '.iso'; } sub create_cdrom_master { my ($live, $media, $opts) = @_; my $label = get_media_label($media) or die "the source device must be described by a label"; my @dest; unless ($opts->{onthefly}) { @dest = ('-o', get_cdrom_master_path($live, $opts)); mkdir_p(dirname($dest[1])); } run_('mkisofs', '-pad', '-l', '-R', '-J', '-v', '-v', '-V', $label, #'-A', $application, '-p', $preparer, '-P', $publisher, '-b', 'isolinux/isolinux.bin', '-c', 'isolinux/boot.cat', '-hide-rr-moved', '-no-emul-boot', '-boot-load-size', 4, '-boot-info-table', '-graft-points', @dest, 'isolinux/isolinux.bin=/usr/lib/syslinux/isolinux-graphic.bin', 'isolinux/isolinux.cfg=' . get_workdir($live) . get_syslinux_path($live, $media, $opts->{boot_only}), $live->{prefix}{boot} . '=' . get_workdir($live) . $live->{prefix}{boot}, if_(!$opts->{boot_only}, $live->{prefix}{loopbacks} . '=' . get_workdir($live) . $live->{prefix}{loopbacks}), ); } #- $opts: #- media: alternate media #- onthefly : if true, the create function must output to stdout sub create_master { my ($live, $opts) = @_; my $media = $opts->{media} || $live->{media}; if (my $create = get_boot_setting($media, 'create', $opts->{boot_only})) { $create->($live, $media, $opts); } else { warn "not implemented yet"; } } sub maybe_umount_device { my ($device) = @_; run_('umount', $device) if cat_('/proc/mounts') =~ m!^$device\s+!m; } sub format_cdrom_device { my ($_live, $media) = @_; run_('cdrecord', '-v', 'dev=' . $media->{device}, "blank=fast"); } sub format_usb_device { my ($_live, $media) = @_; maybe_umount_device($media->{device}); run_('mkdosfs', $media->{device}) or die "unable to format device $media->{device}"; } #- $opts: #- media: alternate media sub format_device { my ($live, $opts) = @_; my $media = $opts->{media} || $live->{media}; $media->{device} or die "no device defined in media configuration"; if (my $format = get_boot_setting($media, 'format', $opts->{boot_only})) { $format->($live, $media); } else { warn "not implemented yet"; } } sub record_cdrom_master { my ($live, $media, $opts) = @_; $media->{device} or die "no device defined in media configuration"; my $src = $opts->{onthefly} ? '-' : get_cdrom_master_path($live, $opts); run_('cdrecord', '-v', 'dev=' . $media->{device}, $src); } sub record_usb_master { my ($live, $media, $opts) = @_; if (my $label = $media->{device} && get_media_label($media)) { run_('mlabel', '-i', $media->{device}, '::' . $label); } my $device = get_media_device($media) or die "unable to find recording device (missing label? try with --device )"; mkdir_p($live->{mnt}); run_('mount', $device, $live->{mnt}) or die "unable to mount $device"; cp_af(get_workdir($live) . $live->{prefix}{boot}, $live->{mnt}); cp_f(get_workdir($live) . get_syslinux_path($live, $media, $opts->{boot_only}), $live->{mnt}); cp_f(get_workdir($live) . $_, $live->{mnt}) foreach get_initrd_path($live, $media), map { $live->{prefix}{boot} . '/' . $_ } qw(vmlinuz live.msg); run_('rsync', '-vdP', '--inplace', get_workdir($live) . $live->{prefix}{loopbacks} . '/', $live->{mnt} . $live->{prefix}{loopbacks}) unless $opts->{boot_only}; run_('umount', $live->{mnt}); maybe_umount_device($device); #- use syslinux -s, "safe, slow and stupid" version of SYSLINUX run_('syslinux', '-s', $device) or die "unable to run syslinux on $device"; } #- $opts: #- media: alternate media #- onthefly : if true, the record function must read from stdin sub record_master { my ($live, $opts) = @_; my $media = $opts->{media} || $live->{media}; if (my $record = get_boot_setting($media, 'record', $opts->{boot_only})) { $record->($live, $media, $opts); } else { warn "not implemented yet"; } } sub pipe_subs { my ($writer, $reader) = @_; my ($r, $w) = POSIX::pipe; if (my $pid = fork()) { POSIX::close($w) or die "couldn't close: $!\n"; my $stdin = POSIX::dup(0) or die "couldn't dup: $!\n"; POSIX::dup2($r, 0) or die "couldn't dup2: $!\n"; POSIX::close($r); $reader->(); POSIX::close(0) or warn "writer exited $?"; POSIX::dup2($stdin, 0) or die "couldn't dup2: $!\n"; waitpid($pid, 0); } else { POSIX::close($r) or die "couldn't close: $!\n"; #- don't screw up reader POSIX::dup2(POSIX::open('/dev/null', &POSIX::O_WRONLY), 2) or die "couldn't dup2: $!\n"; POSIX::dup2($w, 1) or die "couldn't dup2: $!\n"; POSIX::close($w); $| = 1; #- autoflush write exit !$writer->(); } } sub record_onthefly { my ($live, $opts) = @_; my $media = $opts->{media} || $live->{media}; my $record = get_storage_setting($media, 'record'); unless ($record) { warn "not implemented yet"; return; } if (my $create = get_storage_setting($media, 'create')) { #- pipe creation step to recording step pipe_subs(sub { $create->($live, $media, { onthefly => 1 }) }, sub { $record->($live, $media, { onthefly => 1 }) }); } else { #- no creation step, record directly $record->($live, $media); } } sub copy_wizard { my ($live) = @_; my $root = get_workdir($live) . $live->{prefix}{boot}; my @available_storage = sort(grep { -d "$root/$_" && exists $storage{$_}{detect} } all($root)); my @available_devices; require interactive; require wizards; my $in = 'interactive'->vnew('su'); my ($storage, $device, $format); my $w = wizards->new({ name => N("Live system copy wizard"), pages => { welcome => { name => N("Welcome to the live system copy wizard"), next => 'storage', }, storage => { name => N("Please select the medium type"), data => [ { type => 'list', allow_empty_list => 1, val => \$storage, list => \@available_storage } ], next => 'device', }, device => { name => N("Please select the device that will contain the new live system"), pre => sub { my %devices = map { $_->{device} => $_ } $storage{$storage}{detect}->(); $_->{formatted_name} = $_->{usb_description} || $_->{info} || $_->{device} foreach values %devices; @available_devices = (); require fs::proc_partitions; foreach (fs::proc_partitions::read([ values %devices ])) { if ($_->{rootDevice} && exists $devices{$_->{rootDevice}}) { my $description = $devices{$_->{rootDevice}}{usb_description} || $devices{$_->{rootDevice}}{info}; $_->{formatted_name} = $description ? "$description ($_->{device})" : $_->{device}; push @available_devices, $_; } } delete $devices{$_->{rootDevice}} foreach @available_devices; unshift @available_devices, map { $devices{$_} } sort keys %devices; undef $device; }, data => [ { type => 'list', allow_empty_list => 1, val => \$device, , list => \@available_devices, format => sub { $_[0]{formatted_name} } }, { text => N("Format selected device"), val => \$format, type => 'bool' } ], complete => sub { return 0 if defined $device; $in->ask_warn(N("Error"), N("You must select a device!")); 1; }, post => sub { my $media = { media => { storage => $storage, device => '/dev/' . $device->{device} } }; format_device($live, $media) if $format; record_onthefly($live, $media); }, end => 1, }, } }); $w->process($in); } sub complete_config { my ($live) = @_; my $default_prefix = { media_mnt => '/media', live_mnt => '/live', loopbacks => '/loopbacks', images => '/images', boot => '/boot', initrd => '/initrd', }; if ($live->{copy_wizard}) { add2hash($live->{prefix} ||= {}, $default_prefix); #- assumes the current live media is mounted there $live->{workdir} = $live->{prefix}{live_mnt} . $live->{prefix}{media_mnt}; $live->{mnt} = '/tmp/mnt'; } else { #- set unsupplied config dirs $live->{workdir} ||= '/tmp/draklive'; $live->{mnt} ||= get_workdir($live) . "/mnt"; #- check for minimum requirements ref $live->{media} && $live->{media}{storage} or die "no media storage definition"; ref $live->{system} or die "no system definition"; $live->{system}{kernel} or die "no kernel has been configured"; mkdir_p(get_workdir($live)); add2hash($live->{prefix} ||= {}, $default_prefix); } } sub clean { my ($live) = @_; rm_rf($_) foreach grep { -e $_ } get_workdir($live), get_system_root($live); } my @actions = ( { 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 }, { name => 'format', do => \&format_device }, { name => 'record', do => \&record_master }, { name => 'record-onthefly', do => \&record_onthefly }, ); my @all = qw(install boot loop master); require_root_capability(); my (%live, %opts); GetOptions( "help" => sub { Pod::Usage::pod2usage('-verbose' => 1) }, "copy-wizard" => \$live{copy_wizard}, "boot-only" => \$opts{boot_only}, "all" => sub { $_->{to_run} = 1 foreach grep { member($_->{name}, @all) } @actions }, (map { $_->{name} => \$_->{to_run} } @actions), "device:s" => sub { $live{media}{device} = $_[1] }, "region:s" => sub { $live{system}{region} = $_[1] }, "all-regions" => sub { $live{all_regions} = 1 }, "config:s" => sub { my $path = $_[1]; #- don't use do(), since it can't see lexicals in the enclosing scope my $cfg = eval(cat_($path)) or die "unable to load $path: $@"; put_in_hash(\%live, $cfg); print "loaded $path as config file\n"; }, ) or Pod::Usage::pod2usage(); require standalone; if ($live{copy_wizard}) { complete_config(\%live); copy_wizard(\%live); } else { every { !$_->{to_run} } @actions and die 'nothing to do'; unless (keys(%live)) { warn 'no live definition'; Pod::Usage::pod2usage(); } foreach my $region ($live{all_regions} ? sort(keys %{$live{regions}}) : $live{system}{region}) { $region and print qq(=== proceeding with region "$region"\n); $live{system}{region} = $region; complete_config(\%live); foreach (grep { $_->{to_run} } @actions) { print qq(* entering step "$_->{name}"\n); $_->{do}->(\%live, \%opts); 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 --device use this device for live recording (not needed if the device already has the required label) --format format selected device --record record live on selected media --record-onthefly record live by creating master from loopback files on the fly --initrd build initrd only --post-install run post install only (rpms and patches installation) --config use this configuration file as live description --region use the matching set of langs (from the regions configuration hash) --all-regions proceed with all configured regions --copy-wizard run the copy wizard --boot-only consider only boot media (affects boot/master/format/record steps) Examples: draklive --config config/live.cfg --clean draklive --config config/live.cfg --all draklive --config config/live.cfg --record --device /dev/sdb1 =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: { system => { root => '/chroot/live-move', repository => '/mnt/ken/2006.0/i586', 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, }, media => { storage => 'cdrom', }, extra_media => [ { storage => 'usb', }, ], mount => $predefined{mounts}{squash_union} }; =back =head1 DESCRIPTION B builds a live distribution according to a configuration file, creates a master image, and optionnally installs it on a device. See L =head1 AUTHOR Olivier Blin =cut