#
# Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies).
# All rights reserved.
# This component and the accompanying materials are made available
# under the terms of the License "Eclipse Public License v1.0"
# which accompanies this distribution, and is available
# at the URL "http://www.eclipse.org/legal/epl-v10.html".
#
# Initial Contributors:
# Nokia Corporation - initial contribution.
#
# Contributors:
#
# Description: iMaker main Perl script & common routines
#



#
$(error |MAKE=$(MAKE)|MAKE_VERSION=$(MAKE_VERSION)|SHELL=$(SHELL)|MAKECMDGOALS=$(MAKECMDGOALS)|)
#
#!perl
#line 24

use subs qw(CORE::GLOBAL::die);

use strict;
use warnings;
use Cwd;
use Digest::MD5 qw(md5_hex);
use File::Basename;
use File::Copy;
use File::Find;
use File::Path;
use File::Spec;
use File::Temp qw(tempfile);
use POSIX qw(strftime);
use Text::ParseWords;
use Time::Local;

sub InitMkglobals();
sub PrintEnv($);
sub Max(@);
sub Min(@);
sub Trim($;$);
sub Quote($);
sub Unquote($);
sub Int2Hex($;$);
sub Byte2Str($@);
sub Str2Byte($);
sub Str2Xml($);
sub Ascii2Uni($);
sub Uni2Ascii($);
sub GetTimestamp();
sub Sec2Min($);
sub Wcard2Restr($);
sub Wcard2Regex($);
sub ParseCmdWords($);
sub DPrint($@);
sub Echo($$$);
sub PathConv($;$$$);
sub ParseFiles($);
sub GlobFiles($;$);
sub GetBasename($);
sub GetDirname($);
sub GetAbsDirname($;$$$);
sub GetAbsFname($;$$$);
sub GetRelFname($;$$);
sub GetWriteFname($);
sub GetFreeDrive(;$);
sub SubstDrive($$);
sub UnsubstDrive($);
sub Search($$$$$$\@\$);
sub Find($$$$$\$);
sub ChangeDir($);
sub DeleteDir($;$);
sub FindDir($$$$);
sub MakeDir($);
sub MakeChangeDir($);
sub SetWorkdir($);
sub OpenFile(*$$;$);
sub Test($);
sub CutFile($$$$$);
sub Copy($$;$);
sub CopyIby($$);
sub DeleteFile($;$);
sub FindFile($$$$);
sub HeadFile($$$);
sub TailFile($$$);
sub TypeFile($;$);
sub ReadFile($$);
sub WriteFile($$$;$$);
sub UnzipFile($$);
sub Zip($$$$@);
sub Move($$);
sub Touch($@);
sub SetLogfile($);
sub RunSystemCmd($;$$$);
sub ParseSystemCmd($$$$$);
sub GenExclfile($$$$$);
sub GenIbyfile($$$);
sub GenObyfile($$$$@);
sub GenMakefile($$$$$);
sub GenWidgetConf($$$$);
sub AddImageHeader($$$$$);
sub Sleep($);
sub FindSOSFiles($$$$);
sub CheckTool(@);
sub OpCacheInstall($$$);
sub SisInstall($$$$$$$$);
sub GetIPar(;$);
sub PEval($);
sub PeekICmd($);
sub SkipICmd();
sub GetICmd();
sub EndICmd();
sub SplitStep($);
sub RunStep($);
sub RunIExtCmd($);
sub GetConfmkList(;$);
sub GetFeatvarIncdir($);
sub SetVerbose($;$);
sub CloseLog();
sub RunIMakerCmd($$$$$@);
sub RunMakeCmd($$);
sub HandleCmdArg($);
sub HandleExtCmdArg($);
sub MenuRuncmd($);
sub Menu($);
sub Install($$$);

use constant READBUFSIZE => 2097152;  # 2 MB
use constant STARTSTR => '>>>[START]=========8<==========8<==========8<==========8<==========8<==========';
use constant ENDSTR   => '==========>8==========>8==========>8==========>8==========>8===========[END]<<<';

# device[VARID]==... !!
#
use constant BOOTBINARYSTATEMENT => qr/^\s*bootbinary\s*(?:=+|\s)\s*(?:"(.+?)"|(\S+))/i;

use constant FILESPECSTATEMENT =>
    qr/^\s*(?:data|device|dll|extension|file|primary|secondary|variant)\S*?\s*(?:=+|\s)\s*(?:"(.+?)"|(\S+))\s+(?:"(.+?)"|(\S+))(\s+.+?)?\s*$/i;

our ($gArgv, $gCmdcnt, @gCmdoutbuf, %gConfmkList, $gEpocdrive, $gEpocroot, $gError, $gErrwarn, $gEvalerr,
    %gExportvar, $gFiltercmd, @gFindresult, $gICmd, @gIcmd, $gImakerext, $gImgtype, $gKeepgoing, @gLogbuf,
    $gLogfile, %gLogfiles, $gMakecmd, @gMakeinfo, $gOutfilter, $gParamcnt, $gPrintcmd, @gReport, $gStartmk,
    $gStarttime, $gStep, @gStepDur, %gStepIcmd, %gSubstdrv, $gTgterr, %gTool, $gVerbose, $gWinOS, $gWorkdir,
    $gWorkdrive, @iVar);


###############################################################################
#

sub InitMkglobals()
{
    $gCmdcnt     = 0;
    @gCmdoutbuf  = ();
    $gFiltercmd  = qr/\S/;
    @gFindresult = ();
    $gICmd       = "";
    @gIcmd       = ();
    $gImgtype    = "";
    $gOutfilter  = "";
    $gParamcnt   = 0;
    $gPrintcmd   = 0;
    $gStep       = "";
    @gStepDur    = ();
    %gStepIcmd   = ();
    @iVar        = ();  # General purpose variable to be used from $(call peval,...)
}

BEGIN {
    ($gArgv, $gEvalerr, $gStarttime, $gWinOS) = (scalar(@ARGV), 0, time(), $^O =~ /MSWin/i);
    $_ = "default input and pattern-searching space";
    eval("use Archive::Zip qw(:ERROR_CODES)");
    eval("use constant AZ_OK => -1") if $@;
    eval("use Archive::Zip::Tree");
    if ($gWinOS) { eval("
        use Win32API::File qw(:DDD_);
        use Win32::File;
        use constant WIN32_FILE_HIDDEN => Win32::File::HIDDEN");
    } else { eval("
        use constant DDD_REMOVE_DEFINITION => -1;
        use constant WIN32_FILE_HIDDEN => -1");
    }
}

INIT {
    $gWorkdir   = Cwd::cwd();
    $gWorkdrive = ($gWorkdir =~ /^([a-z]:)/i ? uc($1) : "");
    $ENV{EPOCROOT} = ($gWinOS ? "\\" : "$gWorkdir/") if !$ENV{EPOCROOT};
    $ENV{IMAKER_CMDARG} = "" if !defined($ENV{IMAKER_CMDARG});
    $ENV{IMAKER_CYGWIN} = 0 if !$ENV{IMAKER_CYGWIN};

    InitMkglobals();
    %gConfmkList = ();
    $gEpocdrive  = ($ENV{EPOCROOT} =~ /^([a-z]:)/i ? uc($1) : $gWorkdrive);
    ($gEpocroot  = GetAbsDirname($ENV{EPOCROOT})) =~ s/\/+$//;
    $gError      = 0;
    $gErrwarn    = 0;
    %gExportvar  = (); $gExportvar{""} = 0;
    $gKeepgoing  = 0;
    @gLogbuf     = ();
    $gLogfile    = "";
    %gLogfiles   = ();
    $gMakecmd    = "";
    @gMakeinfo   = ("?", "?", "?");
    @gReport     = ();
    $gStartmk    = 0;
    %gSubstdrv   = ();
    $gTgterr     = 0;
    %gTool       = (); map{ $gTool{$_} => $_ } ("cpp", "elf2e32", "interpretsis", "opcache", "unzip");
    $gVerbose    = 1;

    select(STDERR); $|++;
    select(STDOUT); $|++;

    # Overload die
    *CORE::GLOBAL::die = sub {
        $gError = 1 if !$gEvalerr;
        return if (PeekICmd("iferror") && !$gEvalerr);
        CORE::die(@_) if ($gEvalerr || !$gKeepgoing);
        $gErrwarn = 1;
        warn(@_);
    };

    # Handler for __DIE__ signal
    $SIG{__DIE__} = sub {
        return if $gEvalerr;
        $gErrwarn = 1;
        warn(@_);
        exit(1);
    };

    # Handler for __WARN__ signal
    $SIG{__WARN__} = sub {
        if (($gEvalerr != 1) && ($gKeepgoing < 3) && ($_[0] ne "\n")) {
            select(STDERR);
            my $msg = ($gStep ? "($gStep): " : "") . $_[0];
            if ($gErrwarn && ($gKeepgoing < 2)) {
                   DPrint(0, "*** Error: $msg") }
            else { DPrint(127, "Warning: $msg") }
            select(STDOUT);
        }
        $gErrwarn = 0;
    };

    if (!$gArgv) {
        warn("iMaker is running under Cygwin!\n")
            if (!$ENV{IMAKER_CYGWIN} && $^O =~ /cygwin/i);
        my $perlver = sprintf("%vd", $^V);
        warn("iMaker uses Perl version $perlver! Recommended versions are 5.6.1, 5.8.x and 5.10.x.\n")
            if ($perlver !~ /^5\.(?:6\.1|(?:8|10)\.\d+)$/);
    }
}


###############################################################################
# Main program

{
    if ($gArgv) {
        my $iopt = shift(@ARGV);
        print(map("$_\n", GetFeatvarIncdir("@ARGV"))), exit(0) if ($iopt eq "--incdir");
        print(map("$_\n", @ARGV)), exit(0) if ($iopt eq "--splitarg");
        die("Unknown internal imaker.pl option: `$iopt'.\n");
    }

    delete($ENV{MAKE}) if $gWinOS;
    map { delete($ENV{$_}) } qw(MAKECMDGOALS MAKEFILES MAKEFLAGS MAKELEVEL MAKE_VERSION);

    $ENV{CONFIGROOT} = GetAbsDirname($ENV{CONFIGROOT} || "$gEpocroot/epoc32/rom/config");
    $ENV{ITOOL_DIR}  = GetAbsDirname($ENV{ITOOL_DIR}  || "$gEpocroot/epoc32/tools/rom");
    $ENV{IMAKER_DIR} = GetAbsDirname($ENV{IMAKER_DIR});

    $ENV{IMAKER_EXPORTMK}  = "";
    $ENV{IMAKER_MAKE}      = ($gWinOS ? "$ENV{IMAKER_DIR}/mingw_make.exe" : $ENV{MAKE} || "make") if !$ENV{IMAKER_MAKE};
    $ENV{IMAKER_MAKESHELL} = ($ENV{COMSPEC} || "cmd.exe") if (!$ENV{IMAKER_MAKESHELL} && $gWinOS);
    $ENV{IMAKER_MKCONF}    = $ENV{CONFIGROOT} . ',image_conf_(.+?)\.mk$,_(?:ncp)?\d+\.mk$,1' if !$ENV{IMAKER_MKCONF};

    my $pathsep = ($gWinOS ? ";" : ":");
    $ENV{PATH}  = join(";", grep(!/[\\\/]cygwin[\\\/]/i, split(/;+/, $ENV{PATH}))) if (!$ENV{IMAKER_CYGWIN} && $gWinOS);
    ($ENV{PATH} = Trim($ENV{PATH})) =~ s/"$/";/ if $gWinOS;  # http://savannah.gnu.org/bugs/index.php?25412
    $ENV{PATH}  = PathConv("$ENV{ITOOL_DIR}", $gWinOS) . $pathsep . PathConv("$gEpocroot/epoc32/tools", $gWinOS) .
        $pathsep . ($gWinOS ? PathConv("$gEpocroot/epoc32/gcc/bin", 1) . ";" : "") . $ENV{PATH};

    $ENV{PERL5LIB} = $ENV{IMAKER_DIR} . ($ENV{PERL5LIB} ? "$pathsep$ENV{PERL5LIB}" : "");

    die($@) if !defined($gImakerext = do("$ENV{IMAKER_DIR}/imaker_extension.pm")) && $@;

    my ($version, $verfile) = ("", "$ENV{IMAKER_DIR}/imaker_version.mk");
    open(FILE, "<$verfile") and map { $version = $1 if /^\s*IMAKER_VERSION\s*[+:?]?=\s*(.*?)\s*$/ } <FILE>;
    close(FILE);
    if ($version) { DPrint(1, "$version\n") }
    else { warn("Can't read iMaker version from `$verfile'.\n") }

    if ($ENV{IMAKER_CMDARG} =~ /^\s*--?(install|clean)=?(.*?)\s*$/i) {
        Install(lc($1) eq "clean", "$ENV{IMAKER_DIR}/../group/bld.inf", $2);
        exit(0);
    }

    $gMakecmd = "$ENV{IMAKER_MAKE} -R --no-print-directory" .
        ($ENV{IMAKER_MAKESHELL} ? " SHELL=\"$ENV{IMAKER_MAKESHELL}\"" : "");
    my $cmdout = qx($gMakecmd -f "$0" 2>&1);
    ($cmdout = (defined($cmdout) ? $cmdout : "")) =~ s/\n+$//;
    die("Can't run Make properly: `$cmdout'\n")
        if ($cmdout !~ /\|MAKE=(.*?)\|MAKE_VERSION=(.*?)\|SHELL=(.*?)\|/);
    @gMakeinfo = ($1, $2, $3);
    warn(($gMakeinfo[1] eq "" ? "Can't resolve Make version" : "iMaker uses Make version $gMakeinfo[1]") .
        ", recommended version is 3.81.\n") if ($gMakeinfo[1] !~ /^\s*3\.81/);

    RunIMakerCmd("$gMakecmd TIMESTAMP=" . GetTimestamp() .
        " -I \"$ENV{CONFIGROOT}\" -f \"$ENV{IMAKER_DIR}/imaker.mk\"", $ENV{IMAKER_CMDARG}, "", 0, 0, ());
}


###############################################################################
#

sub PrintEnv($)
{
    return if !@gMakeinfo;
    DPrint(shift(), "=" x 79 . "\n" .
        "User        : " . (getlogin() || "?") . "@" . ($ENV{HOSTNAME} || $ENV{COMPUTERNAME} || "?") . " on $^O\n" .
        "Time        : " . localtime() . "\n" .
        "Current dir : `$gWorkdir'\n" .
        "iMaker tool : `$ENV{IMAKER_TOOL}' -> `$0'\n" .
        "Cmdline args: `$ENV{IMAKER_CMDARG}'\n" .
        "Perl        : `$^X' version " . sprintf("%vd\n", $^V) .
        "PERL5LIB    : `$ENV{PERL5LIB}'\n" .
        "PERL5OPT    : `" . (defined($ENV{PERL5OPT}) ? "$ENV{PERL5OPT}'\n" : "'\n") .
        "Make        : `$gMakeinfo[0]' version $gMakeinfo[1]\n" .
        "Make shell  : `$gMakeinfo[2]'\n" .
        "EPOCROOT    : `$ENV{EPOCROOT}'\n" .
        "CONFIGROOT  : `$ENV{CONFIGROOT}'\n" .
        "PATH        : `$ENV{PATH}'\n");
    @gMakeinfo = ();
}

sub Max(@)
{
    my $max = (shift() || 0);
    map { $max = $_ if $_ > $max } @_;
    return($max);
}

sub Min(@)
{
    my $min = (shift() || 0);
    map { $min = $_ if $_ < $min } @_;
    return($min);
}

sub Trim($;$)
{
    (my $str = shift()) =~ s/^\s+|\s+$//g;
    $str =~ s/\s+(?=\s)//g if shift();
    return($str);
}

sub Quote($)
{
    local $_ = shift();
    return("") if !defined();
    s/\\( |n|t)/\\\\$1/g;
    return($_);
}

sub Unquote($)
{
    local $_ = shift();
    return("") if !defined();
    s/(?<!\\)(?<=\\n)\s+(\\n)?//g;
    s/(?<!\\)\s+(?=\\n)//g;
    s/(?<!\\)\\ / /g;
    s/(?<!\\)\\n/\n/g;
    s/(?<!\\)\\t/\t/g;
    s/\\\\( |n|t)/\\$1/g;
    s/\x00//g;
    return($_);
}

sub Int2Hex($;$)
{
    my ($int, $len) = @_;
    return((defined($len) ? $len : ($len = ($int < 4294967296 ? 8 : 16))) < 9 ? sprintf("%0${len}X", $int) :
        sprintf("%0" . ($len - 8) . "X%08X", int($int / 4294967296), $int % 4294967296));  # 4294967296 = 4 G
}

sub Byte2Str($@)
{
    my ($base, @byte) = @_;
    return(join("", map(($_ % 16 ? "" : sprintf("%04X:", $base + $_)) . sprintf(" %02X", $byte[$_]) .
        (!(($_ + 1) % 16) || ($_ == (@byte - 1)) ? "\n" : ""), (0 .. (@byte - 1)))));
}

sub Str2Byte($)
{
    my ($str, $ind, @byte) = (shift(), 0, ());
    $str =~ s/,$/, /;
    map {
        $ind++;
        s/^\s+|\s+$//g;
        if (/^\d+$/ && $_ < 256) {
            push(@byte, $_);
        } elsif (/^0x[0-9A-F]+$/i && hex() < 256) {
            push(@byte, hex());
        } else {
            die("Invalid $ind. byte: `$_'.\n");
            return;
        }
    } split(/,/, $str);
    return(@byte);
}

sub Str2Xml($)
{
    my $str = shift();
    $str =~ s/(.)/{'"'=>'&quot;', '&'=>'&amp;', "'"=>'&apos;', '<'=>'&lt;', '>'=>'&gt;'}->{$1} || $1/ge;
    return($str);
}

sub Ascii2Uni($)
{
    (local $_ = shift()) =~ s/(?<!\r)\n/\r\n/g;  # Use CR+LF newlines
    s/(.)/$1\x00/gs;
    return("\xFF\xFE$_");
}

sub Uni2Ascii($)
{
    (local $_ = shift()) =~ s/(.)\x00/$1/gs;
    s/\r\n/\n/g;
    return(substr($_, 2));
}

sub GetTimestamp()
{
    my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = localtime();
    return(sprintf("%04d%02d%02d%02d%02d%02d%02d",
        $year + 1900, $mon + 1, $mday, $hour, $min, $sec, int(($yday + ($year == 109 ? 3 : -3)) / 7) + 1));
}

sub Sec2Min($)
{
    my $sec = shift();
    return(sprintf("%02d:%02d", $sec / 60, $sec % 60));
}

sub Wcard2Restr($)
{
    (my $wcard = shift()) =~ s/(.)/{"*"=>".*", "?"=>"."}->{$1} || "\Q$1\E"/ge;
    return($wcard);
}

sub Wcard2Regex($)
{
    my $restr = Wcard2Restr(shift());
    return(qr/$restr/i);
}

sub ParseCmdWords($)
{
    my $line = Trim(shift());
    $line =~ s/\\/\\\\/g if $gWinOS;
    return(Text::ParseWords::parse_line('\s+', 0, $line));
}


###############################################################################
#

sub DPrint($@)
{
    my ($verbose, @outlist) = @_;
    map { tr/\x00\x1F/#/ } @outlist;
    print(@outlist) if !$verbose || ($verbose & $gVerbose);
    push(@gLogbuf, @outlist) if ($verbose < 32) || ($verbose & $gVerbose);
    return if ($gLogfile eq "" || !@gLogbuf);
    print(LOG @gLogbuf);
    @gLogbuf = ();
}

sub Echo($$$)
{
    return if SkipICmd();
    my ($verbose, $str) = (shift(), shift());
    DPrint($verbose, shift() ? "$str\n" : Unquote($str));
}


###############################################################################
# File operations

sub PathConv($;$$$)
{
    my $path = shift();
    if (shift()) { $path =~ tr-\/-\\- }
    else { $path =~ tr-\\-\/- }
    return($path) if (!$gWinOS || $path =~ /^(?:\/\/|\\\\)/);
    my $drive = shift();
    return(ucfirst(($path =~ /^[a-z]:/i ? "" : ($_[0] ? $_[0] : $gWorkdrive)) . $path))
        if !$drive;
    $drive = $gWorkdrive if !($drive = shift());
    $path =~ s/^$drive//i;
    return($path);
}

sub ParseFiles($)
{
    my ($file, @files) = (" " . shift() . " ", ());
    push(@files, defined($1) ? $1 : (defined($2) ? $2 : ())) while ($file =~ /\s(?:"\s*"|"+(.+?)"+|((\\\s|\S)+))(?=\s)/g);
    return(@files);
}

sub GlobFiles($;$)
{
    return(@gFindresult) if (my $file = shift()) =~ /^__find__$/i;
    return(map(/[\*\?]/ ? sort({lc($a) cmp lc($b)} grep(!/[\/\\]\.\.?$/,
        glob(scalar(s/\*/\{\.\*\,\*\}/g, /\s/) ? "\"$_\"" : $_))) : $_, (shift() ? $file : ParseFiles($file))));
}

sub GetBasename($)
{
    return((File::Basename::fileparse(shift()))[0]);
}

sub GetDirname($)
{
    (my $dir = shift()) =~ s/^>>?(?!>)//;
    return((File::Basename::fileparse($dir))[1]);
}

sub GetAbsDirname($;$$$)
{
    (my $dir = shift()) =~ s/^>>?(?!>)//;
    $dir = "." if ($dir eq "");
    my $absdir = "";
    eval { local $gEvalerr = 1; $absdir = Cwd::abs_path($dir) };
    return(PathConv($absdir || File::Spec->rel2abs($dir,
        $dir !~ /^$gWorkdrive/i && $dir =~ /^([a-z]:)/i ? "$1/" : ""), shift(), shift(), shift()));
}

sub GetAbsFname($;$$$)
{
    my $file = shift();
    return($file) if ($file eq "" || $file =~ /STD(IN|OUT|ERR)$/);
    my $append = ($file =~ s/^>>(?!>)// ? ">>" : "");
    return($append . PathConv(File::Spec->catpath("", GetAbsDirname(GetDirname($file)), GetBasename($file)), shift(), shift(), shift()));
}

sub GetRelFname($;$$)
{
    my ($file, $base) = (shift(), shift());
    my $append = ($file =~ s/^>>(?!>)// ? ">>" : "");
    ($file = PathConv(File::Spec->abs2rel($file, GetAbsDirname(defined($base) && ($base ne "") ? $base : ".")),
        shift(), 1, "[a-z]:")) =~ s/^[\/\\]+//;
    return("$append$file");
}

sub GetWriteFname($)
{
    (my $file = shift()) =~ s/^>?/>/;
    return($file);
}

sub GetFreeDrive(;$)
{
    my $drives = Win32API::File::GetLogicalDrives();
    for my $drive ("F".."Z", "A".."E") {
        return("$drive:") if !($drives & (2 ** (ord($drive) - ord("A"))));
    }
    return("") if shift();
    die("GetFreeDrive: No free drive available.\n");
}

sub SubstDrive($$)
{
    my ($drive, $path) = (uc(shift()), GetAbsDirname(shift()));
    DPrint(16, "SubstDrive: `$drive' => `$path'\n");
    $gSubstdrv{$drive} = 1, return if !(Win32API::File::GetLogicalDrives() & (2 ** (ord($drive) - ord("A")))) &&
        Win32API::File::DefineDosDevice(0, $drive, $path);
    die("Can't substitute `$drive' => `$path'\n");
}

sub UnsubstDrive($)
{
    return if (my $drive = uc(shift())) eq "";
    DPrint(16, "UnsubstDrive: `$drive'\n");
    delete($gSubstdrv{$drive}), return if Win32API::File::DefineDosDevice(DDD_REMOVE_DEFINITION, $drive, []) &&
        !(Win32API::File::GetLogicalDrives() & (2 ** (ord($drive) - ord("A"))));
    warn("Can't remove substituted drive `$drive'\n");
}

sub Search($$$$$$\@\$)
{
    my ($dir, $basere, $inclre, $exclre, $subdir, $finddir, $files, $total) = @_;
    my @dir = my @file = ();

    opendir(SDIR, $dir) or warn("Can't open directory `$dir'.\n");
    while (local $_ = readdir(SDIR)) {
        next if ($_ eq ".") || ($_ eq "..");
        push(@dir, $_) if ((my $isdir  = !(my $isfile = -f($_ = "$dir/$_")) && -d()) && $subdir);
        next if ($finddir ? $isfile : $isdir);
        ++$$total;
        (my $fname = $_) =~ s/$basere//;
        push(@file, $_) if ($fname =~ /$inclre/) && ($fname !~ /$exclre/) &&
            (($finddir != 2) || !@{[glob((/\s/ ? "\"$_\"" : $_) . "/{[^.],.[^.],.??*,*}")]});
    }
    closedir(SDIR);
    push(@$files, sort({lc($a) cmp lc($b)} @file));

    foreach (sort({lc($a) cmp lc($b)} @dir)) {
        Search($_, $basere, $inclre, $exclre, 1, $finddir, @$files, $$total);
    }
}

sub Find($$$$$\$)
{
    my ($dur, $dir, $inclpat, $exclpat, $subdir, $finddir, $total) = (time(), @_);
    ($dir, $$total) = (GetAbsDirname($dir), 0);
    my ($inclre, $exclre, @files) = ("", "", ());
    if ($inclpat =~ /^\//) {
        $inclre = eval("qr$inclpat");
        $inclpat = "";
    } else {
        $inclre = join("|", map(Wcard2Restr($_), split(/\s+/, $inclpat)));
        $inclre = qr/\/(?:$inclre)$/i;
    }
    if ($exclpat =~ /^\//) {
        $exclre = eval("qr$exclpat");
        $exclpat = "";
    } else {
        $exclre = join("|", map(Wcard2Restr($_), split(/\s+/, $exclpat)));
        $exclre = qr/\/(?:$exclre)$/i;
    }
    DPrint(16, "Find" . ($finddir == 2 ? "EmptyDir" : ($finddir ? "Dir" : "File")) . ": Directory `$dir'" .
        ($subdir ? " and subdirectories" : "") . ", pattern `" . ($inclpat ne "" ? "$inclpat' $inclre" : "$inclre'") .
        ($exclre eq qr/\/(?:)$/i ? "" : " excluding `" . ($exclpat ne "" ? "$exclpat' $exclre" : "$exclre'")));
    foreach (GlobFiles($dir, 1)) {
        Search($_, qr/^$_/i, $inclre, $exclre, $subdir, $finddir, @files, $$total) if -d();
    }
    DPrint(16, ", found " . @files . "/$$total " . ($finddir ? "directories" : "files") .
        ", duration: " . Sec2Min(time() - $dur) . "\n");
    return(@files);
}

sub ChangeDir($)
{
    if ((my $dir = GetAbsDirname(shift())) ne GetAbsDirname(".")) {
        DPrint(16, "ChangeDir: `$dir'\n");
        chdir($dir) or die("Can't change to directory `$dir'.\n");
    }
}

sub DeleteDir($;$)
{
    return if !-d(my $dir = GetAbsDirname(shift()));
    DPrint(16, "DeleteDir: `$dir'\n");
    for my $sec (0, 2, 5) {
        warn("Can't delete directory `$dir', retrying in $sec seconds...\n"), sleep($sec) if $sec;
        eval { local $gEvalerr = 1; File::Path::rmtree($dir) };
        return if !-d($dir);
        RunSystemCmd($gWinOS ? 'rmdir /q /s "' . PathConv($dir, 1) . '"' :
            "rm -fr '$dir'", 2);
        sleep(1);
        return if !-d($dir);
    }
    $dir = "Can't delete directory `$dir'.\n";
    shift() ? warn($dir) : die($dir);
}

sub FindDir($$$$)
{
    my ($dir, $inclpat, $exclpat, $opt) = @_;
    $opt = "" if !defined($opt);
    push(@gFindresult, Find($dir, $inclpat, $exclpat, $opt =~ /r/, 1, local $_));
}

sub MakeDir($)
{
    return if -d(my $dir = shift());
    eval { local $gEvalerr = 1; File::Path::mkpath($dir = GetAbsDirname($dir)) };
    if (-d($dir)) {
        DPrint(16, "MakeDir: `" . GetAbsDirname($dir) ."'\n");
    } else {
        DPrint(16, "MakeDir: `$dir'\n");
        die("Can't create directory `$dir'.\n");
    }
}

sub MakeChangeDir($)
{
    MakeDir(my $dir = shift());
    ChangeDir($dir);
}

sub SetWorkdir($)
{
    MakeChangeDir(shift());
    $gWorkdrive = (Cwd::cwd() =~ /^([a-z]:)/i ? uc($1) : "");
    $gWorkdir   = GetAbsDirname(".");
}

sub OpenFile(*$$;$)
{
    my ($fhandle, $file, $binmode, $print) = @_;
    MakeDir(GetDirname($file)) if $file =~ /^>/;
    DPrint(16, defined($print) ? $print : ($file =~ /^>/ ? "Write" : "Read") . "File: `$file'\n");
    return(open($fhandle, $file)) if !$binmode;
    return(open($fhandle, $file) and binmode($fhandle));
}

sub Test($)
{
    if (-d(my $file = shift())) {
        DPrint(16, "TestDir: `" . GetAbsDirname($file) . "'\n");
    } elsif (-f($file)) {
        DPrint(16, "TestFile: `" . GetAbsFname($file) . "'\n");
    } else {
        DPrint(16, "Test: `$file'\n");
        die("File or directory `$file' doesn't exist.\n");
    }
}

sub CutFile($$$$$)
{
    my ($msg, $src, $dest, $head, $len) = @_;
    my ($buf, $srctmp) = (undef, "$src.tmp");

    OpenFile(*INFILE, $src, 1, $msg) or
        die("Can't read file `$src'.\n"), return;

    my $out = GetWriteFname($head ? $dest : $srctmp);
    OpenFile(*OUTFILE, $out, 1) or die("Can't write to `$out'.\n"), return;
    while ($len > 0) {
        read(INFILE, $buf, $len < READBUFSIZE ? $len : READBUFSIZE);
        print(OUTFILE $buf);
        $len -= READBUFSIZE;
    }
    close(OUTFILE);

    $out = GetWriteFname($head ? $srctmp : $dest);
    OpenFile(*OUTFILE, $out, 1) or die("Can't write to `$out'.\n"), return;
    print(OUTFILE $buf) while read(INFILE, $buf, READBUFSIZE);
    close(OUTFILE);
    close(INFILE);
    Move($srctmp, $src);
}

sub Copy($$;$)
{
    my ($src, $dest, $dir) = @_;
    $dir = defined($dir) && $dir;
    my $file = !($dir || -d($src));
    $src  = ($file ? GetAbsFname($src) : GetAbsDirname($src));
    $dest = ($file ? GetAbsFname(-d($dest) ? "$dest/" . GetBasename($src) : $dest) :
        GetAbsDirname($dir ? $dest : "$dest/" . GetBasename($src)));
    if ($file && ($dest =~ /^>>[^>]/)) {
        OpenFile(*FILE, $dest, 1, "AppendFile: `$src' => `$dest'\n")
            or die("Can't append to `$dest'.\n"), return;
        File::Copy::copy($src, *FILE) and
            close(FILE) and return;
    }
    elsif ($file) {
        MakeDir(GetDirname($dest));
        DPrint(16, "CopyFile: `$src' => `$dest'\n");
        warn("CopyFile: Destination file `$dest' already exists\n") if -f($dest);
        File::Copy::copy($src, $dest) and return;
    } else {
        DPrint(16, "CopyDir: `$src' => `$dest'\n");
        return if !RunSystemCmd(!$gWinOS ? "cp \"$src\"/* \"$dest\" -frv" :
            'xcopy "' . PathConv($src, 1) . '" "' . PathConv($dest, 1) . '" /e /h /i /q /y /z', 2);
    }
    die("Can't copy `$src' to `$dest'.\n");
}

sub CopyIby($$)
{
    my ($file, $dir) = (GetAbsFname(shift()), shift());
    OpenFile(*FILE, $file, 0) or die("Can't read file `$file'.\n"), return;
    map {
        Copy(defined($1) ? $1 : $2, "$dir/" . (defined($3) ? $3 : $4)) if $_ =~ FILESPECSTATEMENT;
    } <FILE>;
    close(FILE);
}

sub DeleteFile($;$)
{
    return if !-f(my $file = GetAbsFname(shift()));
    DPrint(16, "DeleteFile: `$file'\n");
    for my $sec (0, 1, 2) {
        warn("Can't delete file `$file', retrying in $sec second(s)...\n"), sleep($sec) if $sec;
        unlink($file);
        return if !-f($file);
    }
    $file = "Can't delete file `$file'.\n";
    shift() ? warn($file) : die($file);
}

sub FindFile($$$$)
{
    my ($dir, $inclpat, $exclpat, $opt) = @_;
    $opt = "" if !defined($opt);
    my @find = Find($opt !~ /f/ ? $dir : GetDirname($dir), $opt !~ /f/ ? $inclpat : GetBasename($dir),
        $exclpat, $opt =~ /r/, 0, local $_);
    push(@gFindresult, $opt !~ /f/ ? @find : map("|$_|$inclpat", @find));
}

sub HeadFile($$$)
{
    my ($src, $dest, $len) = (GetAbsFname(shift()), GetAbsFname(shift()), shift());
    $len = hex($len) if $len =~ /^0x/;
    CutFile("HeadFile: Cut first $len bytes from `$src' => `$dest'\n", $src, $dest, 1, $len);
}

sub TailFile($$$)
{
    my ($src, $dest, $len) = (GetAbsFname(shift()), GetAbsFname(shift()), shift());
    $len = hex($len) if $len =~ /^0x/;
    CutFile("TailFile: Cut last $len bytes from `$src' => `$dest'\n", $src, $dest, 0, (-s($src) ? -s($src) : 0) - $len);
}

sub TypeFile($;$)
{
    my ($file, $str, $mode) = (GetAbsFname(shift()), "", shift() || "");
    OpenFile(*FILE, $file, $mode, "TypeFile: `$file'" .
        ($gOutfilter && ($mode ne "b") ? ", filter: `/$gOutfilter/i'" : "") . "\n") or
            die("Can't read file `$file'.\n"), return;
    DPrint(8, STARTSTR . "\n");
    read(FILE, $str, -s($file));
    if ($mode eq "b") {
        DPrint(1, Byte2Str(0, map(ord(), split(//, $str))));
    } else {
        $str = Uni2Ascii($str) if $mode eq "u";
        DPrint(1, map("$_\n", grep(!$gOutfilter || /$gOutfilter/i, split(/\n/, $str))));
        $gOutfilter = "";
    }
    DPrint(8, ENDSTR . "\n");
    close(FILE);
}

sub ReadFile($$)
{
    my ($file, $warn) = (GetAbsFname(shift()), shift());
    OpenFile(*RFILE, $file, 0) or
        ($warn ? (warn("Can't read file `$file'.\n"), return(())) : die("Can't read file `$file'.\n"));
    my @file = map(chomp() ? $_ : $_, grep(!/^\s*$/, <RFILE>));
    close(RFILE);
    return(@file);
}

sub WriteFile($$$;$$)
{
    my ($file, $str, $mode, $opt) = (GetAbsFname(shift()), shift(), shift() || "", shift());
    OpenFile(*WFILE, GetWriteFname($file), $mode) or
        die("Can't write to `$file'.\n"), return;
    if ($mode eq "b") {
        my @byte = Str2Byte($str);
        DPrint(64, Byte2Str($file =~ s/^>>(?!>)// ? -s($file) : 0, @byte));
        print(WFILE map(chr(), @byte));
    } else {
        $opt = "" if !defined($opt);
        $str = Unquote($str) if ($opt !~ /q/);
        $str =~ s/(?<=\S)\/\//\//g if ($opt =~ /c/);
        DPrint(16, $str) if shift();
        $str = Ascii2Uni($str) if ($mode eq "u");
        print(WFILE $str);
    }
    close(WFILE);
}

sub UnzipFile($$)
{
    my ($zipfile, $dir) = (GetAbsFname(shift()), GetAbsDirname(shift()));
    DPrint(16, "UnzipFile: `$zipfile'");
    Archive::Zip::setErrorHandler(sub{});
    my ($error, $zip) = (0, Archive::Zip->new());
    if ($zip->read($zipfile) != AZ_OK) {
        DPrint(16, " to directory `$dir'\n");
        die("Can't read zip archive `$zipfile'.\n");
        return;
    }
    my @files = map($_->fileName(), grep(!$_->isDirectory(), $zip->members()));
    DPrint(16, ", " . @files . " files to directory `$dir'\n");
    foreach my $file (@files) {
        DPrint(16, "ExtractFile: `$dir/$file'");
        eval { local $gEvalerr = 1; $error = ($zip->extractMember($file, "$dir/$file") != AZ_OK) };
        DPrint(16, $error ? " Failed\n" : "\n");
        die("Can't extract file `$file' to directory `$dir'.\n") if $error;
        $error = 0;
    }
}

sub Zip($$$$@)
{
    my ($zipfile, $dir, $opt, $prefix) = (GetAbsFname(shift()), shift(), shift(), shift());

    $opt = (defined($opt) ? ", options: `$opt'" : "");
    $prefix = GetAbsDirname($prefix) if $prefix ne "";
    my %files = ();
    foreach my $file (@_) {
        my $zname = "";
        ($file, $zname) = ($1, $2) if ($file =~ /^\|(.*)\|(.*)$/);
        next if !($file = (!$dir ? (-f($file) ? GetAbsFname($file) : "") : (-d($file) ? GetAbsDirname($file) : "")));
        ($zname = ($zname eq "" ? $file : (!$dir ?
            GetAbsFname($zname) : GetAbsDirname($zname)))) =~ s/^(?:$gEpocroot|[a-z]:)?\/+//i;
        if ($opt !~ /j/) {
            $zname =~ s/^.*?\/+/$prefix\// if ($prefix ne "");
        } else {
            $zname = ($dir ? "" : GetBasename($file)) if ($prefix eq "") || !s/^$prefix//;
        }
        $files{lc($zname)} = [$file, $zname];
    }

    DPrint(16, ($dir ? "ZipDir: `$zipfile'$opt, " . keys(%files) . " directories" :
        "ZipFile: `$zipfile'$opt, " . keys(%files) . " files") . ($prefix ? ", prefix: $prefix\n" : "\n"));

    Archive::Zip::setErrorHandler(sub{});
    my ($error, $zip) = (0, Archive::Zip->new());
    $zip->read($zipfile) if (my $ziptmp = ($zipfile =~ s/^>>(?!>)// ? "$zipfile.tmp" : ""));
    $zip->zipfileComment("iMaker-generated zip archive `$zipfile'$opt.");

    foreach my $file (sort({lc($$a[0]) cmp lc($$b[0])} values(%files))) {
        DPrint(16, "Add" . ($dir ? "Dir" : "File") . ": `$$file[0]' => `$$file[1]'") if ($opt !~ /q/);
        eval {
            my $warn = 0;
            local $gEvalerr = 1; local $SIG{__WARN__} = sub{ $warn = 1 };
            $error = ($dir ? $zip->addTree($$file[0], $$file[1]) != AZ_OK :
                !$zip->addFile($$file[0], $$file[1])) || $warn;
        };
        DPrint(16, $error ? " Failed\n" : "\n") if ($opt !~ /q/);
        warn("Can't add " . ($dir ? "directory tree" : "file") . "`$$file[0]' to zip archive `$zipfile'.\n") if $error;
        $error = 0;
    }
    ($zip->writeToFileNamed($ziptmp ? $ziptmp : $zipfile) == AZ_OK) or
        die("Can't create zip archive `$zipfile'.\n");
    Move($ziptmp, $zipfile) if $ziptmp;
}

sub Move($$)
{
    my ($src, $dest) = @_;
    my $dir = -d($src);
    $src = ($dir ? GetAbsDirname($src) : GetAbsFname($src));
    MakeDir(GetDirname($dest));
    $dest = ($dir ? GetAbsDirname($dest) : GetAbsFname($dest));
    DPrint(16, "Move" . ($dir ? "Dir" : "File") . ": `$src' => `$dest'\n");
    File::Copy::move($src, $dest) or
        die("Can't move `$src' to `$dest'.\n");
}

sub Touch($@)
{
    my $time = (shift() =~ /^(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/ ?
        Time::Local::timelocal($6, $5, $4, $3, $2 - 1, $1 - 1900) : time);
    if (@_ != 1) {
        DPrint(16, "Touch: " . scalar(@_) . " files/dirs, " .
            POSIX::strftime("%Y-%m-%d %H:%M:%S", localtime($time)) . "\n");
        utime($time, $time, @_) == @_ or
            die("Can't touch all the " . scalar(@_) . " files/dirs.\n");
        return;
    }
    my $file = shift();
    my $dir = -d($file);
    $file = ($dir ? GetAbsDirname($file) : GetAbsFname($file));
    DPrint(16, "Touch" . ($dir ? "Dir" : "File") . ": `$file', " .
        POSIX::strftime("%Y-%m-%d %H:%M:%S", localtime($time)) . "\n");
    utime($time, $time, $file) == 1 or
        die("Can't touch " . ($dir ? "directory" : "file") . " `$file'.\n");
}

sub SetLogfile($)
{
    return if !(my $file = GetAbsFname(shift()));
    my $append = (($file =~ s/^>>(?!>)//) || exists($gLogfiles{$file}) ? ">>" : "");
    CloseLog();
    OpenFile(*LOG, GetWriteFname($file = "$append$file"), 0) or
        warn("Can't log to file `$file'.\n"), return;
    $gLogfiles{$gLogfiles{__prev__} = $gLogfile = $file} = 1;
}


###############################################################################
#

sub RunSystemCmd($;$$$)
{
    return if ($gICmd !~ $gFiltercmd);
    my ($cmd, $keepgoing, $null, $file) = @_;
    DPrint(1, "$cmd\n"), return if $gPrintcmd;
    local $gError = 0 if ($keepgoing = (defined($keepgoing) && ($keepgoing =~ /^[123]$/) ? $keepgoing : 0));
    local $gKeepgoing = Max($gKeepgoing, $keepgoing) if $keepgoing;
    $file = (defined($file) ? GetAbsFname($file) : "");
    @gCmdoutbuf = ();
    DPrint(4, local $_ = "RunSystemCmd(" . GetAbsDirname(".") . "): `$cmd'" .
        ($keepgoing ? ", keep going" . ($keepgoing > 1 ? "($keepgoing)" : "") : "") .
        ($file ? ", redirect to `$file'" : "") . ($null ? ", redirect stdout to null" : "") .
        ($gOutfilter ? ", filter: `/$gOutfilter/i'" : "") . "\n");
    OpenFile(*CMDFILE, GetWriteFname($file), 0) or
        (die("Can't write to `$file'.\n"), $file = "") if $file;
    print(CMDFILE $_) if $file;
    my $dur = time();
    open(CMD, "$cmd 2>&1 |");
    DPrint(8, STARTSTR . "\n");
    while ($_ = <CMD>) {
        chomp();
        push(@gCmdoutbuf, $_);
        next if ($gOutfilter && !/$gOutfilter/i);
        DPrint(8, "$_\n") if !$null;
        print(CMDFILE "$_\n") if $file;
    }
    close(CMD);
    my $error = ($? >> 8);
    close(CMDFILE) if $file;
    push(@gStepDur, $dur = time() - $dur);
    $gOutfilter = "";
    print(map("$_\n", @gCmdoutbuf)) if ($error && !$gKeepgoing && !$null && $gVerbose && !($gVerbose & 8));
    $dur = Sec2Min($dur);
    DPrint(8, substr(ENDSTR, 0, -16) . $dur . substr(ENDSTR, length($dur) - 16) . "\n");
    die("Command `$cmd' failed ($error) in `" . GetAbsDirname(".") . "'.\n") if $error;
    return($error);
}


###############################################################################
#

sub ParseSystemCmd($$$$$)
{
    return if SkipICmd();
    my ($title, $inclre, $exclre, $file, $lines) = @_;
    ($inclre, $exclre) = (eval("qr$inclre"), $exclre ne "" ? eval("qr$exclre") : qr/^$/);
    $lines = ($lines ? $lines - 1 : 0);

    my @parse = ();
    for (my $i = 0; $i < @gCmdoutbuf; $i++) {
        next if ($gCmdoutbuf[$i] !~ $inclre);
        push(@parse, join(" | ", @gCmdoutbuf[$i .. $i + $lines])) if ($gCmdoutbuf[$i] !~ $exclre);
        $i += $lines;
    }
    return if !@parse;
    if (!$file) {
        DPrint(1, "$title\n", map(sprintf("%" . length(@parse) . "s", $_) . ") $parse[$_ - 1]\n", 1 .. @parse));
    } else {
        WriteFile($title, join("\n", @parse), "", "q");
    }
}


###############################################################################
#

sub GenExclfile($$$$$)
{
    return if SkipICmd();

    my ($exclfile, $base, $prefix, $exclfiles, @exclfiles) = (shift(), GetAbsDirname(shift()), shift(), "", ());

    if (!-f($exclfile)) {
        WriteFile($exclfile, "", "");
    } else {
        OpenFile(*FILE, $exclfile, 1) or die("Can't read file `$exclfile'.\n"), return;
        read(FILE, $exclfiles, -s($exclfile));
        close(FILE);
        @exclfiles = split(/\n/, Uni2Ascii($exclfiles));
    }

    my $findfiles = 0;
    my @addfiles = map($_ ne "**" ? $_ : "*", grep(!(($_ eq "*") && ++$findfiles),
        map(Trim(Unquote(Trim($_))), grep(!/^\s*(?:#.*)?$/, split(/(?<!\\)\\n/, shift())))));

    if ($findfiles) {
        $exclfiles = "";
        foreach (@exclfiles, @addfiles, map(Trim(Unquote(Trim($_))), grep(!/^\s*(?:#.*)?$/, split(/(?<!\\)\\n/, shift())))) {
            (my $file = $_) =~ tr/\\/\//;
            $file =~ s/^(?:[a-z]:)?\/*//i;
            $exclfiles .= ($exclfiles ne "" ? "|" : "") . Wcard2Restr($file);
        }
        push(@addfiles, map(GetRelFname($_, $base), Find($base, "*", "/^\\/(?:$exclfiles)\$/i", 1, 0, local $_)));
    }

    $prefix =~ s/[\/\\]+$//;
    WriteFile($exclfile, join("", map("$_\n", @exclfiles,
        map(s/^(?:[a-z]:)?\\*/$prefix\\/i ? $_ : $_, map(tr/\//\\/ ? $_ : $_, @addfiles)))), "u", "q");
}

sub GenIbyfile($$$)
{
    return if SkipICmd();
    my ($ibyfile, $ibystr, $oride, $prevoride) = (shift(), "", "", "");

    map {
        die("GenIbyfile: Invalid file list configuration: `$_'\n"), return
            if !/^\s*(?:"(.+?)"|(\S+))\s+(?:"(.+?)"|(\S+))\s*$/;
        $_ = [defined($1) ? $1 : $2, defined($3) ? $3 : $4];
    } (my @files = map(Unquote($_), grep(!/^\s*(?:#.*)?$/, split(/(?<!\\)\\n/, shift()))));

    my @ibyconf = map(Unquote($_), grep(!/^\s*(?:#.*)?$/, split(/(?<!\\)\\n/, shift())));

    foreach (@ibyconf) {
        die("GenIbyfile: Invalid configuration: `$_'\n"), return
            if !/^\s*(?:"(.+?)"|(\S+))\s+(hide|remove|(?:replace|udeb|urel)(?:-add)?)\s+(\*|core|rofs[2-6])\s*$/i;
        next if ($4 ne "*") && (uc($4) ne $gImgtype);
        my $action = lc($3);
        my $file = Wcard2Restr(defined($1) ? $1 : $2);
        $file = qr/(?:^|\\|\/)$file$/i;
        foreach (@files) {
            next if (@$_[1] !~ $file);
            $oride = ($action =~ /add$/ ? "ADD" : ($action eq "hide" ? "" : "SKIP"));
            my $src = ($action eq "remove" ? "empty" : @$_[0]);
            if ($action =~ /^udeb/) {
                $src =~ s/(?<=[\/\\])urel(?=[\/\\])/udeb/i;
            } elsif ($action =~ /^urel/) {
                $src =~ s/(?<=[\/\\])udeb(?=[\/\\])/urel/i;
            }
            $ibystr .= ($prevoride && ($oride ne $prevoride) ? "OVERRIDE_END\n" : "") .
                ($oride && ($oride ne $prevoride) ? "OVERRIDE_REPLACE/$oride\n" : "") .
                ($oride ? "override=\"$src\"  " : "hide=") . "\"@$_[1]\"\n";
            $prevoride = $oride;
        }
    }
    WriteFile($ibyfile, ($ibyfile =~ /^>>([^>].*)$/ && -f($1) ? "" : "// Generated `$ibyfile'") .
        "\n\n/* Custom override configuration\n" . join("\n", @ibyconf) . "\n*/\n$ibystr" .
        ($oride ? "OVERRIDE_END\n" : ""), "", "q");
}

sub GenObyfile($$$$@)
{
    return if SkipICmd();

    my ($ibyfile, $srcdir, $subdir, $finddir) = (GetAbsFname(shift()), shift(), shift(), shift());
    my ($header, $footer, $body, %files) = ("", "", "", ());

    foreach my $dir (split(/\s+/, $srcdir)) {
        $dir = GetAbsDirname($dir);
        my ($found, $total, $lines) = (0, 0, "");
        my @param = @_;
        while (@param) {
            my ($filepat, $format, @lines) = (shift(@param), shift(@param), ());
            $header = $format, next if $filepat =~ /^__header__$/i;
            $footer = $format, next if $filepat =~ /^__footer__$/i;
            foreach my $src (Find($dir, $filepat, "", $subdir, $finddir, $total)) {
                next if $files{$src};
                $files{$src} = 1;
                (my $line = $format) =~ s/%1/$src/g;
                $line =~ s/%2/GetRelFname($src, $dir, 1)/ge;
                $line =~ s/%3/GetAbsFname($src)/ge;
                if ($line =~ /%4/) {
                    my $attrib = "";
                    if ($gWinOS) {
                        Win32::File::GetAttributes($src, $attrib);
                        $attrib = (($attrib & WIN32_FILE_HIDDEN) ? "attrib=H" : "");
                    }
                    $line =~ s/%4/$attrib/ge;
                }
                push(@lines, Trim($line));
            }
            $found += @lines;
            $lines .= "//\n// Format: `$format', " . @lines . ($finddir ? " empty directories" : " files") .
                 ": `$filepat'\n" . (@lines ? "//\n" . join("\n", @lines) . "\n" : "");
        }
        $body .= "\n// Collected entries $found/$total from directory `$dir'" .
            ($subdir ? " and subdirectories" : "") . "\n$lines";
    }

    my $append = ($ibyfile =~ s/^>>(?!>)// && -f($ibyfile) && ">>" || "");
    (my $fname = "__" . uc(GetBasename($ibyfile)) . "__") =~ s/\W/_/g;
    my @previby = ();

    if ($append) {
        OpenFile(*FILE, $ibyfile, 0) or die("Can't read file `$ibyfile'.\n"), return;
        @previby = <FILE>;
        close(FILE);
        $previby[0] =~ s/(, collected )(\d+)( entries)$/$1.($2 + keys(%files)).$3/e;
        $previby[@previby - 1] = "";
    }
    WriteFile($ibyfile, join("", @previby) . ($append ? "// Appended" : "// Generated") .
        " `$append$ibyfile', collected " . keys(%files) . " entries\n" .
        ($append ? "" : "\n#ifndef $fname\n#define $fname\n") .
        ($header ? Unquote("\\n$header\\n") : "") . $body . ($footer ? Unquote("\\n$footer\\n") : "") .
        "\n#endif // $fname\n", "", "q");
}

sub GenWidgetConf($$$$)
{
    return if SkipICmd();
    my ($wgzini, $ini, $dir) = (shift(), GetAbsFname(shift()), GetAbsDirname(shift()));
    my @ini   = ($ini eq "" ? () : ReadFile($ini,  0));
    my $files = ($dir eq "" ? "" : join("\n", Find($dir, "*", '/\/(?:' . join("|",
        map(GetBasename($_), ($ini, map(!/^\s*[#[]/ && /^\s*(?:"(.+?)"|(\S+))/ &&
            -e(local $_ = (defined($1) ? $1 : $2)) ? $_ : (), @ini)))) . ')$/i', 0, 0, local $_)));

    WriteFile($wgzini, Unquote(shift()) .
        (@ini ? "# Copied lines from `$ini':\n" . join("\n", @ini) : "") . "\n" .
        ($files ? (@ini ? "\n" : "") . "# Collected files from `$dir':\n$files\n" : ""), "", "q");
}


###############################################################################
#

sub GenMakefile($$$$$)
{
    return if SkipICmd();
    my ($hdrfile, $mkfile, $filter, $prepros, $assignop) =
        (GetAbsFname(shift()), GetAbsFname(shift()), shift(), shift(), shift());
    ChangeDir(GetDirname($hdrfile));
    RunSystemCmd("$prepros " . GetBasename($hdrfile));
    my $maxdef = Max(map(/^\s*\#define\s+($filter)/ && length($1), @gCmdoutbuf));
    WriteFile($mkfile, join('\n',
        map(/^\s*\#define\s+($filter)\s*(.*?)\s*$/ ? sprintf("%-${maxdef}s $assignop %s", $1, $2 eq "" ? 1 : $2) : (), sort(@gCmdoutbuf))) . '\n', "");
}


###############################################################################
#

sub AddImageHeader($$$$$)
{
    return if SkipICmd();
    my ($file, $hdrfile, $hdrstr, $hdrsize, $align) =
        (GetAbsFname(shift()), GetAbsFname(shift()), shift(), shift(), shift());

    $hdrstr =~ s/\/\*.*?\*\///g;
    $hdrstr =~ s/,\s*$//;
    WriteFile($hdrfile, $hdrstr, "b");
    die("Invalid image header size: " . sprintf("0x%X", -s($hdrfile)) . " (!=$hdrsize).\n"), return
        if -s($hdrfile) ne hex($hdrsize);

    $align = Max(hex($align), hex($hdrsize)) - hex($hdrsize);
    WriteFile(">>$hdrfile", ("0," x ($align - 1)) . "0", "b") if $align;
    Copy($file, ">>$hdrfile") if $file ne "";
}


###############################################################################
#

sub Sleep($)
{
    return if SkipICmd();
    sleep(shift());
}


###############################################################################
#

sub FindSOSFiles($$$$)
{
    return if SkipICmd();

    my ($dirs, $imgoby, $pluglog, $opt) = @_;
    my ($file, %files) = ("", ());
    local $_;

    foreach my $dir (GlobFiles($dirs)) {
        my ($featvar, @pluglog) = ("", Find($dir = GetAbsDirname($dir), $pluglog, "", 1, 0, $_));

        foreach $file (@pluglog) {
            OpenFile(*FILE, $file, 0) or warn("Can't read file `$file'.\n"), last;
            while (<FILE>) {
                last if !/^.+?\.pm: Initializing; /;
                $featvar = $1, last if / feature variant = `(.+)'$/;
            }
            close(FILE);
            last if ($featvar ne "");
        }

        foreach $file (Find($dir, $imgoby, "", 1, 0, $_)) {
            OpenFile(*FILE, $file, 0) or warn("Can't read file `$file'.\n"), last;
            while (<FILE>) {
                next if ($_ !~ FILESPECSTATEMENT) && ($_ !~ BOOTBINARYSTATEMENT);
                $file = GetAbsFname(defined($1) ? $1 : $2);
                $files{lc($file)} = $file if !exists($files{lc($file)});
                next if ($file !~ s/\.[0-9a-f]{32}\./\./i);
                $file .= (-f("$file.$featvar.vmap") ? ".$featvar.vmap" : ".vmap");
                $files{lc($file)} = $file if !exists($files{lc($file)});
            }
            close(FILE);
        }

        my ($incfile, $spifile, $plugfile, $patchfile) = (0, 0, 0, 0);
        foreach $file (@pluglog) {
            OpenFile(*FILE, $file, 0) or warn("Can't read file `$file'.\n"), last;
            while (<FILE>) {
                $incfile   = 1, next if /^Finding include hierarchy from /;
                $incfile   = 0, next if ($incfile && /^Found \d+ different include files$/);
                $spifile   = 1, next if /^Finding SPI input files from /;
                $spifile   = 0, next if ($spifile && /^Found \d+ SPI input files$/);
                $plugfile  = 1, next if /^Reading (ROM|ROFS1|UDEB|UREL) files from /;
                $plugfile  = 0, next if ($plugfile && /^Found \d+ entries$/);
                $patchfile = 1, next if /^Finding ROM-patched components$/;
                $patchfile = 0, next if ($patchfile && /^Found \d+ ROM-patched components$/);
                $files{lc($file)} = $file, next
                    if (($incfile || $spifile || $plugfile) && /`(.+)'$/ && !exists($files{lc($file = GetAbsFname($1))}));
                next if (!$patchfile || !/^`(.+)'$/);
                $file = GetAbsFname($1) . ".map";
                $files{lc($file)} = $file, next if -f($file);
                $file =~ s/(\..*?\.map)$/\.\*$1/;
                foreach (glob($file =~ /\s/ ? "\"$file\"" : $file)) {
                    ($file = lc()) =~ s/\.map$//;
                    $files{lc()} = $_, last if exists($files{$file});
                }
            }
            close(FILE);
        }

        $dir .= "/" if $dir !~ /\/$/;
        foreach $file (keys(%files)) {
            delete($files{$file}) if ($file =~ /^$dir/i);
        }
    }

    @gFindresult = () if (!defined($opt) || $opt !~ /a/);
    push(@gFindresult, values(%files));
}


###############################################################################
#

sub CheckTool(@)
{
    return if SkipICmd();
    my ($maxtlen, $maxvlen, @tools) = (4, 9, ());
    while (@_) {
        my ($tool, $vquery, $getver, $version, $md5sum) = (shift(), shift(), shift(), " -", " ?");
        if (length($vquery) > 1) {
            RunSystemCmd($vquery, 3, 1);
            $version = (join("\n", @gCmdoutbuf) =~ eval($getver =~ /^\// ? "qr$getver" : "qr/$getver/ims") ?
                (defined($1) && defined($2) && "`$1 $2'" || defined($1) && "`$1'" || " ?") : " ?");
        }
        OpenFile(*FILE, $tool, 1) and $md5sum = "`" . md5_hex(<FILE>) . "'";
        close(FILE);
        $maxtlen = Max($maxtlen, length($tool));
        $maxvlen = Max($maxvlen, length($version));
        push(@tools, "`$tool'", $version, $md5sum);
    }
    $maxtlen += 2;
    @_ = (" Tool", " Version", " MD5 Checksum", "-" x $maxtlen, "-" x $maxvlen, "-" x 34, @tools);
    DPrint(1, sprintf("%-${maxtlen}s %-${maxvlen}s ", shift(), shift()) . shift() . "\n") while(@_);
}


###############################################################################
#

sub OpCacheInstall($$$)
{
    return if SkipICmd();
    my ($ini, $conf, $tmpdir) = @_;
    my %opt = (-e => "", -i => "", -m => "", -o => "", -u => "");

    foreach $conf ("opcache_config=$conf", ($ini ne "" ? grep(!/^\s*#/, ReadFile($ini, 0)) : ())) {
        (local $_, my $error, my %tmpopt) = ($conf, 0, %opt);
        if (!($error = !(s/^\s*opcache_config\s*[=\s]//i || s/^\s*opcache_content\s*[=\s]/-i /i))) {
            my @opt = ParseCmdWords($_);
            while (@opt) {
                last if ($error = ((($_ = shift(@opt)) !~ /^-[eimou]$/i) ||
                    !defined($tmpopt{$_} = shift(@opt))));
                $tmpopt{$_} =~ s/EPOCROOT/$gEpocroot/g;
            }
        }
        die("OpCacheInstall: Invalid configuration entry: `$conf'\n"), next if $error;
        %opt = %tmpopt;
    }
    if (-d($opt{-i})) {
        $opt{-i} = GetAbsDirname($opt{-i});
    } elsif (-f($opt{-i})) {
        DeleteDir($tmpdir);
        MakeDir($tmpdir);
        RunSystemCmd("$gTool{unzip} x -y \"" . GetAbsFname($opt{-i}) . "\"" .
            " -o\"" . ($tmpdir = GetAbsDirname($tmpdir)) . "\"", 0, 1);
        $opt{-i} = $tmpdir;
    }
    RunSystemCmd("$gTool{opcache} -u \"$opt{-u}\" -e \"$opt{-e}\" -m \"" .
        GetAbsFname($opt{-m}) . "\" -i \"$opt{-i}\" -o \"" . GetAbsDirname($opt{-o}) . "\"");
}


###############################################################################
#

sub SisInstall($$$$$$$$)
{
    return if SkipICmd();

    my ($ini, $intini, $conf, $hda, $hdata, $idata, $outdir, $log) =
        (GetAbsFname(shift()), GetAbsFname(shift()), shift(), GetAbsFname(shift()),
            shift(), shift(), GetAbsDirname(shift()), shift());
    my %gopt = (-d => "C", -k => "5.4", -w => "info", '--ignore-err' => 0);

    my %haldata = ();
    map { $haldata{uc($1)} = $2 if /^\s*(\S+)\s+(\S+)\s*$/ } split(/(?<!\\)\\n/, $hdata);
    $gOutfilter = '\S';
    RunSystemCmd("$gTool{cpp} -nostdinc -undef \"$hda\"", 1, 1, $log) if ($hda ne "");

    local @_ = (map(!/^\s*E(\S+)\s*=\s*(\S+)\s*$/ ? () : (uc($1) . " = " .
        (exists($haldata{uc($2)}) ? $haldata{uc($2)} : (exists($haldata{uc("E$1_$2")}) ?
            $haldata{uc("E$1_$2")} : $2)) . "\n"), @gCmdoutbuf),
        map(/^\s*$/ ? () : Trim($_) . "\n", split(/(?<!\\)\\n/, $idata)));

    WriteFile($intini, join("", @_), "", "q");
    RunSystemCmd("$gTool{interpretsis} -i \"$intini\"", 3, 1);
    map { $_[$1 - 1] = undef if /Unsupported keyword.+?(\d+)/i } @gCmdoutbuf;
    WriteFile($intini, join("", grep(defined(), @_)), "", "q");

    my ($clean, @dir) = (0, Find($outdir, "*", "", 1, 1, $_));
    @_ = ("sis_config=$conf", ($ini ne "" ? grep(!/^\s*#/, ReadFile($ini, 0)) : ()), "sis_content=");

    for (my $i = 0; $i < @_; $i++) {
        local $_ = $_[$i];
        my ($error, $global, $runtool, %opt) = (0, 0, 0, %gopt);
        if (!($error = !(s/^\s*sis_(config)\s*[=\s]//i || s/^\s*sis_(content)\s*[=\s]/-s /i))) {
            $global = ($1 =~ /config/i);
            my @opt = ParseCmdWords($_);
            while (@opt) {
                $_ = shift(@opt);
                shift(@opt) if ((my $next = (@opt ? ($opt[0] !~ /^!?[-+]/ ? $opt[0] : "") : "")) ne "");
                next if /^!?-[cilwx]$/;
                if (s/^!//) { delete($opt{$_}) }
                else {
                    $_[$#_]  .= "\"$next\"", next if (!$i && /^-s$/);
                    ($opt{$_} = $next) =~ s/EPOCROOT/$gEpocroot/g;
                    $runtool  = ($next !~ /^\s*$/) if /^-s$/;
                }
            }
        }
        die("SisInstall: Invalid configuration entry: `$_[$i]'\n"), next if $error;
        %gopt = %opt if $global;
        next if !$runtool;

        foreach (-d($opt{-s}) ? Find($opt{-s}, '/\.sisx?$/i', "", 0, 0, $_) : (GetAbsFname($opt{-s}))) {
            ($opt{-s}, my $puid) = ($_, "?");
            OpenFile(*SISFILE, $_, 1, "") and sysread(SISFILE, $puid, 3 * 4) and
                $puid = sprintf("%08X", unpack("V", substr($puid, 8, 4)));
            close(SISFILE);
            DPrint(16, "SisInstall: `$_', pUID: $puid" . ($opt{'--ignore-err'} ? ", ignore errors\n" : "\n"));

            my $icmd = $gTool{interpretsis} . (join("", map(($opt{$_} ne "" ? " $_ \"$opt{$_}\"" : " $_"),
                sort({lc($a) cmp lc($b)} grep(/^-[^s]/ && !/^--ignore-err$/, keys(%opt)))))) .
                " -c \"" . (GetAbsDirname($outdir)) . "\" -i \"" . (GetAbsFname($intini)) . "\"";
            $error = RunSystemCmd("$icmd -s \"$opt{-s}\"" . join("", map(" $_",
                sort({lc($a) cmp lc($b)} grep(/^\+/, keys(%opt))))), 1, 1, ">>$log");
            my $errmsg = join(" | ", grep(s/^ERR\s*:\s*//, @gCmdoutbuf));

            $_ = join(", ", map(/^INFO:\s+Installing file:\s+\w:\\sys\\bin\\(.+?.exe)\s*$/io &&
                ($_ = $1) && (qx($gTool{elf2e32} --dump=h --e32input "$outdir/sys/bin/$_") =~
                    /^Uids:\s+.+?\s+([0-9a-f]+)\s+\(/imo) ? "$_: " . uc($1) : (), @gCmdoutbuf));
            DPrint(16, "SisInstall: `" . GetBasename($opt{-s}) . "', exe UIDs: $_\n")
                if ($_ && (!($error ||= $errmsg) || $opt{'--ignore-err'}));

            warn("Installation of SIS file `$opt{-s}' failed" . ($errmsg ? ": `$errmsg'.\n" : ".\n"))
                if ($gErrwarn = $error);
            next if (!$error || $opt{'--ignore-err'});
            $clean = 1;
            warn("Removing installation of SIS file `$opt{-s}'.\n");
            RunSystemCmd("$icmd -x $puid", 3, 1, ">>$log");
        }
    }
    return if !$clean;
    my $i = 0;
    foreach (Find($outdir, "*", "", 1, 1, $_)) {
        if (($i <= $#dir) && ($_ eq $dir[$i])) { $i++ }
        else { DeleteDir($_) }
    }
}


###############################################################################
#

sub GetIPar(;$)
{
    my $par = shift(@gIcmd);
    $par = ((my $empty = !defined($par)) ? "<UNDEFINED>" : PEval($par));
    $gParamcnt = 0 if shift();
    DPrint(32, "iPar: $gParamcnt. `$par'\n") if $gParamcnt && ($gICmd =~ $gFiltercmd);
    $gParamcnt++;
    return($empty ? undef : $par);
}

sub PEval($)
{
    local $_ = shift();
    while (/\@PEVAL{.*}LAVEP\@/) {
        my $start = rindex($_, '@PEVAL{', my $end = index($_, '}LAVEP@') + 7);
        my ($expr, $eval, $evalerr) = (substr($_, $start + 7, $end - $start - 14), undef, "");
        eval {
            local $_;
            local $gEvalerr = (SkipICmd() ? 1 : 2);
            $eval = eval($expr);
            ($evalerr = $@) =~ s/^(.+?) at .*/$1/s;
        };
#        DPrint(64, "PEval: Evaluate `$expr' = `" . (defined($eval) ? $eval : "") . "'\n");
        if (!defined($eval)) {
            $eval = "";
            warn("PEval: Evaluation of `$expr' failed: $evalerr.\n") if !SkipICmd();
        }
        substr($_, $start, $end - $start) = $eval;
    }
    return($_);
}

sub PeekICmd($)
{
    return(defined($gIcmd[0]) && $gIcmd[0] =~ /^$_[0]$/i);
}

sub SkipICmd()
{
    return($gPrintcmd || defined($gICmd) && ($gICmd !~ $gFiltercmd));
}

sub GetICmd()
{
    $gICmd = GetIPar(1);
    DPrint(32, "iCmd: " . ++$gCmdcnt . ". `$gICmd'\n") if defined($gICmd) && ($gICmd ne "") && ($gICmd =~ $gFiltercmd);
}

sub EndICmd()
{
    GetICmd(), return(1) if !defined($gIcmd[0]) || PeekICmd("end");
    return(0);
}


###############################################################################
#

sub SplitStep($)
{
    (my $step = shift()) =~ s/(?<!(\\|\s))\|/ \|/g;  # ???
    return(map((s/^\s+|(?<!\\)\s+$//g, s/\\\|/\|/g) ? $_ : $_, split(/(?<!\\)\|/, "$step ")));
}

sub RunStep($)
{
    ($gStep, my $dur, @gStepDur) = (shift(), time(), ());
    ChangeDir($gWorkdir);
    DPrint(2, "=" x 79 . "\nENTER: `$gStep'\n");

    push(@gReport, $gLogfile ? ("iMaker log", $gLogfile =~ /^>>?([^>].*)$/ ? $1 : $gLogfile, "f") : (),
        SplitStep($gStepIcmd{"REPORT_$gStep"})) if exists($gStepIcmd{"REPORT_$gStep"});

    foreach my $step ("INIT_$gStep", "CLEAN_$gStep", "BUILD_$gStep") {
        next if (!exists($gStepIcmd{$step}) || $gStepIcmd{$step} =~ /^\s*$/);
        DPrint(64, "$step = `$gStepIcmd{$step}'\n");
        @gIcmd = SplitStep($gStepIcmd{$step});
        my ($file, $iferror, @iffi) = ("", 0, ());

        while (GetICmd(), defined($gICmd)) {
            next if (local $_ = lc($gICmd)) eq "";
            if (/^if$/) {
                push(@iffi, (my $if = GetIPar()), $gFiltercmd);
                $gFiltercmd = qr/^X$/ if !$if;
            }
            elsif (/^else$/) {
                $gFiltercmd = ($iffi[$#iffi - 1] ? qr/^X$/ : $iffi[$#iffi]);
            }
            elsif (/^fi$/) {
                $gFiltercmd = pop(@iffi);
                pop(@iffi);
            }
            elsif (/^(error|warning)$/) {
                my ($errwarn, $msg) = (GetIPar(), GetIPar() . "\n");
                next if SkipICmd();
                die($msg)  if $errwarn && /e/;
                warn($msg) if $errwarn && /w/;
            }
            elsif (/^echo(\d+)?(-q)?$/) {
                Echo((defined($1) && ($1 < 128) ? $1 : 1), GetIPar(), defined($2));
            }
            elsif (/^filter$/) {
                $gOutfilter = GetIPar();
            }
            elsif (/^cmd(tee)?(-(k[0123]?|n)+)?$/) {
                RunSystemCmd(GetIPar(), (/k(\d)/ ? int($1) : (/k/ ? 1 : 0)), /n/, /tee/ ? GetIPar() : "");
            }
            elsif (/^parse(f)?(?:-(\d+))?$/) {
                ParseSystemCmd(GetIPar(), GetIPar(), GetIPar(), $1, $2);
            }
            elsif (/^(cd|copy(dir|iby)?|del(dir)?|find(dir)?(-[afr]+)?|headb|logfile|mkcd|mkdir|move|tailb|test|touch|type[bu]?|unzip|workdir|write[bu]?(-[cq]+)?|zip(dir)?(-[jq]+)?)$/) {
                my @files = GlobFiles(GetIPar());
                my $par1 = GetIPar() if /^(?:copy|find|head|move|tail|touch|(un)?zip|write)/;
                my $par2 = GetIPar() if /^(?:find|head|tail|zip)/;
                next if SkipICmd();
                @gFindresult = () if /find(?:dir)?(-[afr]+)?/ && (!defined($1) || ($1 !~ /a/));
                Touch($par1, @files), next                     if /touch/;
                foreach $file (@files) {
                    ChangeDir($file)                           if /^cd/;
                    DeleteDir($file)                           if /deldir/;
                    FindDir($file, $par1, $par2, $1)           if /finddir(-[ar]+)?/;
                    MakeDir($file)                             if /mkdir/;
                    MakeChangeDir($file)                       if /mkcd/;
                    SetWorkdir($file)                          if /workdir/;
                    Zip($file, 1, $1, $par2, GlobFiles($par1)) if /zipdir(-[jq]+)?/;
                    DeleteFile($file)                          if /del/;
                    FindFile($file, $par1, $par2, $1)          if /find(-[afr]+)?$/;
                    HeadFile($file, $par1, $par2)              if /headb/;
                    SetLogfile($file)                          if /logfile/;
                    TailFile($file, $par1, $par2)              if /tailb/;
                    TypeFile($file, $1)                        if /type(b|u)?/;
                    UnzipFile($file, $par1)                    if /unzip/;
                    WriteFile($file, $par1, $1, $2)            if /write(b|u)?(-[cq]+)?/;
                    Zip($file, 0, $1, $par2, GlobFiles($par1)) if /^zip(-[jq]+)?$/;
                    Copy($file, $par1, $1)                     if /copy(dir)?$/;
                    CopyIby($file, $par1)                      if /copyiby/;
                    Move($file, $par1)                         if /move/;
                    Test($file)                                if /test/;
                }
            }
            elsif (/^filtercmd$/) {
                $gFiltercmd = GetIPar();
                $gFiltercmd = ($gFiltercmd eq "" ? qr/\S/ : qr/$gFiltercmd/i);
            }
            elsif (/^genexclst$/) {
                GenExclfile(GetIPar(), GetIPar(), GetIPar(), GetIPar(), GetIPar());
            }
            elsif (/^geniby(-[dr]+)?$/) {
                my ($opt, $iby, $dir, @par) = ($1 || "", GetIPar(), GetIPar(), ());
                push(@par, GetIPar(), GetIPar()) while !EndICmd();
                GenObyfile($iby, $dir, $opt =~ /r/, $opt =~ /d/ ? 2 : 0, @par);
            }
            elsif (/^genorideiby$/) {
                GenIbyfile(GetIPar(), GetIPar(), GetIPar());
            }
            elsif (/^genmk$/) {
                GenMakefile(GetIPar(), GetIPar(), GetIPar(), GetIPar(), GetIPar());
            }
            elsif (/^genwgzcfg$/) {
                GenWidgetConf(GetIPar(), GetIPar(), GetIPar(), GetIPar());
            }
            elsif (/^iferror$/) {
                $iferror++;
                $gError = 0, next if $gError;
                while (defined($gIcmd[0])) {
                    GetICmd(), last if PeekICmd("endif") && !--$iferror;
                    $iferror++ if shift(@gIcmd) =~ /^iferror$/i;
                }
            }
            elsif (/^endif$/ && $iferror--) {
            }
            elsif (/^imghdr$/) {
                AddImageHeader(GetIPar(), GetIPar(), GetIPar(), GetIPar(), GetIPar());
            }
            elsif (/^pause$/) {
                DPrint(0, "Press Enter to continue...\n");
                getc();
            }
            elsif (/^sleep$/) {
                Sleep(GetIPar());
            }
            elsif (/^sosfind(-a)?$/) {
                my $opt = $1;
                FindSOSFiles(GetIPar(), GetIPar(), GetIPar(), $opt);
            }
            elsif (/^tool-(\w+)$/) {
                $gTool{$1} = GetIPar();
#                DPrint(2, "SetTool: $1: `$gTool{$1}'\n");
            }
            elsif (/^toolchk$/) {
                my @tools = ();
                push(@tools, GetIPar(), GetIPar(), GetIPar()) while !EndICmd();
                CheckTool(@tools);
            }
            elsif (/^opcache$/) {
                OpCacheInstall(GetIPar(), GetIPar(), GetIPar());
            }
            elsif (/^sisinst$/) {
                SisInstall(GetIPar(), GetIPar(), GetIPar(), GetIPar(),
                    GetIPar(), GetIPar(), GetIPar(), GetIPar());
            }
            elsif (!$gImakerext || !RunIExtCmd($_)) {
                die("Unknown iMaker command `$gICmd'.\n");
            }
        }
    }
    DPrint(2, "EXIT: `$gStep', duration: " . Sec2Min($dur = time() - $dur) . "\n");
    push(@gStepDur, $dur);
}


###############################################################################
#

sub GetConfmkList(;$)
{
    if (!%gConfmkList) {
        my ($dir, $incl, $excl, $depth) = split(/,/, $ENV{IMAKER_MKCONF});
        $dir = GetAbsDirname($dir, 0, 1, $gEpocdrive);
        ($incl, $excl) = (qr/$incl/, qr/$excl/);
        local $_;
        DPrint(16, "FindFile: GetConfmkList: `$ENV{IMAKER_MKCONF}'");
        find(sub { $gConfmkList{$1} = $File::Find::name
            if (/$incl/ && !/$excl/ && (($File::Find::name =~ tr/\///) > (($dir =~ tr/\///) + $depth)));
        }, $dir);
        DPrint(16, ", found " . keys(%gConfmkList) . " files\n");
        $gConfmkList{""} = "" if !%gConfmkList;
    }
    return(sort({lc($a) cmp lc($b)} grep($_ ne "", values(%gConfmkList)))) if shift();
}

sub GetFeatvarIncdir($)
{
    open(FILE, "$gEpocroot/epoc32/tools/variant/" . shift() . ".var") or
        return("Invalid SBV feature variant");
    my @featdata = <FILE>;
    close(FILE);
    my @incdir = ("@featdata" =~ /^\s*EXTENDS\s+(.+?)\s*$/m ? GetFeatvarIncdir($1) : ());
    @incdir = () if ("@incdir" =~ /^Invalid/);
    foreach (@featdata) {
        next if !/^\s*ROM_INCLUDE\s+(\S+)\s+(.+?)\s*$/;
        if ($1 eq "set")        { @incdir = ($2) }
        elsif ($1 eq "prepend") { unshift(@incdir, $2) }
        elsif ($1 eq "append")  { push(@incdir, $2) }
    }
    return(map("$_/" =~ /^$gEpocroot\// ? $_ : $gEpocroot . PathConv($_, 0, 1, $gEpocdrive),
        map(PathConv($_, 0, 0, $gEpocdrive), @incdir)));
}


###############################################################################
#

sub SetVerbose($;$)
{
    my $verbose = Trim(shift());
    $verbose = 127 if $verbose =~ /^debug$/i;
    $gVerbose = int($1), return if ($verbose =~ /^(\d+)$/) && ($1 < 128);
    $gVerbose = 1;
    warn("Verbose level `$verbose' is not integer between 0 - 127\n") if !shift();
}

sub CloseLog()
{
    close(LOG) if $gLogfile;
    $gLogfile = "";
}


###############################################################################
#

sub RunIMakerCmd($$$$$@)
{
    my ($makecmd, $cmdarg, $tgtext, $mklevel, $skipsteps, %prevtgt) = @_;
    $ENV{IMAKER_MKLEVEL} = $mklevel;

    ($cmdarg, my $hptgt, my @targets) = HandleCmdArg($cmdarg);

    foreach my $tgt (@targets) {
        my $skipstep = ($tgt =~ s/#$//) || $skipsteps;
        (my $target = "$tgt$tgtext") =~ s/(\[\d+\])(.+)$/$2$1/;
        if ($target eq "menu") {
            ($cmdarg, $target) = Menu($cmdarg);
            next if ($target eq "menu");
            ($cmdarg) = HandleCmdArg($cmdarg);
        }
        $prevtgt{$target =~ /^([^-]+)/ ? $1 : $target} = 1;
        push(@gReport, Trim((($target !~ /^(.+)\[\d+\]$/) || ($gVerbose & 64) ? $target : $1) .
            ($skipstep ? "#" : "") . " $hptgt"), -1, -$mklevel - 1);
        my $tgtind  = $#gReport;
        my @targets = RunMakeCmd("$makecmd $cmdarg" . ($target eq "defaultgoals" ? "" : " \"$target\"") .
            join("", map(" \"$_\"", split(/\s+/, $hptgt))), $skipstep);
        $gReport[$tgtind - 2] .= " (intermediate)" if @targets;
        $gReport[$tgtind - 1] = pop(@gStepDur);
        $gReport[$tgtind] = $mklevel + 1 if !$gError;
        delete(@gReport[$tgtind - 2 .. $tgtind]) if (@targets && !$gError && !($gVerbose & 64));
        map {
            RunIMakerCmd($makecmd, "$cmdarg $_ $hptgt", $target =~ /(-.*)$/ ? $1 : "", $mklevel + 1, $skipstep, %prevtgt)
                if !exists($prevtgt{$_});
        } @targets;
    }
}

sub RunMakeCmd($$)
{
    ($gStartmk, $gMakecmd, $gError) = (time(), Trim(shift()), 0);
    my ($skipstep, $mkstart, $start, $restart, $cwd, %env) = (shift(), 0, 0, 0, Cwd::cwd(), %ENV);
    my @stepdur = my @targets = ();
    $ENV{IMAKER_MKRESTARTS} = -1;

    do {
        InitMkglobals();
        ($gTgterr, my $printvar, my @steps) = (1, "", ());
        $ENV{IMAKER_MKRESTARTS}++;

        if ($gExportvar{""}) {
            if (!$ENV{IMAKER_EXPORTMK}) {
                (my $tmpfh, $ENV{IMAKER_EXPORTMK}) = File::Temp::tempfile(
                    File::Spec->tmpdir() . "/imaker_temp_XXXXXXXX", SUFFIX => ".mk", UNLINK => 1);
                close($tmpfh);
                $ENV{IMAKER_EXPORTMK} =~ tr-\\-\/-;
            }
            WriteFile($ENV{IMAKER_EXPORTMK}, "# Generated temporary makefile `$ENV{IMAKER_EXPORTMK}'\n" .
                "ifndef __IMAKER_EXPORTMK__\n__IMAKER_EXPORTMK__ := 1\n" .
                join("", map(/^([^:]+)(?:\:(.+))?$/ && !defined($2) ? "$1=$gExportvar{$_}\n" :
                    "ifeq (\$(filter $1,\$(TARGETNAME)),)\n$2=$gExportvar{$_}\nendif\n",
                        sort({($a =~ /([^:]+)$/ && uc($1)) cmp ($b =~ /([^:]+)$/ && uc($1))}
                            grep(!/^(?:|.*[+:?])$/, keys(%gExportvar))))) .
                "else\n" .
                join("", map(/^\d{3}(.+[+:?])$/ ? "$1=$gExportvar{$_}\n" : (), sort({$a cmp $b} keys(%gExportvar)))) .
                "endif # __IMAKER_EXPORTMK__\n", "", "q", 1);
            $gExportvar{""} = 0;
        }

        open(MCMD, "$gMakecmd 2>&1 |");
        while (local $_ = <MCMD>) {
            chomp();
            DPrint(1, "$_\n"), next if !s/^#iMaker\x1E//;
#           DPrint(64, "#iMaker#$_\n");

            if (/^BEGIN$/) {
                $mkstart = time();
                $start = $mkstart if !$start;
                next;
            }
            if (/^STEPS=(.*)$/) {
                my $steps = $1;
                @steps = split(/\s+/, $steps), next if ($steps !~ s/^target://);
                @targets = grep($_ ne "", map(Trim($_), split(/(?<!\\)\|/, $steps)));
                next;
            }
            $gImgtype   = $1,    next if /^IMAGE_TYPE=(.*)$/;
            $gKeepgoing = $1,    next if /^KEEPGOING=(.*)$/;
            $gPrintcmd  = $1,    next if /^PRINTCMD=(.*)$/;
            SetVerbose($1),      next if /^VERBOSE=(.*)$/;
            $gStepIcmd{$1} = $2, next if /^((?:BUILD|CLEAN|INIT|REPORT)_\S+?)=(.*)$/;

            if (/^env (\S+?)=(.*)$/) {
                DPrint(64, "$1 = `" . ($ENV{$1} = $2) . "'\n")
                    if (!defined($ENV{$1}) || ($ENV{$1} ne $2));
                next;
            }
            if (/^var (\S+?)=(.*)$/) {
                my ($var, $val) = ($1, $2);
                my $upd = ($var !~ s/\?$//);
                $gExportvar{$var} = $val, $gExportvar{""}++
                    if (!exists($gExportvar{$var}) || ($upd && $gExportvar{$var} ne $val));
                next;
            }
            if (/^print (\d+) (\S+?)=(.*)$/) {
                $printvar  = ("=" x 79) . sprintf("\n%-$1s = `$gMakecmd'\n", "Make command") if ($printvar eq "");
                $printvar .= sprintf("%-$1s = `$3'\n", $2);
                next;
            }

            push(@stepdur, [$restart ? "ReMake" : "Make", Sec2Min(time() - $mkstart)]) if /^END$/;
            PrintEnv(2);
            DPrint(2, $printvar);
            die("Unknown iMaker entry: `$_'\n"), next if !/^END$/;

            pop(@steps) if ($restart = (@steps && $steps[$#steps] eq "RESTART"));
            my $durstr = "";
            foreach my $step (@steps) {
                next if $skipstep;
                RunStep($step);
                my ($cmddur, $stepdur) = (0, pop(@gStepDur));
                $durstr = Sec2Min($stepdur);
                if (@gStepDur) {
                    $durstr .= " (";
                    foreach my $dur (@gStepDur) {
                        $cmddur += $dur;
                        $durstr .= Sec2Min($dur) . " + ";
                    }
                    $durstr .= Sec2Min($stepdur - $cmddur) . ")";
                }
                push(@stepdur, [$step, $durstr]);
            }

            $printvar = "";
            my @env = ($ENV{IMAKER_EXPORTMK}, $ENV{IMAKER_MKRESTARTS});
            %ENV = %env;
            ($ENV{IMAKER_EXPORTMK}, $ENV{IMAKER_MKRESTARTS}) = @env;
            InitMkglobals();
            ChangeDir($cwd);

            last if $restart;

            my ($maxilen, $maxslen, $maxdlen) = (length(@stepdur . ""),
                Max(map(length(@$_[0]), @stepdur)), Max(8, map(length(@$_[1]), @stepdur)));
            DPrint(2, "=" x 79 . "\nStep" . " " x ($maxilen + $maxslen - 1) . "Duration\n" .
                "=" x ($maxilen + $maxslen + 2) . " " . "=" x $maxdlen . "\n",
                map(sprintf("%${maxilen}s. %-${maxslen}s", $_ + 1, $stepdur[$_][0]) .
                    " $stepdur[$_][1]\n", 0 .. $#stepdur),
                "-" x ($maxilen + $maxslen + 2) . " " . "-" x $maxdlen . "\n" .
                "Total" . " " x ($maxilen + $maxslen - 2) . Sec2Min(time() - $start) . "\n");
            ($start, @stepdur) = (time(), ());
        }
        close(MCMD);
        die("\n") if ($? >> 8);
        die("Command `$gMakecmd' failed in `" . GetAbsDirname(".") . "'.\n") if ($gTgterr = $gError);
        CloseLog();
    } until !$restart;
    push(@gStepDur, time() - $gStartmk);
    return(@targets);
}


###############################################################################
#

sub HandleCmdArg($)
{
    my $cmdarg = shift();
    my $origarg = $cmdarg = (defined($cmdarg) ? $cmdarg : "");

    my @cmdout = qx($ENV{PERL} -x $0 --splitarg $cmdarg);
    die("Can't parse Make arguments: `$cmdarg'.\n") if $?;

    map {
        chomp();
        s/ /\x1E/g;
        s/\"/\\\"/g;
        s/(\\+)$/$1$1/;
    } @cmdout;
    $cmdarg = " " . join(" ", @cmdout) . " ";

    if ($cmdarg =~ /^.* VERBOSE\x1E*=(\S*) /) {
        (my $verbose = $1) =~ s/\x1E/ /g;
        SetVerbose($verbose, 1);
    }

    if ($cmdarg =~ /\s+--?conf=(\S*)\s+/) {
        (my $prj = $1) =~ /(.*?)(?:;(.*))?$/;
        ($prj, my $conf) = ($1, defined($2) ? $2 : "");
        $cmdarg =~ s/\s+--?conf=\S*\s+/ USE_CONE=mk CONE_PRJ=$prj CONE_CONF=$conf cone-pre defaultgoals /;
    }

    $cmdarg = " " . HandleExtCmdArg($cmdarg) . " " if $gImakerext;

    $gMakecmd = "$ENV{IMAKER_MAKE} -f $0" . join("", map(" \"$_\"", split(/\s+/, Trim($cmdarg))));
    warn("Can't parse Make targets.\n")
        if (!(my $targets = (qx($gMakecmd 2>&1) =~ /\|MAKECMDGOALS=(.*?)\|/ ? " $1 " : "")) &&
            ($cmdarg !~ /\s-(?:-?v(?:ersion?|ersi?|er?)?|versio\S+)\s/));

    GetConfmkList() if
        grep(!/^(help(-.+)?|print-.+)$/ || /^help-config$/, my @targets = split(/\s+/, Trim($targets)));

    my ($mkfile, $mkfiles, $hptgt) = ("", "", "");
    map {
        $cmdarg =~ s/\s+\Q$_\E\s+/ /;
        if (exists($gConfmkList{$_})) {
            ($mkfile = $gConfmkList{$_}) =~ s/ /\x1E/g;
            $mkfiles .= " -f $mkfile";
            $targets =~ s/\s+\Q$_\E\s+/ /;
        }
    } @targets;
    $cmdarg = "$mkfiles$cmdarg";

    map { $targets =~ s/\s\Q$_\E\s/ /; $hptgt .= " $_" }
        grep(/^help-.+$/ && !/^help-config$/, @targets);
    map { $targets =~ s/\s\Q$_\E\s/ /; $hptgt .= " $_" }
        grep(/^print-.+$/, @targets);
    $hptgt = Trim($hptgt);

    if ($targets =~ s/ default(?= )//g) {
        ($targets = Trim($targets)) =~ s/ /\x1E/g;
        $cmdarg .= "TARGET_DEFAULT=$targets" if ($targets ne "");
        $targets = "default";
    }
    @targets = ("defaultgoals@targets") if
        !(@targets = map(s/\x1E/ /g ? $_ : $_, split(/\s+/, Trim($targets)))) || ("@targets" eq "#");

    $mkfiles = "";
    while ($cmdarg =~ s/\s+(-f\s?|--(?:file?|fi?|makefile?|makefi?|make?)[=\s]|IMAKER_CONFMK\x1E*=)(\S+)\s+/ /) {
        $mkfile = $2;
        ($mkfile = GetAbsFname(scalar($mkfile =~ s/\x1E/ /g, $mkfile))) =~ s/ /\\\x1E/g
            if ($1 !~ /^IMAKER_CONFMK/);
        $mkfiles .= ($mkfiles eq "" ? "" : chr(0x1E)) . $mkfile;
    }
    while ($cmdarg =~ s/\s+(\S+?)\x1E*([+:?])=\x1E*(\S+?)\s+/ /) {
        ($gExportvar{sprintf("%03s", ++$gExportvar{""}) . "$1$2"} = $3) =~ s/\x1E/ /g;
    }
    $cmdarg = join(" ", map(scalar(s/\x1E/ /g, "\"$_\""), split(/\s+/, Trim($cmdarg .
        ($mkfiles eq "" && ($ENV{IMAKER_MKLEVEL} || grep(/^default$/, @targets)) ? "" : " IMAKER_CONFMK=$mkfiles")))));

    DPrint(2, "HandleCmdArg: `$origarg' => `$cmdarg', `" . join(" ", @targets) . "', `$hptgt'\n");
    return($cmdarg, $hptgt, @targets);
}


###############################################################################
#

sub MenuRuncmd($)
{
    $ENV{IMAKER_CMDARG} = shift();
    return(map(chomp() ? $_ : $_, qx($ENV{PERL} -x $0 2>&1)));
}

sub Menu($)
{
    (my $cmdarg = " " . shift() . " ") =~ s/\s+"IMAKER_CONFMK="\s+/ /;
    my ($prodind, $product, @product) = (0, "", ());
    my ($tgtind, $target, $tgtcols, $tgtrows, @target)  = (0, "", 4, 0, ());
    my ($vartype, $varudeb, $varsym);
    my $cfgfile = "./imaker_menu.cfg";

    $cmdarg = ($cmdarg =~ /^\s*$/ ? "" : " " . Trim($cmdarg));
    open(FILE, "<$cfgfile") and
        (($prodind, $tgtind, $vartype, $varudeb, $varsym) = map(chomp() ? $_ : $_, <FILE>)) and close(FILE);
    ($prodind, $tgtind, $vartype, $varudeb, $varsym) =
        ($prodind || 0, $tgtind || 0, $vartype || "rnd", $varudeb || 0, $varsym || 0);

    while (1) {
        print("\nPRODUCTS\n--------\n");
        #
        if (!@product) {
            @product = sort({lc($a) cmp lc($b)} grep($_ ne "", keys(%gConfmkList)));
            $prodind = 0 if ($prodind > @product);
        }
        $product = ($prodind ? " $product[$prodind - 1]" : "");
        my $maxlen = Max(map(length($_), @product));
        map {
            printf(" %" . (length(@product)) . "s) %-${maxlen}s  %s\n", $_ + 1, $product[$_], $gConfmkList{$product[$_]});
        } (0 .. $#product);
        print(" NO PRODUCTS FOUND!\n") if !@product;

        print("\nTARGETS\n-------\n");
        #
        if (!@target) {
            @target = grep(s/^== (.+) ==$/$1/, MenuRuncmd("$product PRINTCMD=0 VERBOSE=1 help-target-*-wiki"));
            $tgtind = 0 if ($tgtind > @target);
            $tgtrows = int($#target / $tgtcols + 1);
            my $maxind = 0;
            map {
                if (!($_ % $tgtrows)) {
                    $maxind = length(Min($_ + $tgtrows, $#target + 1)) + 1;
                    $maxlen = Max(map(length(), @target[$_ .. Min($_ + $tgtrows - 1, $#target)]));
                }
                $target[$_] = sprintf("%${maxind}s) %-${maxlen}s", "t" . ($_ + 1), $target[$_]);
            } (0 .. $#target);
        }
        ($target = ($tgtind ? $target[$tgtind - 1] : "")) =~ s/^.+?(\S+)\s*$/$1/;
        foreach my $row (1 .. $tgtrows) {
            foreach my $col (1 .. $tgtcols) {
                my $ind = ($col - 1) * $tgtrows + $row - 1;
                print(($ind < @target ? " $target[$ind]" : "") . ($col != $tgtcols ? " " : "\n"));
            }
        }
        print(" NO TARGETS FOUND!\n") if !@target;

        print("\nCONFIGURATION\n-------------\n");
        #
        print(
          " Product: " . ($prodind ? $product[$prodind - 1] : "NOT SELECTED!") . "\n" .
          " Target : " . ($tgtind ? $target : "NOT SELECTED!") . "\n" .
          " Type   : " . ucfirst($vartype) . "\n" .
          " Debug  : " . ($varudeb ? ($varudeb =~ /full/i ? "Full debug" : "Enabled") : "Disabled") . "\n" .
          " Symbols: " . ($varsym ? "Created\n" : "Not created\n"));

        print("\nOPTIONS\n-------\n");
        #
        print(
          " t) Toggle type between rnd/prd/subcon\n" .
          " u) Toggle debug between urel/udeb/udeb full\n" .
          " s) Toggle symbol creation on/off\n" .
          " r) Reset configuration\n" .
          " h) Print usage information\n" .
          " x) Exit\n\n" .
          "Hit Enter to run: imaker$product$cmdarg TYPE=$vartype USE_UDEB=$varudeb USE_SYMGEN=$varsym $target\n");

        print("\nSelection: ");
        #
        my $input = <STDIN>;
        ($input = (defined($input) ? $input : "?")) =~ s/^\s*(.*?)\s*$/\L$1\E/;

        if ($input =~ /^(\d+)$/ && ($1 > 0) && ($1 <= @product) && ($1 != $prodind)) {
            $prodind = $1;
            ($tgtind, @target) = (0, ());
        }
        elsif ($input =~ /^t(\d+)$/ && ($1 > 0) && ($1 <= @target) && ($1 != $tgtind)) {
            $tgtind = $1;
        }
        elsif ($input eq "t") {
            $vartype = ($vartype =~ /rnd/i ? "prd" : ($vartype =~ /prd/i ? "subcon" : "rnd"));
        }
        elsif ($input eq "u") {
            $varudeb = (!$varudeb ? 1 : ($varudeb !~ /full/i ? "full" : 0));
        }
        elsif ($input eq "s") {
            $varsym = ($varsym ? 0 : 1);
        }
        elsif ($input eq "r") {
            ($prodind, @product) = (0, ());
            ($tgtind, @target)  = (0, ());
            ($vartype, $varudeb, $varsym) = ("rnd", 0, 0);
        }
        elsif ($input eq "h") {
            print("\nTODO: Help");
            sleep(2);
        }
        elsif ($input =~ /^(x|)$/) {
            open(FILE, ">$cfgfile") and
                print(FILE map("$_\n", ($prodind, $tgtind, $vartype, $varudeb, $varsym))) and close(FILE);
            return(("", "menu")) if ($input eq "x");
            $cmdarg = "$product$cmdarg TYPE=$vartype USE_UDEB=$varudeb USE_SYMGEN=$varsym";
            $ENV{IMAKER_CMDARG} = Trim("$cmdarg $target");
            return(($cmdarg, $target eq "" ? "defaultgoals" : $target));
        }
    }
}


###############################################################################
#

sub Install($$$)
{
    my ($clean, $bldinf, $destdir) = @_;
    my $srcdir = GetDirname($bldinf = GetAbsFname($bldinf));
    $destdir = GetAbsDirname($destdir) if $destdir;

    print(($clean ? "\nCleaning" : "\nInstalling") . " `$bldinf'" . ($destdir ? " to `$destdir'\n" : "\n"));

    my $export = 0;
    foreach (grep(!/^\s*\/\//, ReadFile($bldinf, 0))) {
        $export = 1, next if /^\s*PRJ_EXPORTS\s*$/i;
        next if !$export;
        Install($clean, "$srcdir$1", $destdir), next if /^\s*#include\s+"(.+)"\s*$/;
        die("Unknown line `$_'.\n") if !/^\s*(\S+)\s+(.+?)\s*$/;
        my ($src, $dest) = ("$srcdir$1", $2);
        $dest = "$gEpocroot/epoc32$dest" if ($dest =~ s/^\+//);
        $dest .= GetBasename($src) if ($dest =~ s/\s+\/\/$//);
        ($src, $dest) = (GetAbsFname($src), GetAbsFname($dest));
        next if ($destdir && ($dest !~ /^$gEpocroot\/epoc32\/tools\//i));
        $dest = "$destdir/" . GetBasename($dest) if $destdir;
        print(($clean ? "Delete" : "Copy `$src' =>") . " `$dest'\n");
        unlink($dest);
        die("Deletion failed.\n") if ($clean && -e($dest));
        next if $clean;
        File::Path::mkpath(GetDirname($dest));
        File::Copy::copy($src, $dest) or die("Copying failed.\n");
        chmod(0777, $dest);
    }
}


###############################################################################
#

END {
    if (!$gArgv) {
        (my $keepgoing, $gStartmk) = ($gKeepgoing, time() - $gStartmk);
        $gKeepgoing = 1;
        SetLogfile($gLogfiles{__prev__}) if %gLogfiles;
        PrintEnv(0) if $gError;
        die("Command `$gMakecmd' failed in `" . GetAbsDirname(".") . "'.\n")
            if ($gTgterr && !$keepgoing);

        map { UnsubstDrive($_) } sort({$a cmp $b} keys(%gSubstdrv));

        @gIcmd = @gReport;
        (my $report, @gReport) = (2, ());
        my ($maxtlen, $maxvlen, %uniq) = (0, 0, ());
        while (@gIcmd) {
            my ($tgtvar, $durval, $type) = (GetIPar(1), GetIPar(1), GetIPar(1));
            if ($type =~ /^-?\d+$/) {
                push(@gReport, [$tgtvar, $durval, $type]);
                ($maxtlen, %uniq) = (Max($maxtlen, length($tgtvar)), ());
            } else {
                $report = 1, push(@gReport, [$tgtvar, $durval, $type])
                    if ($tgtvar ne "") && !($uniq{"$tgtvar|$durval"}++);
                $maxvlen = Max($maxvlen, length($tgtvar));
            }
        }

        my ($tgtcnt, $warn) = (0, 0);
        DPrint($report, "=" x 79 . "\n" . join("\n", map(@$_[2] =~ /^-?\d+$/ ?
            ($tgtcnt++ ? "-" x 79 . "\n" : "") .
            "Target: " . sprintf("%-${maxtlen}s", @$_[0]) .
            "  Duration: " . Sec2Min(@$_[1] < 0 ? $gStartmk : @$_[1]) .
            "  Status: " . (@$_[2] < 0 ? ($warn = "FAILED") : "OK")
            : sprintf("%-${maxvlen}s", @$_[0]) . " = `@$_[1]'" .
                ((@$_[2] =~ /^[fd]$/i) && !-e(@$_[1]) ? " - DOESN'T EXIST" : ""), @gReport)) .
            (@gReport ? "\n" . "-" x 79 . "\n" : "") .
            "Total duration: " . Sec2Min(time() - $gStarttime) .
            "  Status: " . ($gError && !$keepgoing ? "FAILED" : "OK" .
                ($warn ? " (with keep-going)" : "")) .
            "\n" . "=" x 79 . "\n");

        warn("\$_ has been changed in an uncontrolled manner!\n")
            if !/^default input and pattern-searching space$/;
        CloseLog();
        exit(1) if ($gError && !$keepgoing);
    }
}


__END__ # OF IMAKER.PL
