#!/usr/bin/perl

# draklive $Id: draklive 142254 2007-03-13 11:44:51Z blino $

# Copyright (C) 2005 Mandriva
#                    Olivier Blin <oblin@mandriva.com>
#
# 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 fs;
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',
    # perl -MMDK::Common -e 'print map_index { (32767 - $::i) . " $_" } grep { !m,^/(?:dev|proc|sys|live/distrib), } uniq(<>)' < bootlog.list > config/distrib.sort
    sort => "config/distrib.sort",
    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 => [
                {
                    mountpoint => '/media',
                    type => 'plain',
                },
                $dir_memory,
            ],
        },
        volatile_squash_union => {
            root => '/union',
            overlay => 'unionfs',
            dirs => [
                $dir_distrib_sqfs,
                $dir_memory,
            ],
        },
        multi_squash_union => {
            root => '/union',
            overlay => 'unionfs',
            dirs => [
                $dir_distrib_sqfs,
                {
                    mountpoint => '/system',
                    type => 'squashfs',
                    source => 'system.sqfs'
                },
                $dir_memory,
            ],
        },
    },
);

# 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',
                storage => 'nfs',
                pre => "ifconfig eth0 $client up",
                source => $source,
            };
        },
    },
    mounts => {
        squash_union => sub {
            my ($default_size, $o_min_size) = @_;
            {
                root => '/union',
                overlay => 'unionfs',
                dirs => [
                    $dir_distrib_sqfs,
                    {
                        mountpoint => '/system',
                        type => 'loopfs',
                        pre_allocate => $default_size,
                        if_(defined $o_min_size, min_size => $o_min_size),
                        fs => 'ext2',
                        source => 'system.loop'
                    },
                    {
                        mountpoint => '/system',
                        type => 'tmpfs',
                        fallback => 1,
                    },
                ],
            };
        },
    },
);

my %storage = (
    cdrom => {
        modules => 'bus/usb disk/cdrom|hardware_raid|ide|raw|sata|scsi|usb',
        fs => 'iso9660',
        source => 'LABEL=MDVCDROOT',
        read_only => 1,
        detect => \&detect_devices::burners,
        create => \&create_cdrom_master,
        format => \&format_cdrom_device,
        record_needs_master => 1,
        record => \&record_cdrom_master,
    },
    usb => {
        modules => 'bus/usb disk/raw|usb',
        fs => 'vfat',
        bootloader => 'grub',
        source => 'LABEL=MDVUSBROOT',
        detect => sub { grep { detect_devices::isKeyUsb($_) } detect_devices::get() },
        create => \&create_usb_master,
        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 ($live, $dir) = @_;
            my $dest = get_builddir($live) . $live->{prefix}{build}{loopbacks} . '/' . $_->{source};
            my $root = get_system_root($live) . $_->{build_from};
            my $total = directory_usage($root);
            print "have to process " . int($total/1000000) . " MB\n";
            my $progress = progress_start($total, time(), 6);
            my $sort = Cwd::abs_path($dir->{sort});
            run_foreach(sub {
                            if (/^mksquashfs: file .*, uncompressed size (\d+) bytes\s*(?:DUPLICATE|LINK)?$/) {
                                progress_show_incr($progress, $1, time());
                            }
                        },
                        'mksquashfs', $root, $dest, '-noappend', '-info', if_(-f $sort, '-sort', $sort))
              or die "unable to run mksquashfs";
            progress_end();
        },
        mount => sub {
            my ($live, $dir) = @_;
            $dir->{loop} = "/dev/loop" . $loop_number++;
            (
                "/bin/losetup $dir->{loop} $live->{prefix}{live}{mnt}$live->{prefix}{media}{mnt}$live->{prefix}{media}{loopbacks}/$dir->{source}",
                "mount -o ro -t squashfs $dir->{loop} $live->{prefix}{live}{mnt}$dir->{mountpoint}",
            );
        },
    },
    loopfs => {
        modules => [],
        build => sub {
            my ($live, $dir) = @_;
            my $dest = get_builddir($live) . $live->{prefix}{build}{loopbacks} . '/' . $_->{source};
            device_allocate_fs($dest, $dir->{pre_allocate});
            device_mkfs($dest, $dir->{fs}) if !defined $dir->{min_size};
        },
        mount => sub {
            my ($live, $dir) = @_;
            $dir->{loop} = "/dev/loop" . $loop_number++;
            my $fsck = "chroot </dev/tty1 $live->{prefix}{live}{mnt}$dir_distrib_sqfs->{mountpoint} /sbin/fsck $dir->{loop}";
            (
                "losetup $dir->{loop} $live->{prefix}{live}{mnt}$live->{prefix}{media}{mnt}$live->{prefix}{media}{loopbacks}/$dir->{source}",
                qq(sh -c "$fsck -a || $fsck -y"),
                "mount -t $dir->{fs} $dir->{loop} $live->{prefix}{live}{mnt}$dir->{mountpoint}",
            );
        },
    },
    plain => {},
    tmpfs => {
        mount => sub {
            my ($live, $dir) = @_;
            my $mnt = $live->{prefix}{live}{mnt} . $dir->{mountpoint};
            my $cmd = "mount -t tmpfs none $mnt";
            $dir->{fallback} ? qq(sh -c 'if ! grep -q " $mnt " /proc/mounts; then $cmd; fi') : $cmd;
        },
    },
);

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}=" .
                                    (!$loop{$_->{type}}{read_only} ? 'rw' : 'ro');
                                } reverse grep { !$_->{fallback} } @{$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 directory_usage { first(split /\s/, `du -sb $_[0]`) }

sub run_ {
    my $options = ref $_[0] eq 'HASH' ? shift @_ : {};
    my @cmd = @_;
    $options->{timeout} ||= 'never';
    my $setarch = delete $options->{setarch};
    unshift @cmd, 'setarch', $setarch if $setarch;
    print STDERR "running " . (exists $options->{root} && "(in chroot) ") . join(' ', @cmd) . "\n";
    run_program::raw($options, @cmd);
}

sub run_foreach {
    my ($foreach, @command) = @_;
    print STDERR "running " . join(' ', @command) . "\n";
    open(my $OUTPUT, '-|', @command);
    local $_; #- avoid outside $_ to be overwritten
    $foreach->() while <$OUTPUT>;
    close $OUTPUT;
}

sub get_region_suffix {
    my ($live) = @_;
    my $region = $live->{settings}{region} || 'noregion';
    ($live->{settings}{desktop} || 'generic') . '-' . $region . '-' . $live->{settings}{arch};
}

sub get_builddir {
    my ($live) = @_;
    $live->{settings}{builddir} . '/' . get_region_suffix($live);
}

sub get_system_root {
    my ($live) = @_;
    $live->{settings}{chroot} . '/' . get_region_suffix($live);
}

sub get_initrd_path {
    my ($live, $media) = @_;
    '/' . $media->{storage} . '/initrd.gz';
}

sub get_syslinux_path {
    my ($live, $media, $opts) = @_;
    '/' . $media->{storage} . '/syslinux' . ($opts->{boot} && '-boot-' . $opts->{boot}) . '.cfg';
}

sub find_kernel {
    my ($live) = @_;
    my $kernel = $live->{system}{kernel};
    unless ($kernel) {
        my $vmlinuz = readlink(get_system_root($live) . '/boot/vmlinuz');
        $vmlinuz ||= find { -e $_ } glob_(get_system_root($live) . '/boot/vmlinuz-*');
        $kernel = first($vmlinuz =~ /\bvmlinuz-(.*)$/) or die "no kernel can be found";
    }
    $kernel;
}

sub create_initrd {
    my ($live) = @_;
    foreach ($live->{media}, @{$live->{extra_media}}) {
        create_initrd_for_media($live, $_);
    }
    cp_f(get_builddir($live) . $live->{prefix}{build}{boot} . get_initrd_path($live, $live->{media}),
         $live->{copy_initrd}) if $live->{copy_initrd};
}

sub create_initrd_for_media {
    my ($live, $media) = @_;

    my $initrd_tree = get_builddir($live) . $live->{prefix}{build}{initrd} . '/' . $media->{storage};
    rm_rf($initrd_tree) if -e $initrd_tree;

    my ($lib_prefix, $need_libs, @libs);
    foreach (qw(/lib64 /lib)) {
        my @ld = glob(get_system_root($live) . $_ . '/ld-*.so.*');
        my @libc = glob(get_system_root($live) . $_ . '/libc.so.*');
        if (@ld && @libc) {
            @libs = (@ld, @libc);
            $lib_prefix = $_;
            last;
        }
    }
    $lib_prefix or die 'unable to find system libraries in /lib or /lib64';

    mkdir_p($initrd_tree . $_) foreach
      qw(/bin /dev /proc /sys), $lib_prefix,
      map { $live->{prefix}{live}{mnt} . $_ }
        $live->{prefix}{media}{mnt},
        $live->{mount}{root},
        map { $_->{mountpoint} } @{$live->{mount}{dirs} || []};

    #- use nash with label support
    cp_f(get_system_root($live) . '/sbin/nash', $initrd_tree . '/bin/');

    #- busybox is required to:
    #-   detect usb-storage process (we need sh/while/ps/grep)
    #-   mount loopbacks read-only with losetup (useful over NFS)
    my $busybox = get_system_root($live) . '/usr/bin/busybox';
    cp_f($busybox, $initrd_tree . '/bin')
      or die 'unable to copy busybox from system chroot';
    my @l = map { /functions:/ .. /^$/ ? do { s/\s//g; split /,/ } : () } `$busybox`;
    shift @l;
    symlink('busybox', $initrd_tree . "/bin/$_") foreach @l;

    my $rrpt_dev = get_media_setting($media, 'rereadpt');
    if ($rrpt_dev) {
        $need_libs = 1;
        cp_f(get_system_root($live) . '/sbin/blockdev', $initrd_tree . '/bin')
          or die 'unable to copy blockdev from system chroot';
    }

    if (get_media_setting($media, 'fs') eq 'nfs') {
        $need_libs = 1;
        cp_f(get_system_root($live) . '/sbin/ifconfig', $initrd_tree . '/bin/');
        #- needed to mount NFS (with nolock)
        cp_f(get_system_root($live) . '/bin/mount', $initrd_tree . '/bin/');
        if ($live->{debug}) {
            cp_f(get_system_root($live) . '/bin/ping', $initrd_tree . '/bin/');
            cp_f(glob(get_system_root($live) . $lib_prefix . '/libresolv*.so.*'), $initrd_tree . $lib_prefix);
        }
    }
    if ($live->{debug}) {
        $need_libs = 1;
        cp_f(get_system_root($live) . '/usr/bin/strace', $initrd_tree . '/bin/');
    }

    cp_f(@libs, $initrd_tree . $lib_prefix) if $need_libs;

    require devices;
    devices::make($initrd_tree . "/dev/$_") foreach
        if_($rrpt_dev, $rrpt_dev),
        qw(console initrd null ram systty),
        (map { "tty$_" } 0..5),
        (map { "loop$_" } 0..7);

    my $kernel = find_kernel($live);
    print "using kernel $kernel\n";
    load_moddeps(get_system_root($live), "/lib/modules/" . $kernel);

    my ($modules, $skipped) = partition { exists $moddeps{$_} }
      uniq(map { modules::cond_mapping_24_26($_) } category2modules(get_media_setting($media, 'modules')));
    my ($extra_modules, $extra_missing) = partition { exists $moddeps{$_} }
      nls_modules($live),
      get_media_fs_module($media),
      @{get_media_setting($media, 'extra_modules') || []},
      (map { @{$loop{$_}{modules} || []} } uniq(map { $_->{type} } @{$live->{mount}{dirs} || []})),
      ($live->{mount}{overlay} ? @{$overlay{$live->{mount}{overlay}}{modules} || []} : ());

    my @additional_modules = map { m!([^/]+)\.ko! } @{$live->{system}{additional_modules}};
    cp_f($_, $initrd_tree . "/lib/") foreach @{$live->{system}{additional_modules}};

    my @missing = sort(difference2($extra_missing, \@additional_modules));
    @missing and die "missing mandatory modules:\n" . join("\n", @missing);
    push @$modules, @$extra_modules;

    my @module_deps = uniq(map { moddeps_closure($_) } @$modules);
    mkdir_p($initrd_tree . "/lib");
    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, @additional_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 $pre = get_media_setting($media, 'pre');
    my $fs = get_media_setting($media, 'fs');
    my $rrpt_dev = get_media_setting($media, 'rereadpt');
    my $debug_shell = "sh -c 'if grep -q debug /proc/cmdline; then exec sh; fi'";
    my @mount_options = (
        if_(get_media_setting($media, 'read_only'), "ro"),
        grep { $_ } get_media_setting($media, 'mount_options'),
    );
    output_with_perm(get_builddir($live) . $live->{prefix}{build}{initrd} . '/' . $media->{storage} . '/linuxrc', 0755,
                     join("\n",
                          "#!/bin/nash",
                          (map { "insmod /lib/$_.ko" } @modules),
                          #- required for labels and ps
                          "mount -t proc none /proc",
                          #- required for cdrom labels
                          "mount -t sysfs none /sys",
                          if_(member('usb-storage', @modules),
                              #- wait some seconds for the usb-stor-scan process to be run
                              "sleep 2",
                              q(sh -c 'while ps | grep -q \\\[usb-stor-scan\\\]; do sleep 1; done')),
                          if_($rrpt_dev,
                              "echo *** Waiting for new partitions on device ${rrpt_dev} ***",
                              "sh -c 'while ! ls /sys/block/${rrpt_dev}/${rrpt_dev}* >/dev/null 2>&1; do sleep 3; blockdev --rereadpt /dev/${rrpt_dev} >/dev/null 2>&1; done'"),
                          $debug_shell,
                          if_($pre, deref_array($pre)),
                          ($fs eq 'nfs' ? '/bin/mount -n -o ro,nolock' : 'mount') .
                            if_(@mount_options, " -o " . join(",", @mount_options)) .
                            " -t $fs " . get_media_setting($media, 'source') . " $live->{prefix}{live}{mnt}$live->{prefix}{media}{mnt}",
                          (map { $loop{$_->{type}}{mount}->($live, $_) } grep { exists $loop{$_->{type}}{mount} } @{$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",
                          "sh -c 'rmdir /initrd$target; cd /initrd$live->{prefix}{live}{mnt}; for i in `ls -1`; do mkdir -p $live->{prefix}{live}{mnt}/\$i; mount -n --move \$i $live->{prefix}{live}{mnt}/\$i; done'",
                          if_($live->{system}{initrd_post}, deref_array($live->{system}{initrd_post})),
                          ""));
}

sub compress_initrd_tree {
    my ($live, $media) = @_;

    my $initrd_tree = get_builddir($live) . $live->{prefix}{build}{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_builddir($live) . $live->{prefix}{build}{boot} . 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_builddir($live) . $live->{prefix}{build}{boot} . 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 get_default_append {
    my ($live) = @_;
    join(' ',
         'fastboot', #- needed to avoid fsck
         if_($live->{system}{vga_mode},
             'splash=silent',
             'vga=' . $live->{system}{vga_mode}),
     );
}

my @syslinux_boot_files = qw(/vmlinuz /bootlogo /help.msg);

sub build_syslinux_cfg {
    my ($live, $media, $opts) = @_;
    my $append = get_default_append($live);
    #- syslinux wants files at root (used with vfat fs)
    my $to_root = get_boot_setting($media, 'fs', $opts) eq 'vfat';
    my ($initrd, $kernel, $bootlogo, $help) = map { $to_root ? basename($_) : $_ }
      map { $live->{prefix}{media}{boot} . $_ } get_initrd_path($live, $media), @syslinux_boot_files;
    my $has_bootlogo = -e get_builddir($live) . $live->{prefix}{media}{boot} . '/bootlogo';
    join("\n",
         "default $live->{media}{title}",
         "prompt 1",
         "timeout 40",
         $has_bootlogo ? (
             "gfxboot $bootlogo",
             "F1 $help",
         ) :
             "display $help",
         (map {
             my ($name, $cmdline) = @$_;
             $name =~ s/\s/_/g;
             "label " . ($name || $live->{media}{title}),
             "    kernel $kernel",
             "    append initrd=$initrd $append $cmdline";
         } group_by2('' => '', @{$live->{system}{boot_entries}})),
         "",
     );
}

sub build_grub_cfg {
    my ($live, $media, $device) = @_;
    #- FIXME: use the bootloader module from drakx
    my ($part_nb) = $device =~ /(\d+)$/;
    my $grub_part = "(hd0" . (defined $part_nb ? "," . ($part_nb-1) : "") . ")";
    join("\n",
         "timeout 4",
         "color 2",
         "splashimage $grub_part" . $live->{prefix}{media}{boot} . "/grub/splash.xpm.gz",
         "default 0",
         "viewport 3 2 77 22",
         "shade 1",
         (map {
             my ($name, $cmdline) = @$_;
             "title " . $live->{media}{title} . if_($name, " ($name)"),
             "kernel $grub_part" . $live->{prefix}{media}{boot} . "/vmlinuz " . get_default_append($live) . if_($cmdline, " $cmdline"),
             "initrd " . $live->{prefix}{media}{boot} . get_initrd_path($live, $media);
         } group_by2('' => '', @{$live->{system}{boot_entries}})),
         "",
     );
}

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 = get_builddir($live) . $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";
        $drakx_in_chroot = $local_drakx_in_chroot;
    }

    local %ENV = (%ENV, %{$live->{system}{install_env}});
    $ENV{DRAKLIVE_LANGS} = join(':', uniq((ref $live->{regions} ? @{$live->{regions}{$live->{settings}{region}}} : ()), @{$live->{system}{langs_always}}));
    $ENV{DRAKLIVE_REGION} = $live->{settings}{region};
    run_({ setarch => $live->{settings}{arch} },
	 'perl', $drakx_in_chroot,
         $repository,
         get_system_root($live),
         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})),
         ($live->{system}{stage2_updates} ? (map { ('--stage2-update', Cwd::abs_path($_->[0]), $_->[1]) } @{$live->{system}{stage2_updates}}) : ()),
    ) or die "unable to install system chroot";
    post_install_system($live);
}

sub configure_draklive_resize {
    my ($live) = @_;

    my $resizable_loopback = find { $_->{min_size} } @{$live->{mount}{dirs} || []};
    if ($resizable_loopback) {
        output(get_system_root($live) . '/etc/sysconfig/draklive-resize', <<EOF);
DRAKLIVE_RESIZE=yes
LOOPBACK=$live->{prefix}{live}{mnt}$live->{prefix}{media}{mnt}$live->{prefix}{media}{loopbacks}/$resizable_loopback->{source}
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 ($files, $root) = @_;
    foreach (@$files) {
        my ($source, $dest, $o_perm) = @$_;
        $dest = $root . '/' . $dest;
	mkdir_p(dirname($dest));
        cp_f(glob($source), $dest);
        chmod $o_perm, $dest if $o_perm;
    }
}

sub post_install_system {
    my ($live) = @_;

    my $previous_umask = umask;
    #- workaround buggy installation of directories that are not owned by any packages
    umask 022;

    #- remove previous draklive leftovers if needed
    run_({ root => get_system_root($live) }, 'urpmi.removemedia', '-a');

    foreach (@{$live->{system}{additional_media}}) {
        run_({ root => get_system_root($live) }, 'urpmi.addmedia', if_($_->{distrib}, '--distrib'), $_->{name}, $_->{path})
          or die "unable to add media from $_->{path}";
        @{$_->{packages}} or next;
        run_({ root => get_system_root($live), setarch => $live->{settings}{arch} },
	     'urpmi', '--auto', '--no-verify-rpm', if_(!$_->{distrib}, '--searchmedia', $_->{name}), @{$_->{packages}})
          or die "unable to install packages from $_->{path}";
    }

    #- additional rpms may have dependencies in additional media
    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), setarch => $live->{settings}{arch} },
	     'urpmi', '--auto', '--no-verify-rpm',
             map { $rpm_tmp_dir . '/' . basename($_) } @{$live->{system}{rpms}})
          or die "unable to install additional system rpms";
        rm_rf(get_system_root($live) . $rpm_tmp_dir);
    }

    #- remove urpmi media added by drakx-in-chroot and additional media, they're unusable
    run_({ root => get_system_root($live) }, 'urpmi.removemedia', '-a');

    my $erase = join(' ', @{$live->{system}{erase_rpms}});
    run_({ root => get_system_root($live), setarch => $live->{settings}{arch} },
	 'sh', '-c', "rpm -qa $erase | xargs rpm -e ") if $erase;

    run_({ root => get_system_root($live) }, 'chkconfig', '--del', $_) foreach @{$live->{system}{disable_services}};

    #- 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 /etc/mdadm.conf);
    unlink($_) foreach map { glob_(get_system_root($live) . $_) } (
        "/etc/modprobe.preload.d/*",
        "/etc/udev/rules.d/61-*_config.rules",
        "/etc/dbus-1/machine-id",
    );
    output_with_perm(get_system_root($live) . '/etc/fstab', 0644, "none / unionfs rw 0 0\n");

    #- 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';

    configure_draklive_resize($live);

    if ($live->{system}{preselect_kdm_user}) {
        #- preselect specified user in kdm
        my $kdm_cfg = get_system_root($live) . '/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 = Cwd::abs_path($_) or die "unable to find file " . $_;
        my @command = ('patch', '-p0', '-d', get_system_root($live), '-i', $patch);
        run_(@command, '--dry-run', '-f', '-R') || run_(@command) or die "unable to apply patch " . $patch;
    }

    copy_files_to($live->{system}{files}, get_system_root($live));

    run_({ setarch => $live->{settings}{arch} },
	 "chroot", get_system_root($live), "bash", "-c", $live->{system}{postInstall}) if $live->{system}{postInstall};

    umask $previous_umask;
}

sub umount_external_filesystem {
    my ($live) = @_;
    my $mnt = get_system_root($live) . "/mnt/";
    eval { fs::mount::umount("$mnt/$_") } foreach all($mnt);
}

sub create_loopback_files {
    my ($live) = @_;
    # umount filesystem in the live before creating the loopback
    umount_external_filesystem($live);
    mkdir_p(get_builddir($live) . $live->{prefix}{build}{loopbacks});
    foreach (grep { exists $loop{$_->{type}}{build} } @{$live->{mount}{dirs} || []}) {
        $loop{$_->{type}}{build}->($live, $_);
    }
}

#- 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, $opts) = @_;
    $opts->{boot} ? $storage{$opts->{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 eq 'ext2' ? () : $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) = @_;
    create_initrd($live);
    cp_f(get_system_root($live) . '/boot/vmlinuz-' . find_kernel($live), get_builddir($live) . $live->{prefix}{build}{boot} . '/vmlinuz');
    require bootsplash;
    my $theme = do {
        local $::prefix = get_system_root($live);
        bootsplash::themes_read_sysconfig('800x600');
    };
    my $bootlogo = get_system_root($live) . "/usr/share/gfxboot/themes/$theme->{name}/install/bootlogo";
    if (-f $bootlogo) {
        print "using $bootlogo as gfxboot splash image\n";
        cp_f($bootlogo, get_builddir($live) . $live->{prefix}{build}{boot} . '/bootlogo');
    } else {
        warn "unable to find gfxboot splash ($bootlogo)";
    }
    output(get_builddir($live) . $live->{prefix}{build}{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->{media}{title} <kernel options>

));

    foreach my $media ($live->{media}, @{$live->{extra_media}}) {
        foreach my $boot ('', @{$media->{extra_boot}}) {
            my $opts = { boot => $boot };
            output(get_builddir($live) . $live->{prefix}{build}{boot} . get_syslinux_path($live, $media, $opts),
                   build_syslinux_cfg($live, $media, $opts));
        }
    }
}

sub device_allocate_fs {
    my ($device, $size) = @_;
    run_('dd', "of=$device", 'count=0', 'bs=1', "seek=" . removeXiBSuffix($size));
}

#- format $device as type $type
sub device_mkfs {
    my ($device, $type) = @_;
    if ($type eq 'vfat') {
        run_('mkfs.vfat', $device);
    } elsif (member($type, 'ext2', 'ext3')) {
        run_("mkfs.$type", "-m", 0, if_(!-b $device, '-F'), $device);
    } else {
        die "unable to mkfs for unsupported media type $type";
    }
}

sub set_device_label {
    my ($device, $type, $label) = @_;
    if ($type eq 'vfat') {
        local $ENV{MTOOLS_SKIP_CHECK} = 1;
        run_('mlabel', '-i', $device, '::' . $label);
    } elsif (member($type, 'ext2', 'ext3')) {
        run_('e2label', $device, $label);
    } else {
        die "unable to set label for unsupported media type $type";
    }
}

sub get_cdrom_master_path {
    my ($live, $opts) = @_;
    get_builddir($live) . $live->{prefix}{build}{images} . '/' . ($opts->{boot} ? 'boot-' . $opts->{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 = get_cdrom_master_path($live, $opts);
        mkdir_p(dirname($dest));
    }
    run_('genisoimage', '-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',
         if_($dest, '-o', $dest),
         'isolinux/isolinux.bin=/usr/lib/syslinux/isolinux.bin',
         'isolinux/isolinux.cfg=' . get_builddir($live) . $live->{prefix}{build}{boot} . get_syslinux_path($live, $media, $opts),
         $live->{prefix}{media}{boot} . '=' . get_builddir($live) . $live->{prefix}{build}{boot},
         if_(!$opts->{boot_only}, $live->{prefix}{media}{loopbacks} . '=' . get_builddir($live) . $live->{prefix}{build}{loopbacks}),
     ) or die "unable to run genisoimage";
    run_('mkcd', '--addmd5', $dest) if $dest;
}

sub get_usb_master_path {
    my ($live) = @_;
    get_builddir($live) . $live->{prefix}{build}{images} . '/live.img';
}

sub create_usb_master {
    my ($live, $media, $opts) = @_;
    my $dest = get_usb_master_path($live);
    mkdir_p(dirname($dest));
    #- dumb guess, a bit too large, and valid for FAT only
    my $size = directory_usage(get_builddir($live) . $live->{prefix}{build}{loopbacks}) +
               directory_usage(get_builddir($live) . $live->{prefix}{build}{boot});
    device_allocate_fs($dest, $size);
    device_mkfs($dest, get_media_setting($media, 'fs'))
      or die "unable to format $dest";
    local $opts->{device} = $dest;
    record_usb_master($live, $media, $opts);
}

#- $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)) {
        $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_('wodim', '-v', 'dev=' . $media->{device}, "blank=fast");
}

sub format_usb_device {
    my ($_live, $media) = @_;
    maybe_umount_device($media->{device});
    device_mkfs($media->{device}, get_media_setting($media, 'fs'))
      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)) {
        $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_('wodim', '-v', 'dev=' . $media->{device}, $src);
}

sub record_usb_master {
    my ($live, $media, $opts) = @_;
    my $o_device = $opts->{device} || $media->{device};
    if (my $label = $o_device && get_media_label($media)) {
        set_device_label($o_device, get_media_setting($media, 'fs'), $label);
    }
    my $device = $opts->{device} || get_media_device($media)
      or die "unable to find recording device (missing label? try with --device <device>)";
    my $bootloader = get_media_setting($media, 'bootloader');
    member($bootloader, 'grub', 'syslinux') or die "no bootloader defined in media configuration";
    mkdir_p($live->{mnt});
    run_('mount', if_(-f $device, '-o', 'loop'), $device, $live->{mnt})
      or die "unable to mount $device";
    rm_rf($live->{mnt} . $live->{prefix}{media}{boot}) if -e $live->{mnt} . $live->{prefix}{media}{boot};
    cp_af(get_builddir($live) . $live->{prefix}{build}{boot}, $live->{mnt} . $live->{prefix}{media}{boot});

    my $grub_device_map = $live->{mnt} . $live->{prefix}{media}{boot} . "/grub/device.map";
    if ($bootloader eq 'syslinux') {
        cp_f(get_builddir($live) . $_, $live->{mnt}) foreach map {
            $live->{prefix}{boot} . $_;
        } get_syslinux_path($live, $media, $opts), get_initrd_path($live, $media), @syslinux_boot_files;
    } elsif ($bootloader eq 'grub') {
        #- FIXME: add get_grub_path (when building boot configuration files)
        #         and get_bootloader_path (when copying)
        mkdir_p($live->{mnt} . $live->{prefix}{media}{boot} . '/grub');
        cp_f(get_system_root($live) . $live->{prefix}{build}{boot} . '/grub/splash.xpm.gz', $live->{mnt} . $live->{prefix}{media}{boot} . '/grub');
        output_p($live->{mnt} . $live->{prefix}{media}{boot} . '/grub/menu.lst', build_grub_cfg($live, $media, $device));
        unlink $grub_device_map;
    }

    if (-b $device) {
        if ($bootloader eq 'syslinux') {
            #- use syslinux -s, "safe, slow and stupid" version of SYSLINUX, unless specified otherwise
            run_('syslinux', if_(!$media->{fast_syslinux}, '-s'), $device)
              or die "unable to run syslinux on $device";
        } elsif ($bootloader eq 'grub') {
            my $local_grub_install = get_builddir($live) . $live->{prefix}{build}{scripts} . '/grub-install';
            mkdir_p(dirname($local_grub_install));
            cp_f(get_system_root($live) . '/sbin/grub-install', $local_grub_install);
            my $root = get_system_root($live);
            substInFile {
                s!^\s*exec_prefix=.*!exec_prefix=$root!;
                s!^\s*grub_prefix=/boot/!grub_prefix=$live->{prefix}{media}{boot}/!;
                s!^\s*bootdir=(.*)/boot$!bootdir=$1$live->{prefix}{media}{boot}!;
            } $local_grub_install;

            my $master_device = $device;
            $master_device =~ s/(\d+)$//;
            foreach ($master_device, $device) {
                run_($local_grub_install, '--root-directory=' . $live->{mnt}, '--no-floppy', $_)
                  or die "unable to run grub on $device";
            }
            unlink $grub_device_map;
        }
    } else {
        warn "not running $bootloader on non block device $device";
    }

    do {
        my $loopbacks_source = get_builddir($live) . $live->{prefix}{build}{loopbacks} . '/';
        my $total = directory_usage($loopbacks_source);
        my $all_files = 0;
        my $current_file = 0;
        local $/ = "\r";
        run_foreach($live->{update_progress} ? sub {
                        if (/^\s*(\d+)\s+\d+%\s+/) {
                            $current_file = $1;
                            $live->{update_progress}->(undef, $all_files + $current_file, $total);
                        }
                        if (/(?:^|\n)\S+/) {
                            $all_files += $current_file;
                            $current_file = 0;
                        }
                    } : sub {},
                    'rsync', '-vdP', '--inplace', $loopbacks_source, $live->{mnt} . $live->{prefix}{media}{loopbacks})
          or die "unable to copy loopback files";

        copy_files_to($live->{media}{files}, $live->{mnt});
    } unless $opts->{boot_only};

    run_('umount', $live->{mnt});
    maybe_umount_device($device);

    if ($live->{system}{hide_media_dirs}) {
        run_('mattrib', '+a', '+r', '+s', '+h', '-/', '-i', $device, '::' . $live->{prefix}{media}{$_})
          foreach qw(boot loopbacks);
    }
}

#- $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)) {
        $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, 'record_needs_master') && 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) = @_;

    complete_config($live);
    $live->{system}{vga_mode} = 788 if !defined $live->{system}{vga_mode};
    my $live_media = $live->{prefix}{live}{mnt} . $live->{prefix}{media}{mnt};

    require interactive;
    require wizards;
    my $in = 'interactive'->vnew('su');
    my (@available_storage, @available_devices);
    my ($storage, $device, $format);
    my %source_types = (
        live => N("Use current live system"),
        file => N("Select a file"),
    );
    my ($source_type, $source_path);
    my $source_is_mounted = sub { -d ($live->{settings}{builddir} . $live->{prefix}{media}{boot}) };
    my $umount_source = sub {
        if ($source_type ne 'live' && $source_is_mounted->()) {
            run_('umount', $live->{settings}{builddir});
            rmdir($live->{settings}{builddir});
        }
    };
    my $w;
    $w = wizards->new({
        name => N("Live system copy wizard"),
        pages => {
            welcome => {
                name => N("Welcome to the live system copy wizard"),
                no_back => 1,
                next => 'source',
            },
            source => {
                name => N("Which live system do you want to copy?"),
                data => [ if_(-d ($live_media . $live->{prefix}{media}{loopbacks}),
                              { type => 'list', val => \$source_type,
                                list => sort(keys(%source_types)),
                                format => sub { $source_types{$_[0]} } }),
                          { type => 'file', val => \$source_path,
                            disabled => sub { $source_type eq 'live' } } ],
                pre => $umount_source,
                complete => sub {
                    if ($source_type eq 'live') {
                        $live->{settings}{builddir} = $live_media;
                    } else {
                        require File::Temp;
                        $live->{settings}{builddir} = File::Temp::tempdir();
                        if (!run_('mount', '-o', 'loop', $source_path, $live->{settings}{builddir})) {
                            $in->ask_warn(N("Error"), N("Unable to use selected file"));
                            return 1;
                        }
                    }
                    0;
                },
                post => sub {
                    my $boot = $live->{settings}{builddir} . $live->{prefix}{media}{boot};
                    @available_storage = sort(grep { -d "$boot/$_" && exists $storage{$_}{detect} } all($boot));
                    if (@available_storage == 1) {
                        $storage = $available_storage[0];
                        return 'device';
                    }
                    return '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 $_wait, $live->{update_progress}) = $in->wait_message_with_progress_bar;
                    do {
                        local $::isInstall = 1; # quick hack to embed the wait message
                        $live->{update_progress}->(N("Copying in progress"));
                    };
                    eval {
                        my $opts = { media => { storage => $storage, device => '/dev/' . $device->{device} } };
                        format_device($live, $opts) if $format;
                        record_onthefly($live, $opts);
                    };
                    delete $live->{update_progress};
                    if (my $error = $@) {
                        $in->ask_warn(N("Error"), $error);
                        $w->{pages}{device}{end} = 1;
                    }
                    return "end";
                },
            },
            end => {
                name => N("Congratulations") . "\n\n" . N("Your live system is now copied."),
                no_back => 1,
                end => 1,
            },
        }
    });
    $w->process($in);
    $umount_source->();
    $in->exit;
}

sub read_config {
    my ($live, $config_path, $settings_path) = @_;

    add2hash($live->{settings} ||= {}, { getVarsFromSh($settings_path) }) if $settings_path;
    if ($config_path) {
        #- don't use do(), since it can't see lexicals in the enclosing scope
        my $cfg = eval(cat_($config_path)) or die "unable to load $config_path: $@";
        put_in_hash($live, $cfg);
        print "loaded $config_path as config file\n";
    }
}

sub check_config {
    my ($live) = @_;
    unless (keys(%$live)) {
        warn 'no live definition';
        Pod::Usage::pod2usage();
    }
    #- check for minimum requirements
    ref $live->{media} && $live->{media}{storage} or die "no media storage definition";
    ref $live->{system} or die "no system definition";
}

sub complete_config {
    my ($live) = @_;

    my $default_prefix = {
        build => {
            boot => '/boot',
            images => '/images',
            initrd => '/initrd',
            loopbacks => '/loopbacks',
            scripts => '/scripts',
        },
        media => {
            boot => $live->{system}{hide_media_dirs} ? '/.boot' : '/boot',
            loopbacks => $live->{system}{hide_media_dirs} ? '/.loopbacks' : '/loopbacks',
            mnt => '/media',
        },
        live => {
            mnt => '/live',
        },
    };

    #- set unsupplied config dirs
    add2hash($live->{prefix}{$_} ||= {}, $default_prefix->{$_}) foreach keys %$default_prefix;

    $live->{settings}{builddir} ||= '/tmp/draklive';
    mkdir_p(get_builddir($live));

    $live->{mnt} ||= get_builddir($live) . "/mnt";

    $live->{settings}{arch} ||= `rpm --eval '%{_target_cpu}'`;
    $live->{media}{title} ||= "live";
}

sub dump_config {
    my ($live) = @_;
    use Data::Dumper;
    print Data::Dumper->Dump([ $live ], [ "live" ]);
}

sub clean {
    my ($live) = @_;
    # umount filesystem in the live before cleaning
    umount_external_filesystem($live);
    rm_rf($_) foreach grep { -e $_ } get_builddir($live), get_system_root($live);
}

my @actions = (
    { name => 'dump-config', do => \&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 },
    { 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);
my $config_path = 'config/live.cfg';
my $settings_path = 'config/settings.cfg';
GetOptions(
    "help" => sub { Pod::Usage::pod2usage('-verbose' => 1) },
    "copy-wizard" => \$live{copy_wizard},
    "boot-only" => \$opts{boot_only},
    "boot-image=s" => sub { $opts{boot} = $_[1]; $opts{boot_only} = 1 },
    "all" => sub { $_->{to_run} = 1 foreach grep { member($_->{name}, @all) } @actions },
    (map { $_->{name} => \$_->{to_run} } @actions),
    "device=s" => sub { $live{media}{device} = $_[1] },
    "all-regions" => sub { $live{all_regions} = 1 },
    "config=s" => \$config_path,
    "settings=s" => \$settings_path,
    "define=s" => \%{$live{settings}},
) or Pod::Usage::pod2usage();

require standalone;
if ($live{copy_wizard}) {
    copy_wizard(\%live);
} else {
    every { !$_->{to_run} } @actions and die 'nothing to do';
    read_config(\%live, $config_path, $settings_path);
    check_config(\%live);
    complete_config(\%live);
    foreach my $region ($live{all_regions} ? sort(keys %{$live{regions}}) : $live{settings}{region}) {
        $region and print qq(=== proceeding with region "$region"\n);
        $live{settings}{region} = $region;
        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 <dev>    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 <file>   use this configuration file as live description
                     defaults to "config/live.cfg"

   --settings <file> 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

   --copy-wizard     run the copy wizard

   --boot-only       copy only boot files
                     (affects master/record steps)

   --boot-image <method>
                     create a boot image for the selected method
                     (affects master/record steps, implies --boot-only)

Examples:

 draklive --clean

 draklive --all

 draklive --record --device /dev/sdb1

 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,
    },
    media => {
        storage => 'cdrom',
    },
    extra_media => [
        {
                storage => 'usb',
        },
    ],
    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<draklive> builds a live distribution according to a
configuration file, creates a master image,
and optionnally installs it on a device.

See L<http://qa.mandriva.com/twiki/bin/view/Main/DrakLive>

=head1 AUTHOR

Olivier Blin <oblin@mandriva.com>

=cut
