blob: e89264599b7b5d701746d45fc2bba67fbe285362 [file] [log] [blame]
# 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 Clownfish::CFC::Build;
our $VERSION = '0.006003';
$VERSION = eval $VERSION;
# In order to find Clownfish::CFC::Perl::Build::Charmonic, look in 'lib'
# and cleanup @INC afterwards.
use lib 'lib';
use base qw( Clownfish::CFC::Perl::Build::Charmonic );
no lib 'lib';
use File::Spec::Functions qw( catfile updir catdir curdir );
use File::Copy qw( move );
use File::Path qw( rmtree );
use File::Find qw( find );
use Config;
use Cwd qw( getcwd );
use Carp;
# Establish the filepaths for various assets. If the file `LICENSE` is found
# in the current working directory, this is a CPAN distribution rather than a
# checkout from version control and things live in different dirs.
my $CHARMONIZER_C;
my $LEMON_DIR;
my $INCLUDE;
my $CFC_SOURCE_DIR;
my $MODULES_DIR;
my $IS_CPAN = -e 'LICENSE';
if ($IS_CPAN) {
$CHARMONIZER_C = 'charmonizer.c';
$INCLUDE = 'include';
$LEMON_DIR = 'lemon';
$CFC_SOURCE_DIR = 'src';
$MODULES_DIR = 'modules';
}
else {
$CHARMONIZER_C = catfile( updir(), 'common', 'charmonizer.c' );
$INCLUDE = catdir( updir(), 'include' );
$LEMON_DIR = catdir( updir(), updir(), 'lemon' );
$CFC_SOURCE_DIR = catdir( updir(), 'src' );
$MODULES_DIR = catdir( updir(), 'modules' );
}
my $CMARK_SOURCE_DIR = catdir( $MODULES_DIR, 'CommonMark', 'src' );
my $LEMON_EXE_PATH = catfile( $LEMON_DIR, "lemon$Config{_exe}" );
my $PPPORT_H_PATH = catfile( $INCLUDE, 'ppport.h' );
__PACKAGE__->add_property('with_system_cmark');
sub new {
my ( $class, %args ) = @_;
$args{c_source} = [ $CFC_SOURCE_DIR ];
$args{include_dirs} ||= [];
my @aux_include = (
$INCLUDE,
$CFC_SOURCE_DIR,
curdir(), # for charmony.h
);
push @{ $args{include_dirs} }, @aux_include;
my $self = $class->SUPER::new(
%args,
recursive_test_files => 1,
charmonizer_params => {
charmonizer_c => $CHARMONIZER_C,
},
);
if ($self->with_system_cmark) {
my %pkg_info = eval {
require ExtUtils::PkgConfig;
ExtUtils::PkgConfig->find('libcmark');
};
if ($@) {
push @{ $self->extra_linker_flags }, '-lcmark';
}
else {
push @{ $self->extra_compiler_flags }, $pkg_info{cflags}
if defined $pkg_info{cflags};
push @{ $self->extra_linker_flags }, $pkg_info{libs}
if defined $pkg_info{libs};
}
}
else {
push @{ $self->c_source }, $CMARK_SOURCE_DIR;
push @{ $self->include_dirs }, $CMARK_SOURCE_DIR;
}
return $self;
}
sub _run_make {
my ( $self, %params ) = @_;
my @command = @{ $params{args} };
my $dir = $params{dir};
my $current_directory = getcwd();
chdir $dir if $dir;
unshift @command, 'CC=' . $self->config('cc');
if ( $self->config('cc') =~ /^cl\b/ ) {
unshift @command, "-f", "Makefile.MSVC";
}
elsif ( $^O =~ /mswin/i ) {
unshift @command, "-f", "Makefile.MinGW";
}
unshift @command, "$Config{make}";
system(@command) and confess("$Config{make} failed");
chdir $current_directory if $dir;
}
# Write ppport.h, which supplies some XS routines not found in older Perls and
# allows us to use more up-to-date XS API while still supporting Perls back to
# 5.8.3.
#
# The Devel::PPPort docs recommend that we distribute ppport.h rather than
# require Devel::PPPort itself, but ppport.h isn't compatible with the Apache
# license.
sub ACTION_ppport {
my $self = shift;
if ( !-e $PPPORT_H_PATH ) {
require Devel::PPPort;
$self->add_to_cleanup($PPPORT_H_PATH);
Devel::PPPort::WriteFile($PPPORT_H_PATH);
}
}
# Build the Lemon parser generator.
sub ACTION_lemon {
my $self = shift;
print "Building the Lemon parser generator...\n\n";
$self->_run_make(
dir => $LEMON_DIR,
args => [],
);
}
# Run all .y files through lemon.
sub ACTION_parsers {
my $self = shift;
$self->depends_on('lemon');
my $y_files = $self->rscan_dir( $CFC_SOURCE_DIR, qr/\.y$/ );
for my $y_file (@$y_files) {
my $c_file = $y_file;
my $h_file = $y_file;
$c_file =~ s/\.y$/.c/ or die "no match";
$h_file =~ s/\.y$/.h/ or die "no match";
next if $self->up_to_date( $y_file, $c_file );
$self->add_to_cleanup( $c_file, $h_file );
my $lemon_report_file = $y_file;
$lemon_report_file =~ s/\.y$/.out/ or die "no match";
$self->add_to_cleanup($lemon_report_file);
system( $LEMON_EXE_PATH, '-c', $y_file ) and die "lemon failed";
}
}
# Run all .l files through flex.
sub ACTION_lexers {
my $self = shift;
my $l_files = $self->rscan_dir( $CFC_SOURCE_DIR, qr/\.l$/ );
# Rerun flex if lemon file changes.
my $y_files = $self->rscan_dir( $CFC_SOURCE_DIR, qr/\.y$/ );
for my $l_file (@$l_files) {
my $c_file = $l_file;
my $h_file = $l_file;
$c_file =~ s/\.l$/.c/ or die "no match";
$h_file =~ s/\.l$/.h/ or die "no match";
next
if $self->up_to_date( [ $l_file, @$y_files ],
[ $c_file, $h_file ] );
system( 'flex', '--nounistd', '-o', $c_file, "--header-file=$h_file", $l_file )
and die "flex failed";
}
}
sub ACTION_code {
my $self = shift;
$self->depends_on(qw( charmony ppport parsers ));
my @flags = $self->split_like_shell($self->charmony("EXTRA_CFLAGS"));
# The flag for the MSVC6 hack contains spaces. Make sure it stays quoted.
@flags = map { /\s/ ? qq{"$_"} : $_ } @flags;
$self->extra_compiler_flags( '-DCFCPERL', @flags );
$self->SUPER::ACTION_code;
}
sub _valgrind_base_command {
return
"PERL_DESTRUCT_LEVEL=2 CLOWNFISH_VALGRIND=1 valgrind "
. "--leak-check=yes "
. "--show-reachable=yes "
. "--dsymutil=yes "
. "--suppressions=../../devel/conf/cfcompiler-perl.supp ";
}
# Run the entire test suite under Valgrind.
#
# For this to work, the test suite must be run under a debugging Perl.
#
# A custom suppressions file will probably be needed -- use your judgment.
# To pass in one or more local suppressions files, provide a comma separated
# list like so:
#
# $ ./Build test_valgrind --suppressions=foo.supp,bar.supp
sub ACTION_test_valgrind {
my $self = shift;
# Debian's debugperl uses the Config.pm of the standard system perl
# so -DDEBUGGING won't be detected.
die "Must be run under a perl that was compiled with -DDEBUGGING"
unless $self->config('ccflags') =~ /-D?DEBUGGING\b/
|| $^X =~ /\bdebugperl\b/;
$self->depends_on('code');
# Unbuffer STDOUT, grab test file names and suppressions files.
$|++;
my $t_files = $self->find_test_files; # not public M::B API, may fail
my $valgrind_command = $self->_valgrind_base_command;
if ( my $local_supp = $self->args('suppressions') ) {
for my $supp ( split( ',', $local_supp ) ) {
$valgrind_command .= "--suppressions=$supp ";
}
}
# Iterate over test files.
my @failed;
for my $t_file (@$t_files) {
# Run test file under Valgrind.
print "Testing $t_file...";
die "Can't find '$t_file'" unless -f $t_file;
my $command = "$valgrind_command $^X -Mblib $t_file 2>&1";
my $output = "\n" . ( scalar localtime(time) ) . "\n$command\n";
$output .= `$command`;
# Screen-scrape Valgrind output, looking for errors and leaks.
if ( $?
or $output =~ /ERROR SUMMARY:\s+[^0\s]/
or $output =~ /definitely lost:\s+[^0\s]/
or $output =~ /possibly lost:\s+[^0\s]/
or $output =~ /still reachable:\s+[^0\s]/ )
{
print " failed.\n";
push @failed, $t_file;
print "$output\n";
}
else {
print " succeeded.\n";
}
}
# If there are failed tests, print a summary list.
if (@failed) {
print "\nFailed "
. scalar @failed . "/"
. scalar @$t_files
. " test files:\n "
. join( "\n ", @failed ) . "\n";
exit(1);
}
}
sub ACTION_dist {
my $self = shift;
# We build our Perl release tarball from a subdirectory rather than from
# the top-level $REPOS_ROOT. Because some assets we need are outside this
# directory, we need to copy them in.
my %to_copy = (
'../../CHANGES' => 'CHANGES',
'../../CONTRIBUTING.md' => 'CONTRIBUTING.md',
'../../LICENSE' => 'LICENSE',
'../../NOTICE' => 'NOTICE',
'../../README.md' => 'README.md',
'../../lemon' => 'lemon',
'../src' => 'src',
'../include' => 'include',
'../modules' => 'modules',
'../common/test/cfbase' => 't/cfbase',
'../common/test/cfclash' => 't/cfclash',
'../common/test/cfext' => 't/cfext',
'../common/test/cfinc' => 't/cfinc',
$CHARMONIZER_C => 'charmonizer.c',
);
print "Copying files...\n";
while (my ($from, $to) = each %to_copy) {
confess("'$to' already exists") if -e $to;
system("cp -R $from $to") and confess("cp failed");
}
move( "MANIFEST", "MANIFEST.bak" ) or die "move() failed: $!";
my $saved = _hide_pod( $self, { 'lib/Clownfish/CFC.pod' => 1 } );
$self->depends_on("manifest");
$self->SUPER::ACTION_dist;
_restore_pod( $self, $saved );
# Now that the tarball is packaged up, delete the copied assets.
rmtree($_) for values %to_copy;
unlink("META.yml");
unlink("META.json");
move( "MANIFEST.bak", "MANIFEST" ) or die "move() failed: $!";
}
# Strip POD from files in the `lib` directory. This is a temporary measure to
# allow us to release Clownfish as a separate dist but with a cloaked API.
sub _hide_pod {
my ( $self, $excluded ) = @_;
my %saved;
find(
{
no_chdir => 1,
wanted => sub {
my $path = $File::Find::name;
return if $excluded->{$path};
return unless $path =~ /\.(pm|pod)$/;
open( my $fh, '<:encoding(UTF-8)', $path )
or confess("Can't open '$path' for reading: $!");
my $content = do { local $/; <$fh> };
close $fh;
if ( $path =~ /\.pod$/ ) {
$saved{$path} = $content;
print "Hiding POD for $path\n";
unlink($path) or confess("Can't unlink '$path': $!");
}
else {
my $copy = $content;
$copy =~ s/^=\w+.*?^=cut\s*$//gsm;
return if $copy eq $content;
print "Hiding POD for $path\n";
$saved{$path} = $content;
open( $fh, '>:encoding(UTF-8)', $path )
or confess("Can't open '$path' for writing: $!");
print $fh $copy;
}
},
},
'lib',
);
return \%saved;
}
# Undo POD hiding.
sub _restore_pod {
my ( $self, $saved ) = @_;
while ( my ( $path, $content ) = each %$saved ) {
open( my $fh, '>:encoding(UTF-8)', $path )
or confess("Can't open '$path' for writing: $!");
print $fh $saved->{$path};
}
}
1;