#!/usr/bin/perl

# draklive $Id$

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

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,
            ],
        },
    },
);

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 => 'disk/cdrom|hardware_raid|sata|scsi bus/usb disk/raw',
        fs => 'iso9660',
        source => 'LABEL=MDVCDROOT',
        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 { 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_ {
    print STDERR "running " . join(' ', @_) . "\n";
    run_program::run(@_);
}

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

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

sub create_initrd {
    my ($live) = @_;
    foreach ($live->{media}, @{$live->{extra_media}}) {
        create_initrd_for_media($live, $_);
    }
    $live->{copy_initrd} and cp_f($live->{workdir} . get_initrd_path($live, $live->{media}), $live->{copy_initrd});
}

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

    my $initrd_tree = $live->{workdir} . $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($live->{system}{root} . '/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($live->{system}{root}, "/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', $live->{system}{root} . $moddeps{$_}{full})
      foreach @module_deps, @$modules;

    @$skipped and warn "skipped modules:" . join("\n", '', sort(@$skipped));

    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($live->{workdir} . $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 = $live->{workdir} . $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 = $live->{workdir} . 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 = $live->{workdir} . get_initrd_path($live, $media);
	my $tmp_initrd = '/tmp/initrd.gz';
        cp_f($initrd, $live->{system}{root} . $tmp_initrd);
        {
            local $::prefix = $live->{system}{root};
            bootloader::add_boot_splash($tmp_initrd, $live->{system}{vga_mode});
        }
        cp_f($live->{system}{root} . $tmp_initrd, $initrd);
	unlink($live->{system}{root} . $tmp_initrd);
    }
}

sub build_syslinux_cfg {
    my ($live, $media) = @_;
    #- fastboot is needed to avoid fsck
    my $append = "fastboot splash=silent vga=$live->{system}{vga_mode}";
    my $to_root = get_media_fs_module($media) 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_('drakx-in-chroot',
         $live->{system}{repository},
         $live->{system}{root},
         if_($live->{system}{auto_install}, '--auto_install', abs_path($live->{system}{auto_install})),
         if_($live->{system}{patch_install}, '--defcfg', abs_path($live->{system}{patch_install})))
      or die "unable to install system chroot";
    #- remove urpmi media added by drakx-in-chroot, they're unusable
    run_('chroot', $live->{system}{root}, 'urpmi.removemedia', '-a');
    post_install_system($live);
}

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

    my @rpms = map { abs_path($_) } @{$live->{system}{rpms}};
    each_index { !defined $_ and die "unable to find file " . $live->{system}{rpms}[$::i] } @rpms;
    run_('urpmi', '--auto', '--root', $live->{system}{root}, @rpms) if @rpms;

    my @patches = map { 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', $live->{system}{root}, '-r', '/tmp', '-i', $_) foreach @patches;

    #- make sure harddrake is run
    #- (do it in chroot, or else Storable from the build box may write an incompatible config file)
    system("chroot $live->{system}{root} " .
           "perl -MStorable -e \"Storable::store({ UNKNOWN => {} }, '/etc/sysconfig/harddrake2/previous_hw')\"");

    #- 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->{system}{root} . '/etc/sysconfig/init';

    #- disable first boot wizard
    output($live->{system}{root} . '/etc/sysconfig/firstboot', 'FIRSTBOOT=no');
    #- enable drakx-finish-install
    output($live->{system}{root} . '/etc/sysconfig/finish-install', qq(
FINISH_INSTALL=yes
NETWORK=no
AUTHENTICATION=no
USERS=no
));

    #- preselect guest user in kdm
    my $kdm_cfg = '/usr/share/config/kdm/kdmrc';
    update_gnomekderc($live->{system}{root} . $kdm_cfg,
                      'X-:0-Greeter' => (PreselectUser => 'Default', DefaultUser => 'guest')) if -f $kdm_cfg;
}

sub create_loopback_files {
    my ($live) = @_;
    mkdir_p($live->{workdir} . $live->{prefix}{loopbacks});
    foreach (grep { $_->{build_from} } @{$live->{mount}{dirs} || []}) {
        my $tree = $live->{system}{root} . $_->{build_from};
        my $dest = $live->{workdir} . $live->{prefix}{loopbacks} . '/' . $_->{source};
        unlink($dest);
        $loop{$_->{type}}{build}->($dest, $tree);
    }
    foreach (grep { $_->{pre_allocate} } @{$live->{mount}{dirs} || []}) {
        my $dest = $live->{workdir} . $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 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) = @_;
    create_initrd($live);
    cp_f($live->{system}{root} . '/boot/vmlinuz-' . $live->{system}{kernel}, $live->{workdir} . $live->{prefix}{boot} . '/vmlinuz');
    my $msg = $live->{system}{root} . '/boot/message-graphic';
    cp_f($msg, $live->{workdir} . $live->{prefix}{boot} . '/live.msg') if -f $msg;
    output($live->{workdir} . $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 <kernel options>

));
    foreach ($live->{media}, @{$live->{extra_media}}) {
        output($live->{workdir} . get_syslinux_path($live, $_), build_syslinux_cfg($live, $_));
    }
}

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', $live->{workdir} . $live->{prefix}{images} . '/live.iso');
        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=' . $live->{workdir} . get_syslinux_path($live, $media),
         $live->{prefix}{boot} . '=' . $live->{workdir} . $live->{prefix}{boot},
         $live->{prefix}{loopbacks} . '=' . $live->{workdir} . $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_storage_setting($media, 'create')) {
        $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_storage_setting($media, 'format')) {
        $format->($live, $media);
    } else {
        warn "not implemented yet";
    }
}

sub record_cdrom_master {
    my ($live, $media, $opts) = @_;
    $opts->{refresh_boot_only} and die "record boot isn't possible for cdrom master";
    $media->{device} or die "no device defined in media configuration";
    my $src = $opts->{onthefly} ? '-' : $live->{workdir} . $live->{prefix}{images} . '/live.iso';
    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 <device>)";
    mkdir_p($live->{mnt});
    run_('mount', $device, $live->{mnt})
      or die "unable to mount $device";
    cp_af($live->{workdir} . $live->{prefix}{boot}, $live->{mnt});
    cp_f($live->{workdir} . get_syslinux_path($live, $media), $live->{mnt});
    cp_f($live->{workdir} . $_, $live->{mnt}) foreach
      get_initrd_path($live, $media), map { $live->{prefix}{boot} . '/' . $_ } qw(vmlinuz live.msg);
    run_('rsync', '-vdP', $live->{workdir} . $live->{prefix}{loopbacks} . '/', $live->{mnt} . $live->{prefix}{loopbacks})
      unless $opts->{refresh_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_storage_setting($media, 'record')) {
        $record->($live, $media, $opts);
    } else {
        warn "not implemented yet";
    }
}

sub record_boot {
    my ($live) = @_;
    record_master($live, { refresh_boot_only => 1 });
}

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) = @_;
    #- assumes the current live media is mounted there
    $live->{workdir} = $live->{prefix}{live_mnt} . $live->{prefix}{media_mnt};
    $live->{mnt} = '/tmp/mnt';
    my $root = $live->{workdir} . $live->{prefix}{boot};
    my @available_storage = grep { -d "$root/$_" && exists $storage{$_}{detect} } all($root);
    my @available_devices;
    require interactive;
    require wizards;
    my $in = 'interactive'->vnew;
    my $w = wizards->new;
    my ($storage, $device, $format);
    $w->process({
        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,
            },
        },
    }, $in);
}

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

    #- set unsupplied config dirs
    $live->{workdir} ||= '/tmp/draklive';
    $live->{mnt} ||= $live->{workdir} . "/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($live->{workdir});
}

sub clean {
    my ($live) = @_;
    rm_rf($_) foreach grep { -e $_ } $live->{workdir}, $live->{system}{root};
}

my $default_prefix = {
    media_mnt => '/media',
    live_mnt => '/live',
    loopbacks => '/loopbacks',
    images => '/images',
    boot => '/boot',
    initrd => '/initrd',
};

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_boot', do => \&record_boot },
    { name => 'record_onthefly', do => \&record_onthefly },
);
my @all = qw(install boot loop master);

my (%live, $copy_wizard);
GetOptions(
    "help" => sub { pod2usage('-verbose' => 1) },
    "copy_wizard" => \$copy_wizard,
    "all" => sub { $_->{to_run} = 1 foreach grep { member($_->{name}, @all) } @actions },
    (map { $_->{name} => \$_->{to_run} } @actions),
    "device:s" => sub { $live{media}{device} = $_[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 pod2usage();

unless ($copy_wizard) {
    unless (keys(%live)) {
        warn 'no live definition';
        pod2usage();
    }
    complete_config(\%live);
}
add2hash($live{prefix} ||= {}, $default_prefix);

require standalone;

if ($copy_wizard) {
    copy_wizard(\%live);
} else {
    every { !$_->{to_run} } @actions and die 'nothing to do';
    foreach (grep { $_->{to_run} } @actions) {
        print qq(* entering step "$_->{name}"\n);
        $_->{do}->(\%live);
        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_boot     record bootloader only 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

   --copy_wizard    run the copy wizard

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',
        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<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