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

# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

use strict;
use warnings;

package Charmonizer::Build::Makefile;
use File::Find qw();
use FindBin;
use Carp qw( confess );
use Cwd qw( getcwd );

sub new {
    my ( $class, %args ) = @_;

    # Validate args, create object.
    for (qw( dir filename obj_ext exe_ext cc )) {
        defined $args{$_} or confess("Missing required param '$_'");
    }
    my $dir  = $args{dir};
    my $self = bless {
        dir          => $dir,
        filename     => $args{filename},
        obj_ext      => $args{obj_ext},
        exe_ext      => $args{exe_ext},
        cc           => $args{cc},
        extra_cflags => $args{extra_cflags} || '',
        extra_clean  => $args{extra_clean} || '',
    }, $class;

    # Gather source paths, normalized for the target OS.
    my $orig_dir = getcwd();
    chdir($dir);
    -d 'src' or confess("Can't find 'src' directory within '$dir'");
    my ( @h_files, @c_files, @c_tests );
    push @c_files, "charmonize.c";
    File::Find::find(
        {   wanted => sub {
                if (/\.c$/) {
                    if (/^Test/) {
                        push @c_tests, $File::Find::name;
                    }
                    else {
                        push @c_files, $File::Find::name;
                    }
                }
                elsif (/\.h$/) {
                    push @h_files, $File::Find::name;
                }
            },
        },
        'src',
    );
    chdir($orig_dir);
    $self->{c_files} = [ sort map { $self->pathify($_) } @c_files ];
    $self->{h_files} = [ sort map { $self->pathify($_) } @h_files ];
    $self->{c_tests} = [ sort map { $self->pathify($_) } @c_tests ];
    $self->{c_test_cases}
        = [ grep { $_ !~ /Test\.c/ } @{ $self->{c_tests} } ];

    return $self;
}

sub pathify { confess "abstract method" }

sub unixify {
    my ( $self, $path ) = @_;
    $path =~ tr{\\}{/};
    return $path;
}

sub winnify {
    my ( $self, $path ) = @_;
    $path =~ tr{/}{\\};
    return $path;
}

sub objectify {
    my ( $self, $c_file ) = @_;
    $c_file =~ s/\.c$/$self->{obj_ext}/ or die "No match: $c_file";
    return $c_file;
}

sub execify {
    my ( $self, $file ) = @_;
    $file =~ s/.*?(\w+)\.c$/$1$self->{exe_ext}/ or die "No match: $file";
    return $file;
}

sub build_link_command {
    my ( $self, %args ) = @_;
    my $objects = join( " ", @{ $args{objects} } );
    return "\$(CC) \$(CFLAGS) $objects -o $args{target}";
}

sub c2o_rule {
    qq|.c.o:\n\t\$(CC) \$(CFLAGS) -c \$*.c -o \$@|;
}

sub test_block {
    my ( $self, $c_test_case ) = @_;
    my $exe = $self->execify($c_test_case);
    my $obj = $self->objectify($c_test_case);
    my $test_obj
        = $self->pathify( $self->objectify("src/Charmonizer/Test.c") );
    my $link_command = $self->build_link_command(
        objects => [ $obj, $test_obj ],
        target  => '$@',
    );
    return qq|$exe: $test_obj $obj\n\t$link_command|;
}

sub clean_rule { confess "abstract method" }

sub clean_rule_posix {
    qq|clean:\n\trm -f \$(CLEANABLE)|;
}

sub clean_rule_win {
    qq|clean:\n\tCMD /c FOR %i IN (\$(CLEANABLE)) DO IF EXIST %i DEL /F %i|;
}

sub gen_makefile {
    my ( $self, %args ) = @_;
    my ( $h_files, $c_files, $c_tests, $c_test_cases )
        = @$self{qw( h_files c_files c_tests c_test_cases )};

    # Derive chunks of Makefile content.
    my $progname              = $self->execify('charmonize.c');
    my $c2o_rule              = $self->c2o_rule;
    my $progname_link_command = $self->build_link_command(
        objects => ['$(OBJS)'],
        target  => '$(PROGNAME)',
    );
    my $clean_rule  = $self->clean_rule;
    my $objs        = join " ", map { $self->objectify($_) } @$c_files;
    my $test_objs   = join " ", map { $self->objectify($_) } @$c_tests;
    my $test_blocks = join "\n\n",
        map { $self->test_block($_) } @$c_test_cases;
    my $test_execs = join " ", map { $self->execify($_) } @$c_test_cases;
    my $headers = join " ", @$h_files;

    # Write out Makefile content.
    open my $fh, ">", $self->{filename}
        or die "open '$self->{filename}' failed: $!\n";
    my $content = <<EOT;
# GENERATED BY $FindBin::Script: do not hand-edit!!!

# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

CC= $self->{cc}
DEFS=
CFLAGS= -Isrc \$(DEFS) $self->{extra_cflags}
PROGNAME= $progname

TESTS= $test_execs

OBJS= $objs

TEST_OBJS= $test_objs

HEADERS= $headers

CLEANABLE= \$(OBJS) \$(PROGNAME) \$(TEST_OBJS) \$(TESTS) $self->{extra_clean}

$c2o_rule

all: \$(PROGNAME)

\$(PROGNAME): \$(OBJS)
\t$progname_link_command

\$(OBJS) \$(TEST_OBJS): \$(HEADERS)

tests: \$(TESTS)

$test_blocks

$clean_rule

EOT
    print $fh $content;
}

package Charmonizer::Build::Makefile::Posix;
BEGIN { our @ISA = qw( Charmonizer::Build::Makefile ) }

sub new {
    my $class = shift;
    return $class->SUPER::new(
        filename => 'Makefile',
        obj_ext  => '.o',
        exe_ext  => '',
        cc       => 'cc',
        @_
    );
}

sub clean_rule { shift->clean_rule_posix }
sub pathify    { shift->unixify(@_) }

package Charmonizer::Build::Makefile::MSVC;
BEGIN { our @ISA = qw( Charmonizer::Build::Makefile ) }

sub new {
    my $class = shift;
    return $class->SUPER::new(
        filename     => 'Makefile.MSVC',
        obj_ext      => '.obj',
        exe_ext      => '.exe',
        cc           => 'cl',
        extra_cflags => '-nologo -D_CRT_SECURE_NO_WARNINGS',
        extra_clean  => '*.pdb',
        @_
    );
}

sub c2o_rule {
    qq|.c.obj:\n\t\$(CC) \$(CFLAGS) -c \$< -Fo\$@|;
}

sub build_link_command {
    my ( $self, %args ) = @_;
    my $objects = join( " ", @{ $args{objects} } );
    return "link -nologo $objects /OUT:$args{target}";
}

sub pathify    { shift->winnify(@_) }
sub clean_rule { shift->clean_rule_win }

package Charmonizer::Build::Makefile::MinGW;
BEGIN { our @ISA = qw( Charmonizer::Build::Makefile ) }

sub new {
    my $class = shift;
    return $class->SUPER::new(
        filename => 'Makefile.MinGW',
        obj_ext  => '.o',
        exe_ext  => '.exe',
        cc       => 'gcc',
        @_
    );
}

sub pathify    { shift->winnify(@_) }
sub clean_rule { shift->clean_rule_win }

### actual script follows
package main;

my $makefile_posix = Charmonizer::Build::Makefile::Posix->new( dir => '.' );
my $makefile_msvc = Charmonizer::Build::Makefile::MSVC->new( dir => '.' );
my $makefile_mingw = Charmonizer::Build::Makefile::MinGW->new( dir => '.' );
$makefile_posix->gen_makefile;
$makefile_msvc->gen_makefile;
$makefile_mingw->gen_makefile;

__END__

=head1 NAME

gen_charmonizer_makefiles.pl

=head1 SYNOPSIS

    gen_charmonizer_makefiles.pl - keeps the Makefiles in sync with the live tree.

=head1 DESCRIPTION

Be sure to run this code from the charmonizer subdirectory (where the
existing Makefiles live).