#! /usr/bin/perl

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#
# Create SUSE Linux boot disks.
#
# Original from OpenSuSE 10.2 boot image boot/i386/mkbootdisk
#
# 12th Dec 2009	Jari Turkia <jmjt@lut.fi> Fixes for OpenSuSE 11.2
# 18th Jan 2007	Jari Turkia <jmjt@lut.fi> First version with FAT32-support
# For further info, see:  http://en.opensuse.org/SuSE_install_from_USB_drive
#
# Try 'mkbootdisk --help' for a usage summary.
#
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

use strict 'vars';
use integer;

%::ConfigData = ( full_product_name => "openSUSE 11.2" );


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#
# Parse command line and do something.
#
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

use Getopt::Long;

sub cleanup;
sub dir_sort_func;
sub dir_sort;
sub help;
sub unpack_bootlogo;

my $opt_file = "./bootdisk";
my $opt_verbose = 0;
my $opt_src = undef;
my $opt_64 = 0;
my $opt_96 = 0;
my $opt_keep = undef;
my $opt_syslinux = -x "/usr/bin/mcopy" ? "/usr/bin/syslinux" : "/usr/bin/syslinux-nomtools";
$opt_syslinux = "/usr/bin/syslinux-nomtools" if -x "/usr/bin/syslinux-nomtools";
my $opt_disk = undef;
my $opt_backup_mbr = undef;
my $opt_disk_in = undef;

my ($boot_disks, $tmp_dir, $buf, $i);

END { cleanup }
$SIG{INT} = \&cleanup;
$SIG{TERM} = \&cleanup;

$ENV{PATH} = "/bin:/usr/bin:/sbin:/usr/sbin";


chomp (my $arch = `uname -m`);
$opt_64 = 1 if $arch eq 'x86_64';

GetOptions(
  'help|h'       => \&help,
  'verbose|v'    => \$opt_verbose,
  'out|o=s'      => \$opt_file,
  '96'           => sub { $opt_64 = 0; $opt_96 = 1 },
  '64'           => sub { $opt_64 = 1; $opt_96 = 0 },
  '32'           => sub { $opt_64 = 0; $opt_96 = 0 },
  'keep'         => \$opt_keep,
  'syslinux=s'   => \$opt_syslinux,
  'partition=s'  => \$opt_disk,
  'backup-mbr=s' => \$opt_backup_mbr,
);

$opt_src = shift;

help unless $opt_src && -d($opt_src);

die "error: must be root to run this script\n" if $<;

die "error: $opt_syslinux not found\nPlease install package \"syslinux\" first.\n" unless -f($opt_syslinux) && -x($opt_syslinux);

chomp ($tmp_dir = `mktemp -d /tmp/mkbootdisk.XXXXXXXXXX`);
die "error: mktemp failed\n" if $?;

$arch = $opt_64 ? 'x86_64' : 'i386';

my $src = "$opt_src/boot/$arch/loader";
$opt_64 = $opt_96 = 0 if -f "$src/isolinux.cfg";
$src = "$opt_src/boot/loader" unless -f "$src/isolinux.cfg";
$src = "$opt_src/loader" unless -f "$src/isolinux.cfg";
$src = $opt_src unless -f "$src/isolinux.cfg";

die "$opt_src: no $arch installation source\n" unless -f "$src/isolinux.cfg";

mkdir "$tmp_dir/src", 0755;
mkdir "$tmp_dir/mp", 0755;
system "cp -a '$src'/* $tmp_dir/src" and die "error: failed to copy boot loader files\n";

$src = "$tmp_dir/src";

# delete unnecessary files
system "rm -f $tmp_dir/src/{*live*,directory.yast}";
# system "rm -f $tmp_dir/src/{06400480,16001200}.spl";
if(!$opt_96) {
  for my $f (<$src/*64>) {
    if($opt_64) {
      (my $s = $f) =~ s/64$//;
      rename $f, $s;
    }
    else {
      unlink $f;
    }
  }
}

# prepare syslinux config file

open F, "$src/isolinux.cfg"; my @cfg = <F>; close F;
open F, ">$src/syslinux.cfg"; print F @cfg; close F,
unlink "$src/isolinux.cfg";
unlink "$src/isolinux.bin";

system "cp $opt_syslinux $src/ldlinux.sys" and die "error: no syslinux?\n";

$boot_disks = 2;

unlink "$src/ldlinux.sys";

my $mp;

  # make (usb) disk bootable

  # mbr taken from makebootfat package
  my $new_mbr =
    "\xeb\x58\x90\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" .
    "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" .
    "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" .
    "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" .
    "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" .
    "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfa\x31\xc0\x8e\xd8\x8e" .
    "\xc0\x8e\xd0\xbc\x00\x7c\xfb\xfc\x89\xe6\xbf\x00\x06\xb9\x00\x01" .
    "\xf3\xa5\xea\x77\x06\x00\x00\x88\x16\x00\x08\xbe\x9b\x07\xf6\xc2" .
    "\x80\x74\x03\xbe\x9f\x07\xe8\xc7\x00\xb4\x08\xcd\x13\x31\xc0\x88" .
    "\xf0\x40\xa3\x74\x07\x80\xe1\x3f\x88\x0e\x76\x07\xbe\xbe\x07\x31" .
    "\xc0\xb9\x04\x00\xf6\x04\x80\x74\x03\x40\x89\xf7\x83\xc6\x10\xe2" .
    "\xf3\x83\xf8\x01\x74\x03\xe9\x88\x00\x8a\x16\x00\x08\xb8\x00\x41" .
    "\xbb\xaa\x55\x31\xc9\x30\xf6\xf9\xcd\x13\x72\x2e\x81\xfb\x55\xaa" .
    "\x75\x28\xf6\xc1\x01\x74\x23\xbe\xa3\x07\xe8\x73\x00\x57\xbe\x64" .
    "\x07\x8b\x5d\x08\x89\x5c\x08\x8b\x5d\x0a\x89\x5c\x0a\x8a\x16\x00" .
    "\x08\x8c\xd8\x8e\xc0\xb8\x00\x42\xeb\x34\xbe\xa9\x07\xe8\x50\x00" .
    "\x57\x8b\x45\x08\x8b\x55\x0a\xf7\x36\x76\x07\x42\x89\xd1\x31\xd2" .
    "\xf7\x36\x74\x07\x88\xc5\xd1\xe8\xd1\xe8\x24\xc0\x08\xc1\x88\xd6" .
    "\x8a\x16\x00\x08\x8c\xd8\x8e\xc0\xbb\x00\x7c\xb8\x01\x02\xcd\x13" .
    "\x72\x16\x5e\x81\x3e\xfe\x7d\x55\xaa\x75\x08\xfa\xea\x00\x7c\x00" .
    "\x00\x77\x05\xbe\x78\x07\xeb\x03\xbe\x8e\x07\xe8\x02\x00\xeb\xfe" .
    "\xac\x20\xc0\x74\x0c\xb4\x0e\x8a\x3e\x62\x04\xb3\x07\xcd\x10\xeb" .
    "\xef\xc3\x00\x00\x10\x00\x01\x00\x00\x7c\x00\x00\x00\x00\x00\x00" .
    "\x00\x00\x00\x00\x00\x00\x00\x00\x4e\x6f\x20\x6f\x70\x65\x72\x61" .
    "\x74\x69\x6e\x67\x20\x73\x79\x73\x74\x65\x6d\x0d\x0a\x00\x44\x69" .
    "\x73\x6b\x20\x65\x72\x72\x6f\x72\x0d\x0a\x00\x46\x44\x44\x00\x48" .
    "\x44\x44\x00\x20\x45\x42\x49\x4f\x53\x0d\x0a\x00";

  my $part = $opt_disk;

  $opt_disk =~ s/(\d+)$//;
  my $pn = $1;

  die "not a partition: $opt_disk\n" unless $pn ne "";

  $opt_disk =~ s/(?<=\d)p$//;

  print "disk $opt_disk, partition $part\n";

  die "sorry, must be a primary partition (number 1 - 4)\n" if $pn < 1 || $pn > 4;

  my ($bpc, $fatsize);

  for (`fsck.vfat -n -v $part 2>/dev/null`) {
    if(/(\d+)\s+bytes\s+per\s+cluster/) {
      $bpc = $1;
      next;
    }
    if(/FATs,\s+(\d+)\s+bit\s+entries/) {
      $fatsize = $1;
      next;
    }
  }

  die "not a FAT file system\n" unless $bpc >= 512 && $fatsize >= 12;

  die "must be 32-bit FAT\n" unless $fatsize == 32;

  system "$opt_syslinux $part" and die "error: syslinux failed\n";
  # Check to see if our destination partition has been set as bootable
  my $bootable_part = `fdisk -l $opt_disk` or die "error: partition read failed\n";
  if ($bootable_part !~ /$part\s+\*\s/s) {
    # Go set it bootable using fdisk
    open (FDISK, "| fdisk $opt_disk > /dev/null") or die "error: fdisk failed\n";
    print FDISK "a\n" or die "error: activate failed\n";
    print FDISK $pn . "\n" or die "error: activate failed\n";
    print FDISK "w\n" or die "error: activate failed\n";
    close(FDISK);
  }

  # Check to see if our destination drive is already mounted
  open (MOUNTS, "/proc/mounts") or die "error: cannot read mounts\n";
  while (<MOUNTS>) {
    next if (!m:^$part\s+(\S+):);

    # Yep, the destination drive is mounted.
    # Weed out any possible escaped characters.
    my $mounted = $1;
    while ($mounted =~ /\\(\d+)/) {
      my $oct = $1;
      my $char = chr(oct($1));
      $mounted =~ s/\\$oct/$char/g;
    }
    # Unmount!
    system ("umount", $mounted) and die "error: unmounting destination drive failed\n";
  }
  close (MOUNTS);

  system "mount -tmsdos $part $tmp_dir/mp" and die "error: mount failed\n";
  $mp = "$tmp_dir/mp";

  for (dir_sort $src) {
    if($_ ne 'bootlogo') {
      system "cp -r $src/$_ $mp" and die "error: copy failed\n";
    }
    else {
      for my $i (unpack_bootlogo $src) {
        system "cp -r $src/$i $mp" and die "error: copy failed\n";
      }
    }
  }

  system "umount $mp" and die "error: umount failed\n";
  undef $mp;

  if($opt_backup_mbr) {
    my $backup;
    open F, "$opt_disk";
    sysread F, $backup, 0x200;
    close F;

    die "mbr backup failed\n" unless length($backup) == 0x200;

    open W, ">$opt_backup_mbr" or die "$opt_backup_mbr: $!\n";
    die "mbr backup failed\n" unless syswrite(W, $backup) == 0x200;
    close W;
  }

  open W, ">$opt_disk" or die "$opt_disk: $!\n";
  die "writing mbr failed\n" unless syswrite(W, $new_mbr) == length($new_mbr);
  close W;

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#
# Unmount image and remove temorary files.
#
sub cleanup
{
  system "umount $mp" if $mp;
  undef $mp;
  system "rm -r $tmp_dir" if ! $opt_keep && -d "$tmp_dir";
  undef $tmp_dir;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#
# Sorting function to ensure files are written in the correct order.
#
sub dir_sort_func
{
  my ($wa, $wb, $i, $p, $r);

  $p = 2;
  for $i qw ( .*\.spl initrd64 linux64 initrd linux memtest bootlogo message .*\.cfg ) {
    if($i eq 'bootlogo') {
      $r = $p;
      $p <<= 1;
    }
    $wa = $p if $a =~ /^$i$/;
    $wb = $p if $b =~ /^$i$/;
    $p <<= 1;
  }

  $wa = $r unless $wa;
  $wb = $r unless $wb;

  return $wb - $wa + ($a cmp $b);
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#
# Sort directory.
#
sub dir_sort
{
  my ($i, $size, @dir);

  opendir D, shift;
  @dir = grep { !/^\./ } readdir D;
  closedir D;

  return ( sort dir_sort_func @dir );
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub help
{
  (my $p = $0) =~ s:.*/::;

  print STDERR
  "Usage: $p [options] cd_mount_point\n" .
  "Create boot disk images from SUSE Linux DVD or CD1 or make (USB) disk bootable.\n" .
  "Options:\n" .
  "  --out file\t\twrite disks as fileN (default: bootdisk)\n" .
  "  --32\t\t\tcreate boot disks for 32 bit arch\n" .
  "  --64\t\t\tcreate boot disks for 64 bit arch\n" .
  "  --partition device\tmake this disk with this partition bootable\n" .
  "  --backup-mbr file\tsave mbr to file\n" .
  "Examples:\n" .
  "  $p /media/cdrom\n" .
  "  - write boot disks as bootdisk1 ... bootdiskN (N is approx. 8)\n" .
  "  $p --64 --out foo /media/cdrom\n" .
  "  - write 64 bit boot disks as foo1 ... fooN\n" .
  "  $p --partition /dev/sdb1 --backup-mbr mbr_old /media/cdrom\n" .
  "  - copy install files to /dev/sdb1 and write new mbr to /dev/sdb\n";

  exit 0;
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub unpack_bootlogo
{
  my ($dir, $tmp, $files, @files, @ext);
  local $_;

  $dir = shift;
  $tmp = "$dir/bootlogo.unpacked";

  mkdir "$tmp", 0755;

  @files = `cpio --quiet -t <$dir/bootlogo`;

  system "cd $tmp; cpio --quiet -i <../bootlogo";

  for (@files) {
    chomp;
    if(-k("$tmp/$_") && ! -l("$tmp/$_")) {
      push @ext, $_;
      undef $_;
    }
  }

  open P, "| cd $tmp; cpio --quiet -o >../bootlogo";
  print P "$_\n" for grep $_, @files;
  close P;

  system "mv $tmp/$_ $dir" for @ext;

  return ( 'bootlogo', @ext );
}

