The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/env perl

use strict;
use warnings;
#use lib qw(/home/agentz/gmake-db/lib /home/agentz/mdom-gmake/lib);

#use Smart::Comments;
#use Smart::Comments '####';
use Getopt::Long;
use Makefile::Parser::GmakeDB;
use IPC::Run3;
use File::Slurp;
use Makefile::AST::Evaluator;
use List::Util 'first';

my $VERSION = $Makefile::Parser::GmakeDB::VERSION;

my @DefaultMakefile = (
    'GNUmakefile',
    'makefile',
    'Makefile'
);

my $user_makefile;
my $print_version;
my ($makefile, $njobs, @goals);
Getopt::Long::Configure ("bundling");
GetOptions(
    'f|file|makefile=s' => \$user_makefile,
    'v|version' => \$print_version,
) or die "Usage: $0 [-f makefile] goals...\n";
### $makefile
### @ARGV

$Makefile::AST::Evaluator::JustPrint = 0;
$Makefile::AST::Evaluator::Quiet = 1;
$Makefile::AST::Evaluator::IgnoreErrors = 1;
$Makefile::AST::Evaluator::AlwaysMake = 1;
$Makefile::AST::Evaluator::Question = 1;

if ($print_version) {
    print <<"_EOC_";
makesimple $VERSION
_EOC_
    exit 0;
}

our $MAKE;
my @var_defs;
for my $arg (@ARGV) {
    if ($arg =~ /(.*?)=(.*)/) {
        my ($var, $value) = ($1, $2);
        if ($var eq 'MAKE') {
            $MAKE = $value;
        }
        push @var_defs, $arg;
    } else {
        push @goals, $arg;
    }
}
if (!defined $MAKE) {
    ($MAKE = $0) =~ s/.*[\\\/]//;
}

$makefile = $user_makefile;
if (!defined $makefile) {
    $makefile = first { -f $_ } @DefaultMakefile;
} elsif ($makefile ne '-' and !-f $makefile) {
    warn "$MAKE: $makefile: No such file or directory\n";
    push @goals, $makefile; # This is required
}

### var defs via command line: @var_defs

my $level = $ENV{MAKESIMPLE_LEVEL};
if (!defined $level) { $level = 0; }
else { $level++ }
#### %ENV
$ENV{MAKELEVEL} = $level;
$ENV{MAKESIMPLE_LEVEL} = $level;

my ($stdout, $stderr);
run3 ['make', '-pqRrs', '-f', $makefile, @var_defs], undef, \$stdout, \$stderr;
## $stderr
my $exit_code = $? >> 8;
if ($stderr and $exit_code == 2 and $stderr !~ /^make:/) {
    $stderr =~ s/^make:/$MAKE:/msg;
    warn $stderr;
    exit $exit_code;
}
if ($stderr =~ /warning: (overriding|ignoring old) commands for target/) {
    warn $stderr;
}

#die "GNU make stdout: $stdout\n";

# XXX debug only
#write_file('/home/agentz/mdom-gmake/make.db', $stdout);

# patch the database output to work around gmake bugs
patch_database(\$stdout);

# XXX debug only
#write_file('/home/agentz/mdom-gmake/make.db.patched', $stdout);

#if ($stdout =~ m{^\s*\./Makefile_\S+\s*:\s*[^\n]*$}ms) {
#    die $&;
#}
#print $stdout;
#exit 0;

$Makefile::AST::Runtime = 0;

my $ast = Makefile::Parser::GmakeDB->parse(\$stdout);
$ast->{makefile} = $makefile;
## $ast
## var a: $ast->get_var('a')
## var b: $ast->get_var('b')
#die;

my $default_goal = $ast->default_goal;
push @goals, $ast->default_goal
    if !@goals && defined $default_goal;
### @goals

if (!@goals && !defined $makefile) {
    warn "$MAKE: *** No targets specified and no makefile found.  Stop.\n";
    exit(2);
}

# XXX uniq @goals?
push @goals, keys %{ $ast->targets }, keys %{ $ast->prereqs };

$ast->add_var(Makefile::AST::Variable->new({
    name => 'MAKE',
    flavor => 'simple',
    value => ['$(MAKE)'],
    origin => 'default',
}));
my $eval = Makefile::AST::Evaluator->new($ast);

my @simple_rules;
my @str_for_default;
my @str_for_others;
Makefile::AST::Evaluator->add_trigger(
    firing_rule => sub {
        my ($self, $rule, $ast_cmds) = @_;
        ### $rule
        ### $ast_cmds
        my $str;
        my $target = $rule->target;
        my $colon = $rule->colon;
        my @normal_prereqs = @{ $rule->normal_prereqs };
        my $normal_prereqs =
            @normal_prereqs ? " @normal_prereqs" : '';
        my @order_prereqs = @{ $rule->order_prereqs };
        my $order_prereqs =
            @order_prereqs ? " | @order_prereqs" : '';
        $str .= $target.$colon.$normal_prereqs.$order_prereqs."\n";
        for my $cmd (@$ast_cmds) {
            $str .= "\t" . $cmd->as_str . "\n";
        }
        if ($target eq $default_goal) {
            push @str_for_default, $str;
        } else {
            push @str_for_others, $str;
        }
    }
);
$eval->set_required_target($user_makefile)
    if defined $user_makefile;
#warn "Default goal: $default_goal\n";
for my $goal (@goals) {
    ### goal: $goal
    $eval->make($goal);
}

print join "\n", @str_for_default, @str_for_others;

# XXX promote the fixes on the GNU make side
sub patch_database {
    my $ref = shift;
    #$$ref =~ s/(\n\S+)#/$1\\#$2/gsm;
    $$ref =~ s/^([^\n]*)(?<!\\)\\(\S[^\n]*\n#  Implicit rule search has)/$1\\\\$2/msg;
    $$ref =~ s/^([^\n]*)(?<!\\)#(\S[^\n]*\n#  Implicit rule search has)/$1\\#$2/msg;
    $$ref =~ s/^([^\n]*)(?<!\\):(\S[^\n]*:\n#  Implicit rule search has)/$1\\:$2/msg;
}
__END__

=head1 NAME

makesimple - De-sugar GNU makefiles to its simplest form using the GmakeDB parser

=head1 SYNOPSIS

    $ makesimple -f myGNUmakefile.mk > simplest.mk

=head1 DESCRIPTION

The makesimple script is a makefile simplifier. It
converts a full-fledged GNU makefile to a highly de-sugared basic
makefile which is almost a call-path tree dump.

=head1 SVN REPOSITORY

For the very latest version of this script, check out the source from

L<http://github.com/agentzh/makefile-parser-pm>.

There is anonymous access to all.

=head1 AUTHOR

Zhang "agentzh" Yichun, C<< <agentzh@gmail.com> >>

=head1 COPYRIGHT AND LICENSE

Copyright (c) 2005-2008 by Zhang "agentzh" Yichun (agentzh).

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=head1 SEE ALSO

L<Makefile::Parser::GmakeDB>, L<Makefile::AST>, L<Makefile::AST::Evaluator>.