blob: ca706aeb714a86e8cbdb84b31aadeed2149ff7fb [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::Perl::Build;
use base qw( Module::Build );
our $VERSION = '0.006003';
$VERSION = eval $VERSION;
use File::Spec::Functions qw( catdir catfile curdir updir abs2rel rel2abs );
use File::Path qw( mkpath );
use Config;
use Carp;
# Add a custom Module::Build hashref property to pass additional build
# parameters
if ( $Module::Build::VERSION <= 0.30 ) {
__PACKAGE__->add_property( clownfish_params => {} );
}
else {
# TODO: add sub for property check
__PACKAGE__->add_property(
'clownfish_params',
default => {},
);
}
# Rationale
# When the distribution tarball for the Perl bindings is built, core/, and any
# other needed files/directories are copied into the perl/ directory within the
# main source directory. Then the distro is built from the contents of the
# perl/ directory, leaving out all the files in ruby/, etc. However, during
# development, the files are accessed from their original locations.
my $IS_CPAN_DIST = -d 'core' || -d 'cfcore';
my @BASE_PATH;
push(@BASE_PATH, updir()) unless $IS_CPAN_DIST;
my $AUTOGEN_DIR = 'autogen';
my $LIB_DIR = 'lib';
my $BUILDLIB_DIR = 'buildlib';
sub new {
my $self = shift->SUPER::new( @_ );
# TODO: use Charmonizer to determine whether pthreads are userland.
if ( $Config{osname} =~ /openbsd/i && $Config{usethreads} ) {
my $extra_ldflags = $self->extra_linker_flags;
push @$extra_ldflags, '-lpthread';
$self->extra_linker_flags(@$extra_ldflags);
}
my $autogen_header = $self->clownfish_params('autogen_header');
if ( !defined($autogen_header) ) {
$self->clownfish_params( autogen_header => <<'END_AUTOGEN' );
***********************************************
!!!! DO NOT EDIT !!!!
This file was auto-generated by Build.PL.
***********************************************
END_AUTOGEN
}
return $self;
}
# Return the list of Clownfish include dirs. Uses directories from
# - Module::Build parameter
# - Environment variable CLOWNFISH_INCLUDE
# - Clownfish include directories found in @INC
# Note that this function must be called at build time because dependencies
# might not be installed yet at configure time.
sub cf_include_dirs {
my $self = shift;
my $cf_include = $self->clownfish_params('include');
my @dirs;
@dirs = ref($cf_include) ? @$cf_include : ( $cf_include )
if defined($cf_include);
# Add include dirs from CLOWNFISH_INCLUDE environment variable.
if ($ENV{CLOWNFISH_INCLUDE}) {
push( @dirs, split( /:/, $ENV{CLOWNFISH_INCLUDE} ) );
}
# Add include dirs from @INC.
for my $dir (@INC) {
my $cf_incdir = catdir( $dir, 'Clownfish', '_include' );
push( @dirs, $cf_incdir ) if -d $cf_incdir;
}
return @dirs;
}
# Return the list of Clownfish C include dirs.
# Note that this function must be called at build time because dependencies
# might not be installed yet at configure time.
sub cf_c_include_dirs {
my $self = shift;
my $include_dirs = $self->include_dirs;
my @dirs;
@dirs = ref($include_dirs) ? @$include_dirs : ( $include_dirs )
if defined($include_dirs);
push( @dirs,
curdir(), # for ppport.h
catdir( $AUTOGEN_DIR, 'include' ),
$self->cf_include_dirs,
);
return @dirs;
}
sub cf_base_path {
my $self_or_class = shift;
return @BASE_PATH;
}
sub cf_linker_flags {
my $self = shift;
my $dlext = $Config{dlext};
# Only needed on Windows
return () if $dlext !~ /dll$/i;
# Link against import library on MSVC
my $ext = $Config{cc} =~ /^cl\b/ ? 'lib' : $dlext;
my @linker_flags;
for my $module_name (@_) {
# Find library to link against
my @module_parts = split( '::', $module_name );
my $class_name = $module_parts[-1];
my $lib_file;
my $found;
for my $dir ( catdir( $self->blib, 'arch' ), @INC ) {
$lib_file = catfile(
$dir, 'auto', @module_parts, "$class_name.$ext",
);
if ( -f $lib_file ) {
$found = 1;
last;
}
}
die("No Clownfish library file found for module $module_name")
if !$found;
push( @linker_flags, $lib_file );
}
return @linker_flags;
}
sub _cfh_filepaths {
my $self = shift;
my @paths;
my $source_dirs = $self->clownfish_params('source');
for my $source_dir (@$source_dirs) {
next unless -e $source_dir;
push @paths, @{ $self->rscan_dir( $source_dir, qr/\.cf[hp]$/ ) };
}
return \@paths;
}
sub cf_copy_include_file {
my ($self, @path) = @_;
my $dest_dir = catdir( $self->blib, 'arch', 'Clownfish', '_include' );
for my $include_dir ($self->cf_c_include_dirs) {
my $file = catfile ( $include_dir, @path );
if ( -e $file ) {
$self->copy_if_modified(
from => $file,
to => catfile( $dest_dir, @path ),
);
return;
}
}
die( "Clownfish include file " . catfile(@path) . " not found" );
}
my %hierarchy_cache;
sub _compile_clownfish {
my $self = shift;
require Clownfish::CFC::Model::Hierarchy;
require Clownfish::CFC::Binding::Perl;
require Clownfish::CFC::Binding::Perl::Class;
return @hierarchy_cache{ qw( hierarchy binding ) }
if %hierarchy_cache;
# Compile Clownfish.
my $hierarchy = Clownfish::CFC::Model::Hierarchy->new(
dest => $AUTOGEN_DIR,
);
my $source_dirs = $self->clownfish_params('source');
for my $source_dir (@$source_dirs) {
$hierarchy->add_source_dir($source_dir);
}
for my $include_dir ($self->cf_include_dirs) {
$hierarchy->add_include_dir($include_dir);
}
$hierarchy->build;
$hierarchy->read_host_data_json;
# Process all Binding classes in buildlib.
local @INC = ( @INC, '.' );
my $pm_filepaths = $self->rscan_dir( $BUILDLIB_DIR, qr/\.pm$/ );
for my $pm_filepath (@$pm_filepaths) {
next unless $pm_filepath =~ /Binding/;
require $pm_filepath;
my $package_name = $pm_filepath;
$package_name =~ s/buildlib\/(.*)\.pm$/$1/;
$package_name =~ s/\//::/g;
$package_name->bind_all($hierarchy);
}
my $binding = Clownfish::CFC::Binding::Perl->new(
hierarchy => $hierarchy,
lib_dir => $LIB_DIR,
header => $self->clownfish_params('autogen_header'),
footer => '',
);
@hierarchy_cache{ qw( hierarchy binding ) } = ( $hierarchy, $binding );
return ( $hierarchy, $binding );
}
# Deprecated.
sub ACTION_pod {
my $self = shift;
$self->depends_on('clownfish');
}
sub ACTION_clownfish {
my $self = shift;
$self->add_to_cleanup($AUTOGEN_DIR);
my $modules = $self->clownfish_params('modules');
my @xs_filepaths;
for my $module (@$modules) {
my @module_dir = split( '::', $module->{name} );
my $class_name = pop(@module_dir);
push @xs_filepaths, catfile( $LIB_DIR, @module_dir, "$class_name.xs" );
}
my $buildlib_pm_filepaths = $self->rscan_dir( $BUILDLIB_DIR, qr/\.pm$/ );
my $cfh_filepaths = $self->_cfh_filepaths;
my $log_filepath = catfile( $AUTOGEN_DIR, 'hierarchy.json' );
# Don't bother parsing Clownfish files if everything's up to date.
return
if $self->up_to_date(
[ @$cfh_filepaths, @$buildlib_pm_filepaths ],
[ @xs_filepaths, $log_filepath, ]
);
# Write out all autogenerated files.
print "Parsing Clownfish files...\n";
my ( $hierarchy, $perl_binding ) = $self->_compile_clownfish;
require Clownfish::CFC::Binding::Core;
my $core_binding = Clownfish::CFC::Binding::Core->new(
hierarchy => $hierarchy,
header => $self->clownfish_params('autogen_header'),
footer => '',
);
print "Writing Clownfish autogenerated files...\n";
my $inc_dir = catdir( $self->blib, 'arch', 'Clownfish', '_include' );
my $cfh_modified = $core_binding->write_all_modified;
if ($cfh_modified) {
unlink('typemap');
print "Writing typemap...\n";
$self->add_to_cleanup('typemap');
$perl_binding->write_xs_typemap;
$core_binding->copy_headers($inc_dir);
}
# Rewrite XS if either any .cfh files or relevant .pm files were modified.
my $buildlib_modified = ! $self->up_to_date(
\@$buildlib_pm_filepaths, [ @xs_filepaths, $log_filepath ]
);
if ( $cfh_modified || $buildlib_modified ) {
$self->add_to_cleanup(@xs_filepaths);
$perl_binding->write_host_code;
$perl_binding->write_hostdefs;
for my $module (@$modules) {
$perl_binding->write_bindings(
boot_class => $module->{name},
parcels => $module->{parcels},
);
}
$core_binding->write_host_data_json($inc_dir);
unless ($IS_CPAN_DIST) {
print "Writing POD...\n";
my $pod_files = $perl_binding->write_pod;
$self->add_to_cleanup($_) for @$pod_files;
}
}
# Touch autogenerated files in case the modifications were inconsequential
# and didn't trigger a rewrite, so that we won't have to check them again
# next pass.
for my $xs_filepath (@xs_filepaths) {
if (!$self->up_to_date(
[ @$cfh_filepaths, @$buildlib_pm_filepaths ], $xs_filepath
)
)
{
utime( time, time, $xs_filepath ); # touch
}
}
$hierarchy->write_log;
}
# 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' ) {
require Devel::PPPort;
$self->add_to_cleanup('ppport.h');
Devel::PPPort::WriteFile();
}
}
sub ACTION_compile_custom_xs {
my $self = shift;
$self->depends_on('ppport');
# Make sure all parcels are read and registered.
$self->_compile_clownfish();
my $modules = $self->clownfish_params('modules');
for my $module (@$modules) {
$self->_compile_custom_xs($module);
}
}
sub _compile_custom_xs {
my ( $self, $module ) = @_;
require ExtUtils::CBuilder;
require ExtUtils::ParseXS;
my $module_name = $module->{name};
my @module_parts = split( '::', $module_name );
my @module_dir = @module_parts;
my $class_name = pop(@module_dir);
my $cbuilder = ExtUtils::CBuilder->new( config => $self->config );
my $libdir = catdir( $LIB_DIR, @module_dir );
my $archdir = catdir( $self->blib, 'arch', 'auto', @module_parts );
mkpath( $archdir, 0, 0777 ) unless -d $archdir;
my @objects;
# Make objects.
if ($module->{make_target}) {
my $make_objects = $self->cf_make_objects($module->{make_target});
push @objects, @$make_objects;
}
# Compile C source files.
my $c_files = [];
my $source_dirs = $module->{c_source_dirs} || [];
for my $source_dir (@$source_dirs) {
push @$c_files, @{ $self->rscan_dir( $source_dir, qr/\.c$/ ) };
}
my $autogen_src_dir = catdir( $AUTOGEN_DIR, 'source' );
for my $parcel_name ( @{ $module->{parcels} } ) {
my $parcel = Clownfish::CFC::Model::Parcel->acquire($parcel_name);
my $prefix = $parcel->get_prefix;
# Assume *_parcel.c is built with make.
push @$c_files, catfile( $autogen_src_dir, "${prefix}parcel.c" )
if !$module->{make_target};
push @$c_files, catfile( $autogen_src_dir, "${prefix}perl.c" );
}
my $extra_cflags = $self->clownfish_params('cflags');
for my $c_file (@$c_files) {
my $o_file = $c_file;
my $ccs_file = $c_file;
$o_file =~ s/\.c$/$Config{_o}/ or die "no match";
$ccs_file =~ s/\.c$/.ccs/ or die "no match";
push @objects, $o_file;
next if $self->up_to_date( $c_file, $o_file );
$self->add_to_cleanup($o_file);
$self->add_to_cleanup($ccs_file);
$cbuilder->compile(
source => $c_file,
extra_compiler_flags => $extra_cflags,
include_dirs => [ $self->cf_c_include_dirs ],
object_file => $o_file,
);
}
# .xs => .c
my $xs_filepath = catfile( $libdir, "$class_name.xs" );
my $perl_binding_c_file = catfile( $libdir, "$class_name.c" );
$self->add_to_cleanup($perl_binding_c_file);
if ( !$self->up_to_date( $xs_filepath, $perl_binding_c_file ) ) {
ExtUtils::ParseXS::process_file(
filename => $xs_filepath,
prototypes => 0,
output => $perl_binding_c_file,
);
}
# .c => .o
my $version = $self->dist_version;
my $perl_binding_o_file = catfile( $libdir, "$class_name$Config{_o}" );
unshift @objects, $perl_binding_o_file;
$self->add_to_cleanup($perl_binding_o_file);
if ( !$self->up_to_date( $perl_binding_c_file, $perl_binding_o_file ) ) {
# Don't use Clownfish compiler flags for XS
$cbuilder->compile(
source => $perl_binding_c_file,
extra_compiler_flags => $self->extra_compiler_flags,
include_dirs => [ $self->cf_c_include_dirs ],
object_file => $perl_binding_o_file,
# 'defines' is an undocumented parameter to compile(), so we
# should officially roll our own variant and generate compiler
# flags. However, that involves writing a bunch of
# platform-dependent code, so we'll just take the chance that this
# will break.
defines => {
VERSION => qq|"$version"|,
XS_VERSION => qq|"$version"|,
},
);
}
# Create .bs bootstrap file, needed by Dynaloader.
my $bs_file = catfile( $archdir, "$class_name.bs" );
$self->add_to_cleanup($bs_file);
if ( !$self->up_to_date( $perl_binding_o_file, $bs_file ) ) {
require ExtUtils::Mkbootstrap;
ExtUtils::Mkbootstrap::Mkbootstrap($bs_file);
if ( !-f $bs_file ) {
# Create file in case Mkbootstrap didn't do anything.
open( my $fh, '>', $bs_file )
or confess "Can't open $bs_file: $!";
}
utime( (time) x 2, $bs_file ); # touch
}
# Clean up after CBuilder under MSVC.
$self->add_to_cleanup('compilet*');
$self->add_to_cleanup('*.ccs');
$self->add_to_cleanup( catfile( $libdir, "$class_name.ccs" ) );
$self->add_to_cleanup( catfile( $libdir, "$class_name.def" ) );
$self->add_to_cleanup( catfile( $libdir, "${class_name}_def.old" ) );
$self->add_to_cleanup( catfile( $libdir, "$class_name.exp" ) );
$self->add_to_cleanup( catfile( $libdir, "$class_name.lib" ) );
$self->add_to_cleanup( catfile( $libdir, "$class_name.lds" ) );
$self->add_to_cleanup( catfile( $libdir, "$class_name.base" ) );
# .o => .(a|bundle)
my $lib_file = catfile( $archdir, "$class_name.$Config{dlext}" );
if ( !$self->up_to_date( [ @objects, $AUTOGEN_DIR ], $lib_file ) ) {
my $linker_flags = $self->extra_linker_flags;
my @xs_prereqs = $self->_cf_find_xs_prereqs($module);
push @$linker_flags, $self->cf_linker_flags(@xs_prereqs);
$cbuilder->link(
module_name => $module_name,
objects => \@objects,
lib_file => $lib_file,
extra_linker_flags => $linker_flags,
);
# Install .lib file on Windows
# TODO: Install .dll.a when building with GCC on Windows?
my $implib_file = catfile( $libdir, "$class_name.lib" );
if ( -e $implib_file ) {
$self->copy_if_modified(
from => $implib_file,
to => catfile( $archdir, "$class_name.lib" ),
);
}
}
}
sub _cf_find_xs_prereqs {
my ( $self, $module ) = @_;
my $modules = $self->clownfish_params('modules');
my @xs_prereqs;
for my $parcel_name ( @{ $module->{parcels} } ) {
my $parcel = Clownfish::CFC::Model::Parcel->acquire($parcel_name);
for my $prereq_parcel ( @{ $parcel->prereq_parcels } ) {
my $prereq_module;
if ($prereq_parcel->included) {
$prereq_module = $prereq_parcel->get_xs_module;
}
else {
my $prereq_parcel_name = $prereq_parcel->get_name;
for my $candidate (@$modules) {
my @matches = grep {
$_ eq $prereq_parcel_name
} @{ $candidate->{parcels} };
if (@matches) {
$prereq_module = $candidate->{name};
last;
}
}
}
die("No XS module found for parcel $parcel_name")
if !defined($prereq_module);
push @xs_prereqs, $prereq_module
if $prereq_module ne $module->{name}
&& !grep { $_ eq $prereq_module } @xs_prereqs;
}
}
return @xs_prereqs;
}
sub ACTION_code {
my $self = shift;
$self->depends_on(qw(
clownfish
compile_custom_xs
));
$self->SUPER::ACTION_code;
}
sub ACTION_clean {
my $self = shift;
if ( $self->can('cf_make_clean') ) {
$self->cf_make_clean();
}
$self->SUPER::ACTION_clean;
}
# Monkey patch ExtUtils::CBuilder::Platform::Windows::GCC::format_linker_cmd
# to make extensions work on MinGW.
#
# nwellnhof: The original ExtUtils::CBuilder implementation uses dlltool and a
# strange incremental linking scheme. I think this is only needed for ancient
# versions of GNU ld. It somehow breaks exporting of symbols via
# __declspec(dllexport). Starting with version 2.17, one can pass .def files
# to GNU ld directly, which requires only a single command and gets the
# exports right.
{
no warnings 'redefine';
require ExtUtils::CBuilder::Platform::Windows::GCC;
*ExtUtils::CBuilder::Platform::Windows::GCC::format_linker_cmd = sub {
my ($self, %spec) = @_;
my $cf = $self->{config};
# The Config.pm variable 'libperl' is hardcoded to the full name
# of the perl import library (i.e. 'libperl56.a'). GCC will not
# find it unless the 'lib' prefix & the extension are stripped.
$spec{libperl} =~ s/^(?:lib)?([^.]+).*$/-l$1/;
unshift( @{$spec{other_ldflags}}, '-nostartfiles' )
if ( $spec{startup} && @{$spec{startup}} );
# From ExtUtils::MM_Win32:
#
## one thing for GCC/Mingw32:
## we try to overcome non-relocateable-DLL problems by generating
## a (hopefully unique) image-base from the dll's name
## -- BKS, 10-19-1999
File::Basename::basename( $spec{output} ) =~ /(....)(.{0,4})/;
$spec{image_base} = sprintf( "0x%x0000", unpack('n', $1 ^ $2) );
%spec = $self->write_linker_script(%spec)
if $spec{use_scripts};
foreach my $path ( @{$spec{libpath}} ) {
$path = "-L$path";
}
my @cmds; # Stores the series of commands needed to build the module.
# split off any -arguments included in ld
my @ld = split / (?=-)/, $spec{ld};
push @cmds, [ grep {defined && length} (
@ld ,
'-o', $spec{output} ,
"-Wl,--image-base,$spec{image_base}" ,
@{$spec{lddlflags}} ,
@{$spec{libpath}} ,
@{$spec{startup}} ,
@{$spec{objects}} ,
@{$spec{other_ldflags}} ,
$spec{libperl} ,
@{$spec{perllibs}} ,
$spec{def_file} ,
$spec{map_file} ? ('-Map', $spec{map_file}) : ''
) ];
return @cmds;
};
}
1;
__END__
=head1 NAME
Clownfish::CFC::Perl::Build - Build Clownfish modules.
=head1 DESCRIPTION
Clownfish::CFC::Perl::Build is a subclass of L<Module::Build> which builds
the Perl bindings for Clownfish modules.
=head1 SYNOPSIS
use Clownfish::CFC::Perl::Build;
use File::Spec::Functions qw( catdir );
my @cf_base_path = Clownfish::CFC::Perl::Build->cf_base_path;
my $builder = Clownfish::CFC::Perl::Build->new(
module_name => 'My::Module',
dist_abstract => 'Do something with this and that',
dist_author => 'The Author <author@example.com>',
dist_version => '0.1.0',
clownfish_params => {
source => [ catdir( @cf_base_path, 'core' ) ],
modules => [
{
name => 'My::Module',
c_source_dirs => 'xs',
parcels => [ 'MyModule' ],
},
{
name => 'My::Module::Test',
parcels => [ 'TestMyModule' ],
},
],
},
requires => {
'Other::Module' => '0.3.0',
},
configure_requires => {
'Clownfish::CFC::Perl::Build' => 0.006003,
},
build_requires => {
'Clownfish::CFC::Perl::Build' => 0.006003,
},
);
$builder->create_build_script();
=head1 BUILD ACTIONS
Clownfish::CFC::Perl::Build defines the following build actions.
=head2 code
Build the whole project. The C<code> action searches the C<buildlib>
directory for .pm files whose path contains the string C<Binding>. For each
module found, the class method C<bind_all> will be called with a
L<Clownfish::CFC::Model::Hierarchy> object as argument. This method
should register all the L<Clownfish::CFC::Binding::Perl::Class> objects
for which bindings should be generated.
For example, the file C<buildlib/My/Module/Binding.pm> could look like:
package My::Module::Binding;
sub bind_all {
my ($class, $hierarchy) = @_;
my $binding = Clownfish::CFC::Binding::Perl::Class->new(
class_name => 'My::Module::Class',
);
Clownfish::CFC::Binding::Perl::Class->register($binding);
}
=head2 clownfish
Compile the Clownfish headers and generate code in the C<autogen> directory.
Generate POD from Clownfish headers.
=head1 CONSTRUCTOR
=head2 new( I<[labeled params]> )
my $builder = Clownfish::CFC::Perl::Build->new(%args);
Creates a new Clownfish::CFC::Perl::Build object. C<%args> can contain all
arguments that can be passed to L<Module::Build::API/new>. It accepts an
additional argument C<clownfish_params> which is a hashref with the following
params:
=over
=item *
B<source> - An arrayref of source directories containing the .cfh and .c files
of the module. Defaults to C<[ 'core' ]> or to C<[ '../core' ]> if C<core>
can't be found.
=item *
B<include> - An arrayref of include directories containing .cfh files from
other Clownfish modules needed by the module. Empty by default. Should contain
the Clownfish system include directories if needed.
=item *
B<autogen_header> - A string that will be prepended to the files generated by
the Clownfish compiler.
=item *
B<cflags> - A string with additional compiler flags used to compile the
Clownfish .c files.
=back
=head1 CLASS METHODS
=head2 cf_base_path()
my @path = Clownfish::CFC::Perl::Build->cf_base_path();
Returns the base path components of the source tree where C<core> was found.
Currently either C<()> or C<('..')>.
=head1 METHODS
=head2 cf_copy_include_file( I<[path components]> )
$builder->cf_copy_include_file(@path);
Look for a file with path components C<@path> in all of the Module::Build
include dirs and copy it to C<blib>, so it will be installed in a Clownfish
system include directory. Typically used for additional .h files that the
.cfh files need.
=head2 clownfish_params()
my $value = $builder->clownfish_params($key);
$builder->clownfish_params($key => $value);
Get or set a Clownfish build param. Supports all the parameters that can be
passed to L</new>.
=cut