558 lines
13 KiB
Perl
558 lines
13 KiB
Perl
package FFI::Build;
|
|
|
|
use strict;
|
|
use warnings;
|
|
use 5.008001;
|
|
use FFI::Build::File::Library;
|
|
use Carp ();
|
|
use File::Glob ();
|
|
use File::Basename ();
|
|
use List::Util 1.45 ();
|
|
use Capture::Tiny ();
|
|
use File::Path ();
|
|
|
|
# ABSTRACT: Build shared libraries for use with FFI
|
|
our $VERSION = '1.31'; # VERSION
|
|
|
|
|
|
sub _native_name
|
|
{
|
|
my($self, $name) = @_;
|
|
join '', $self->platform->library_prefix, $name, scalar $self->platform->library_suffix;
|
|
}
|
|
|
|
sub new
|
|
{
|
|
my($class, $name, %args) = @_;
|
|
|
|
Carp::croak "name is required" unless defined $name;
|
|
|
|
my $self = bless {
|
|
source => [],
|
|
cflags_I => [],
|
|
cflags => [],
|
|
libs_L => [],
|
|
libs => [],
|
|
alien => [],
|
|
}, $class;
|
|
|
|
my $platform = $self->{platform} = $args{platform} || FFI::Build::Platform->default;
|
|
my $file = $self->{file} = $args{file} || FFI::Build::File::Library->new([$args{dir} || '.', $self->_native_name($name)], platform => $self->platform);
|
|
my $buildname = $self->{buildname} = $args{buildname} || '_build';
|
|
my $verbose = $self->{verbose} = $args{verbose} || 0;
|
|
my $export = $self->{export} = $args{export} || [];
|
|
|
|
if(defined $args{cflags})
|
|
{
|
|
my @flags = ref $args{cflags} ? @{ $args{cflags} } : $self->platform->shellwords($args{cflags});
|
|
push @{ $self->{cflags} }, grep !/^-I/, @flags;
|
|
push @{ $self->{cflags_I} }, grep /^-I/, @flags;
|
|
}
|
|
|
|
if(defined $args{libs})
|
|
{
|
|
my @flags = ref $args{libs} ? @{ $args{libs} } : $self->platform->shellwords($args{libs});
|
|
push @{ $self->{libs} }, grep !/^-L/, @flags;
|
|
push @{ $self->{libs_L} }, grep /^-L/, @flags;
|
|
}
|
|
|
|
if(defined $args{alien})
|
|
{
|
|
my @aliens = ref $args{alien} ? @{ $args{alien} } : ($args{alien});
|
|
foreach my $alien (@aliens)
|
|
{
|
|
unless(eval { $alien->can('cflags') && $alien->can('libs') })
|
|
{
|
|
my $pm = "$alien.pm";
|
|
$pm =~ s/::/\//g;
|
|
require $pm;
|
|
}
|
|
push @{ $self->{alien} }, $alien;
|
|
push @{ $self->{cflags} }, grep !/^-I/, $self->platform->shellwords($alien->cflags);
|
|
push @{ $self->{cflags_I} }, grep /^-I/, $self->platform->shellwords($alien->cflags);
|
|
push @{ $self->{libs} }, grep !/^-L/, $self->platform->shellwords($alien->libs);
|
|
push @{ $self->{libs_L} }, grep /^-L/, $self->platform->shellwords($alien->libs);
|
|
}
|
|
}
|
|
|
|
$self->source(ref $args{source} ? @{ $args{source} } : ($args{source})) if $args{source};
|
|
|
|
$self;
|
|
}
|
|
|
|
|
|
sub buildname { shift->{buildname} }
|
|
sub export { shift->{export} }
|
|
sub file { shift->{file} }
|
|
sub platform { shift->{platform} }
|
|
sub verbose { shift->{verbose} }
|
|
sub cflags { shift->{cflags} }
|
|
sub cflags_I { shift->{cflags_I} }
|
|
sub libs { shift->{libs} }
|
|
sub libs_L { shift->{libs_L} }
|
|
sub alien { shift->{alien} }
|
|
|
|
my @file_classes;
|
|
sub _file_classes
|
|
{
|
|
unless(@file_classes)
|
|
{
|
|
if(defined $FFI::Build::VERSION)
|
|
{
|
|
foreach my $inc (@INC)
|
|
{
|
|
push @file_classes,
|
|
map { my $f = $_; $f =~ s/\.pm$//; "FFI::Build::File::$f" }
|
|
grep !/^Base\.pm$/,
|
|
map { File::Basename::basename($_) }
|
|
File::Glob::bsd_glob(
|
|
File::Spec->catfile($inc, 'FFI', 'Build', 'File', '*.pm')
|
|
);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
# When building out of git without dzil, $VERSION will not
|
|
# usually be defined and any file plugins that require a
|
|
# specific version will break, so we only use core file
|
|
# classes for that.
|
|
push @file_classes, map { "FFI::Build::File::$_" } qw( C CXX Library Object );
|
|
}
|
|
|
|
# also anything already loaded, that might not be in the
|
|
# @INC path (for testing ususally)
|
|
push @file_classes,
|
|
map { my $f = $_; $f =~ s/::$//; "FFI::Build::File::$f" }
|
|
grep !/Base::/,
|
|
grep /::$/,
|
|
keys %{FFI::Build::File::};
|
|
|
|
@file_classes = List::Util::uniq(@file_classes);
|
|
foreach my $class (@file_classes)
|
|
{
|
|
next if(eval { $class->can('new') });
|
|
my $pm = $class . ".pm";
|
|
$pm =~ s/::/\//g;
|
|
require $pm;
|
|
}
|
|
}
|
|
@file_classes;
|
|
}
|
|
|
|
|
|
sub source
|
|
{
|
|
my($self, @file_spec) = @_;
|
|
|
|
foreach my $file_spec (@file_spec)
|
|
{
|
|
if(eval { $file_spec->isa('FFI::Build::File::Base') })
|
|
{
|
|
push @{ $self->{source} }, $file_spec;
|
|
next;
|
|
}
|
|
if(ref $file_spec eq 'ARRAY')
|
|
{
|
|
my($type, $content, @args) = @$file_spec;
|
|
my $class = "FFI::Build::File::$type";
|
|
unless($class->can('new'))
|
|
{
|
|
my $pm = "FFI/Build/File/$type.pm";
|
|
require $pm;
|
|
}
|
|
push @{ $self->{source} }, $class->new(
|
|
$content,
|
|
build => $self,
|
|
platform => $self->platform,
|
|
@args
|
|
);
|
|
next;
|
|
}
|
|
my @paths = File::Glob::bsd_glob($file_spec);
|
|
path:
|
|
foreach my $path (@paths)
|
|
{
|
|
foreach my $class (_file_classes)
|
|
{
|
|
foreach my $regex ($class->accept_suffix)
|
|
{
|
|
if($path =~ $regex)
|
|
{
|
|
push @{ $self->{source} }, $class->new($path, platform => $self->platform, build => $self);
|
|
next path;
|
|
}
|
|
}
|
|
}
|
|
Carp::croak("Unknown file type: $path");
|
|
}
|
|
}
|
|
|
|
@{ $self->{source} };
|
|
}
|
|
|
|
|
|
sub build
|
|
{
|
|
my($self) = @_;
|
|
|
|
my @objects;
|
|
|
|
my $ld = $self->platform->ld;
|
|
|
|
foreach my $source ($self->source)
|
|
{
|
|
if($source->can('build_all'))
|
|
{
|
|
my $count = scalar $self->source;
|
|
if($count == 1)
|
|
{
|
|
return $source->build_all($self->file);
|
|
}
|
|
else
|
|
{
|
|
die "@{[ ref $source ]} has build_all method, but there is not exactly one source";
|
|
}
|
|
}
|
|
|
|
$ld = $source->ld if $source->ld;
|
|
my $output;
|
|
while(my $next = $source->build_item)
|
|
{
|
|
$ld = $next->ld if $next->ld;
|
|
$output = $source = $next;
|
|
}
|
|
push @objects, $output;
|
|
}
|
|
|
|
my $needs_rebuild = sub {
|
|
my(@objects) = @_;
|
|
return 1 unless -f $self->file->path;
|
|
my $target_time = [stat $self->file->path]->[9];
|
|
foreach my $object (@objects)
|
|
{
|
|
my $object_time = [stat "$object"]->[9];
|
|
return 1 if $object_time > $target_time;
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
return $self->file unless $needs_rebuild->(@objects);
|
|
|
|
File::Path::mkpath($self->file->dirname, 0, oct(755));
|
|
|
|
my @cmd = (
|
|
$ld,
|
|
$self->libs_L,
|
|
$self->platform->ldflags,
|
|
(map { "$_" } @objects),
|
|
$self->libs,
|
|
$self->platform->flag_export(@{ $self->export }),
|
|
$self->platform->flag_library_output($self->file->path),
|
|
);
|
|
|
|
my($out, $exit) = Capture::Tiny::capture_merged(sub {
|
|
$self->platform->run(@cmd);
|
|
});
|
|
|
|
if($exit || !-f $self->file->path)
|
|
{
|
|
print $out;
|
|
die "error building @{[ $self->file->path ]} from @objects";
|
|
}
|
|
elsif($self->verbose >= 2)
|
|
{
|
|
print $out;
|
|
}
|
|
elsif($self->verbose >= 1)
|
|
{
|
|
print "LD @{[ $self->file->path ]}\n";
|
|
}
|
|
|
|
$self->file;
|
|
}
|
|
|
|
|
|
sub clean
|
|
{
|
|
my($self) = @_;
|
|
my $dll = $self->file->path;
|
|
unlink $dll if -f $dll;
|
|
foreach my $source ($self->source)
|
|
{
|
|
my $dir = File::Spec->catdir($source->dirname, $self->buildname);
|
|
if(-d $dir)
|
|
{
|
|
unlink $_ for File::Glob::bsd_glob("$dir/*");
|
|
rmdir $dir;
|
|
}
|
|
}
|
|
}
|
|
|
|
1;
|
|
|
|
__END__
|
|
|
|
=pod
|
|
|
|
=encoding UTF-8
|
|
|
|
=head1 NAME
|
|
|
|
FFI::Build - Build shared libraries for use with FFI
|
|
|
|
=head1 VERSION
|
|
|
|
version 1.31
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
use FFI::Platypus;
|
|
use FFI::Build;
|
|
|
|
my $build = FFI::Build->new(
|
|
'frooble',
|
|
source => 'ffi/*.c',
|
|
);
|
|
|
|
# $lib is an instance of FFI::Build::File::Library
|
|
my $lib = $build->build;
|
|
|
|
my $ffi = FFI::Platypus->new( api => 1 );
|
|
# The filename will be platform dependant, but something like libfrooble.so or frooble.dll
|
|
$ffi->lib($lib->path);
|
|
|
|
... # use $ffi to attach functions in ffi/*.c
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
Using libffi based L<FFI::Platypus> is a great alternative to XS for writing library bindings for Perl.
|
|
Sometimes, however, you need to bundle a little C code with your FFI module, but this has never been
|
|
that easy to use. L<Module::Build::FFI> was an early attempt to address this use case, but it uses
|
|
the now out of fashion L<Module::Build>.
|
|
|
|
This module itself doesn't directly integrate with CPAN installers like L<ExtUtils::MakeMaker> or
|
|
L<Module::Build>, but there is a light weight layer L<FFI::Build::MM> that will allow you to easily
|
|
use this module with L<ExtUtils::MakeMaker>. If you are using L<Dist::Zilla> as your dist builder,
|
|
then there is also L<Dist::Zilla::Plugin::FFI::Build>, which will help with the connections.
|
|
|
|
There is some functional overlap with L<ExtUtils::CBuilder>, which was in fact used by L<Module::Build::FFI>.
|
|
For this iteration I have decided not to use that module because although it will generate dynamic libraries
|
|
that can sometimes be used by L<FFI::Platypus>, it is really designed for building XS modules, and trying
|
|
to coerce it into a more general solution has proved difficult in the past.
|
|
|
|
Supported languages out of the box are C, C++ and Fortran. Rust is supported via a language plugin,
|
|
see L<FFI::Platypus::Lang::Rust>.
|
|
|
|
=head1 CONSTRUCTOR
|
|
|
|
=head2 new
|
|
|
|
my $build = FFI::Build->new($name, %options);
|
|
|
|
Create an instance of this class. The C<$name> argument is used when computing the file name for
|
|
the library. The actual name will be something like C<lib$name.so> or C<$name.dll>. The following
|
|
options are supported:
|
|
|
|
=over 4
|
|
|
|
=item alien
|
|
|
|
List of Aliens to compile/link against. L<FFI::Build> will work with any L<Alien::Base> based
|
|
alien, or modules that provide a compatible API.
|
|
|
|
=item buildname
|
|
|
|
Directory name that will be used for building intermediate files, such as object files. This is
|
|
C<_build> by default.
|
|
|
|
=item cflags
|
|
|
|
Extra compiler flags to use. Things like C<-I/foo/include> or C<-DFOO=1>.
|
|
|
|
=item dir
|
|
|
|
The directory where the library will be written. This is C<.> by default.
|
|
|
|
=item export
|
|
|
|
Functions that should be exported (Windows + Visual C++ only)
|
|
|
|
=item file
|
|
|
|
An instance of L<FFI::Build::File::Library> to which the library will be written. Normally not needed.
|
|
|
|
=item libs
|
|
|
|
Extra library flags to use. Things like C<-L/foo/lib -lfoo>.
|
|
|
|
=item platform
|
|
|
|
An instance of L<FFI::Build::Platform>. Usually you want to omit this and use the default instance.
|
|
|
|
=item source
|
|
|
|
List of source files. You can use wildcards supported by C<bsd_glob> from L<File::Glob>.
|
|
|
|
=item verbose
|
|
|
|
By default this class does not print out the actual compiler and linker commands used in building
|
|
the library unless there is a failure. You can alter this behavior with this option. Set to
|
|
one of these values:
|
|
|
|
=over 4
|
|
|
|
=item zero (0)
|
|
|
|
Default, quiet unless there is a failure.
|
|
|
|
=item one (1)
|
|
|
|
Output the operation (compile, link, etc) and the file, but nothing else
|
|
|
|
=item two (2)
|
|
|
|
Output the complete commands run verbatim.
|
|
|
|
=back
|
|
|
|
=back
|
|
|
|
=head1 METHODS
|
|
|
|
=head2 dir
|
|
|
|
my $dir = $build->dir;
|
|
|
|
Returns the directory where the library will be written.
|
|
|
|
=head2 buildname
|
|
|
|
my $builddir = $build->builddir;
|
|
|
|
Returns the build name. This is used in computing a directory to save intermediate files like objects. For example,
|
|
if you specify a file like C<ffi/foo.c>, then the object file will be stored in C<ffi/_build/foo.o> by default.
|
|
C<_build> in this example (the default) is the build name.
|
|
|
|
=head2 export
|
|
|
|
my $exports = $build->export;
|
|
|
|
Returns a array reference of the exported functions (Windows + Visual C++ only)
|
|
|
|
=head2 file
|
|
|
|
my $file = $build->file;
|
|
|
|
Returns an instance of L<FFI::Build::File::Library> corresponding to the library being built. This is
|
|
also returned by the C<build> method below.
|
|
|
|
=head2 platform
|
|
|
|
my $platform = $build->platform;
|
|
|
|
An instance of L<FFI::Build::Platform>, which contains information about the platform on which you are building.
|
|
The default is usually reasonable.
|
|
|
|
=head2 verbose
|
|
|
|
my $verbose = $build->verbose;
|
|
|
|
Returns the verbose flag.
|
|
|
|
=head2 cflags
|
|
|
|
my @cflags = @{ $build->cflags };
|
|
|
|
Returns the compiler flags.
|
|
|
|
=head2 cflags_I
|
|
|
|
my @cflags_I = @{ $build->cflags_I };
|
|
|
|
Returns the C<-I> cflags.
|
|
|
|
=head2 libs
|
|
|
|
my @libs = @{ $build->libs };
|
|
|
|
Returns the library flags.
|
|
|
|
=head2 libs_L
|
|
|
|
my @libs = @{ $build->libs };
|
|
|
|
Returns the C<-L> library flags.
|
|
|
|
=head2 alien
|
|
|
|
my @aliens = @{ $build->alien };
|
|
|
|
Returns a the list of aliens being used.
|
|
|
|
=head2 source
|
|
|
|
$build->source(@files);
|
|
|
|
Add the C<@files> to the list of source files that will be used in building the library.
|
|
The format is the same as with the C<source> attribute above.
|
|
|
|
=head2 build
|
|
|
|
my $lib = $build->build;
|
|
|
|
This compiles the source files and links the library. Files that have already been compiled or linked
|
|
may be reused without recompiling/linking if the timestamps are newer than the source files. An instance
|
|
of L<FFI::Build::File::Library> is returned which can be used to get the path to the library, which can
|
|
be feed into L<FFI::Platypus> or similar.
|
|
|
|
=head2 clean
|
|
|
|
$build->clean;
|
|
|
|
Removes the library and intermediate files.
|
|
|
|
=head1 AUTHOR
|
|
|
|
Author: Graham Ollis E<lt>plicease@cpan.orgE<gt>
|
|
|
|
Contributors:
|
|
|
|
Bakkiaraj Murugesan (bakkiaraj)
|
|
|
|
Dylan Cali (calid)
|
|
|
|
pipcet
|
|
|
|
Zaki Mughal (zmughal)
|
|
|
|
Fitz Elliott (felliott)
|
|
|
|
Vickenty Fesunov (vyf)
|
|
|
|
Gregor Herrmann (gregoa)
|
|
|
|
Shlomi Fish (shlomif)
|
|
|
|
Damyan Ivanov
|
|
|
|
Ilya Pavlov (Ilya33)
|
|
|
|
Petr Pisar (ppisar)
|
|
|
|
Mohammad S Anwar (MANWAR)
|
|
|
|
Håkon Hægland (hakonhagland, HAKONH)
|
|
|
|
Meredith (merrilymeredith, MHOWARD)
|
|
|
|
Diab Jerius (DJERIUS)
|
|
|
|
=head1 COPYRIGHT AND LICENSE
|
|
|
|
This software is copyright (c) 2015,2016,2017,2018,2019,2020 by Graham Ollis.
|
|
|
|
This is free software; you can redistribute it and/or modify it under
|
|
the same terms as the Perl 5 programming language system itself.
|
|
|
|
=cut
|