1package FFI::Build::MM;
2
3use strict;
4use warnings;
5use 5.008004;
6use Carp ();
7use FFI::Build;
8use JSON::PP ();
9use File::Glob ();
10use File::Basename ();
11use File::Path ();
12use File::Copy ();
13use ExtUtils::MakeMaker 7.12;
14
15# ABSTRACT: FFI::Build installer code for ExtUtils::MakeMaker
16our $VERSION = '1.56'; # VERSION
17
18
19sub new
20{
21  my($class, %opt) = @_;
22
23  my $save = defined $opt{save} ? $opt{save} : 1;
24
25  my $self = bless { save => $save }, $class;
26  $self->load_prop;
27
28  $self;
29}
30
31
32sub mm_args
33{
34  my($self, %args) = @_;
35
36  if($args{DISTNAME})
37  {
38    $self->{prop}->{distname} ||= $args{DISTNAME};
39    $self->{prop}->{share}    ||= "blib/lib/auto/share/dist/@{[ $self->distname ]}";
40    $self->{prop}->{arch}     ||= "blib/arch/auto/@{[ join '/', split /-/, $self->distname ]}";
41    $self->save_prop;
42  }
43  else
44  {
45    Carp::croak "DISTNAME is required";
46  }
47
48
49  if(my $build = $self->build)
50  {
51    foreach my $alien (@{ $build->alien })
52    {
53      next if ref $alien;
54      $args{BUILD_REQUIRES}->{$alien} ||= 0;
55    }
56  }
57
58  if(my $test = $self->test)
59  {
60    foreach my $alien (@{ $test->alien })
61    {
62      next if ref $alien;
63      $args{TEST_REQUIRES}->{$alien} ||= 0;
64    }
65  }
66
67  %args;
68}
69
70sub distname { shift->{prop}->{distname} }
71
72sub sharedir
73{
74  my($self, $new) = @_;
75
76  if(defined $new)
77  {
78    $self->{prop}->{share} = $new;
79    $self->save_prop;
80  }
81
82  $self->{prop}->{share};
83}
84
85sub archdir
86{
87  my($self, $new) = @_;
88
89  if(defined $new)
90  {
91    $self->{prop}->{arch} = $new;
92    $self->save_prop;
93  }
94
95  $self->{prop}->{arch};
96}
97
98sub load_build
99{
100  my($self, $dir, $name, $install) = @_;
101  return unless -d $dir;
102  my($fbx) = File::Glob::bsd_glob("./$dir/*.fbx");
103
104  my $options;
105  my $platform = FFI::Build::Platform->default;
106
107  if($fbx)
108  {
109    $name = File::Basename::basename($fbx);
110    $name =~ s/\.fbx$//;
111    $options = do {
112      package FFI::Build::MM::FBX;
113      our $DIR      = $dir;
114      our $PLATFORM = $platform;
115
116      # make sure we catch all of the errors
117      # code copied from `perldoc -f do`
118      my $return = do $fbx;
119      unless ( $return  ) {
120          Carp::croak( "couldn't parse $fbx: $@" ) if $@;
121          Carp::croak( "couldn't do $fbx: $!" )     unless defined $return;
122          Carp::croak( "couldn't run $fbx" )       unless $return;
123      }
124
125      $return;
126    };
127  }
128  else
129  {
130    $name ||= $self->distname;
131    $options = {
132      source => ["$dir/*.c", "$dir/*.cxx", "$dir/*.cpp"],
133    };
134    # if we see a Go, Rust control file then we assume the
135    # ffi mod is written in that language.
136    foreach my $control_file ("$dir/Cargo.toml", "$dir/go.mod")
137    {
138      if(-f $control_file)
139      {
140        $options->{source} = [$control_file];
141        last;
142      }
143    }
144  }
145
146  $options->{platform} ||= $platform;
147  $options->{dir}      ||= ref $install ? $install->($options) : $install;
148  $options->{verbose}  = 1 unless defined $options->{verbose};
149  FFI::Build->new($name, %$options);
150}
151
152sub build
153{
154  my($self) = @_;
155  $self->{build} ||= $self->load_build('ffi', undef, $self->sharedir . "/lib");
156}
157
158sub test
159{
160  my($self) = @_;
161  $self->{test} ||= $self->load_build('t/ffi', 'test', sub {
162    my($opt) = @_;
163    my $buildname = $opt->{buildname} || '_build';
164    "t/ffi/$buildname";
165  });
166}
167
168sub save_prop
169{
170  my($self) = @_;
171  return unless $self->{save};
172  open my $fh, '>', 'fbx.json';
173  print $fh JSON::PP::encode_json($self->{prop});
174  close $fh;
175}
176
177sub load_prop
178{
179  my($self) = @_;
180  return unless $self->{save};
181  unless(-f 'fbx.json')
182  {
183    $self->{prop} = {};
184    return;
185  }
186  open my $fh, '<', 'fbx.json';
187  $self->{prop} = JSON::PP::decode_json(do { local $/; <$fh> });
188  close $fh;
189}
190
191sub clean
192{
193  my($self) = @_;
194  foreach my $stage (qw( build test ))
195  {
196    my $build = $self->$stage;
197    $build->clean if $build;
198  }
199  unlink 'fbx.json' if -f 'fbx.json';
200}
201
202
203sub mm_postamble
204{
205  my($self) = @_;
206
207  my $postamble = ".PHONY: fbx_build ffi fbx_test ffi-test fbx_clean ffi-clean\n\n";
208
209  # make fbx_realclean ; make clean
210  $postamble .= "realclean :: fbx_clean\n" .
211                "\n" .
212                "fbx_clean ffi-clean:\n" .
213                "\t\$(FULLPERL) -MFFI::Build::MM=cmd -e fbx_clean\n\n";
214
215  # make fbx_build; make
216  $postamble .= "pure_all :: fbx_build\n" .
217                "\n" .
218                "fbx_build ffi:\n" .
219                "\t\$(FULLPERL) -MFFI::Build::MM=cmd -e fbx_build\n\n";
220
221  # make fbx_test; make test
222  $postamble .= "subdirs-test_dynamic subdirs-test_static subdirs-test :: fbx_test\n" .
223                "\n" .
224                "fbx_test ffi-test:\n" .
225                "\t\$(FULLPERL) -MFFI::Build::MM=cmd -e fbx_test\n\n";
226
227  $postamble;
228}
229
230sub action_build
231{
232  my($self) = @_;
233  my $build = $self->build;
234  if($build)
235  {
236    my $lib = $build->build;
237    if($self->archdir)
238    {
239      File::Path::mkpath($self->archdir, 0, oct(755));
240      my $archfile = File::Spec->catfile($self->archdir, File::Basename::basename($self->archdir) . ".txt");
241      open my $fh, '>', $archfile;
242      my $lib_path = $lib->path;
243      $lib_path =~ s/^blib\/lib\///;
244      print $fh "FFI::Build\@$lib_path\n";
245      close $fh;
246    }
247  }
248  return;
249}
250
251sub action_test
252{
253  my($self) = @_;
254  my $build = $self->test;
255  $build->build if $build;
256}
257
258sub action_clean
259{
260  my($self) = @_;
261  my $build = $self->clean;
262  ();
263}
264
265sub import
266{
267  my(undef, @args) = @_;
268  foreach my $arg (@args)
269  {
270    if($arg eq 'cmd')
271    {
272      package main;
273
274      my $mm = sub {
275        my($action) = @_;
276        my $build = FFI::Build::MM->new;
277        $build->$action;
278      };
279
280      no warnings 'once';
281
282      *fbx_build = sub {
283        $mm->('action_build');
284      };
285
286      *fbx_test = sub {
287        $mm->('action_test');
288      };
289
290      *fbx_clean = sub {
291        $mm->('action_clean');
292      };
293    }
294  }
295}
296
2971;
298
299__END__
300
301=pod
302
303=encoding UTF-8
304
305=head1 NAME
306
307FFI::Build::MM - FFI::Build installer code for ExtUtils::MakeMaker
308
309=head1 VERSION
310
311version 1.56
312
313=head1 SYNOPSIS
314
315In your Makefile.PL:
316
317 use ExtUtils::MakeMaker;
318 use FFI::Build::MM;
319
320 my $fbmm = FFI::Build::MM->new;
321
322 WriteMakefile($fbmm->mm_args(
323   ABSTRACT     => 'My FFI extension',
324   DISTNAME     => 'Foo-Bar-Baz-FFI',
325   NAME         => 'Foo::Bar::Baz::FFI',
326   VERSION_FROM => 'lib/Foo/Bar/Baz/FFI.pm',
327   ...
328 ));
329
330 sub MY::postamble {
331   $fbmm->mm_postamble;
332 }
333
334Then put the C, C++ or Fortran files in C<./ffi> for your runtime library
335and C<./t/ffi> for your test time library.
336
337=head1 DESCRIPTION
338
339This module provides a thin layer between L<FFI::Build> and L<ExtUtils::MakeMaker>.
340Its interface is influenced by the design of L<Alien::Build::MM>.  The idea is that
341for your distribution you throw some C, C++ or Fortran source files into a directory
342called C<ffi> and these files will be compiled and linked into a library that can
343be used by your module.  There is a control file C<ffi/*.fbx> which can be used to
344control the compiler and linker options.  (options passed directly into L<FFI::Build>).
345The interface for this file is still under development.
346
347=head1 CONSTRUCTOR
348
349=head2 new
350
351 my $fbmm = FFI::Build::MM->new;
352
353Create a new instance of L<FFI::Build::MM>.
354
355=head1 METHODS
356
357=head2 mm_args
358
359 my %new_args = $fbmm->mm_args(%old_args);
360
361This method does two things:
362
363=over 4
364
365=item reads the arguments to determine sensible defaults (library name, install location, etc).
366
367=item adjusts the arguments as necessary and returns an updated set of arguments.
368
369=back
370
371=head2 mm_postamble
372
373 my $postamble = $fbmm->mm_postamble;
374
375This returns the Makefile postamble used by L<ExtUtils::MakeMaker>.  The synopsis above for
376how to invoke it properly.  It adds the following Make targets:
377
378=over 4
379
380=item fbx_build / ffi
381
382build the main runtime library in C<./ffi>.
383
384=item fbx_test / ffi-test
385
386Build the test library in C<./t/ffi>.
387
388=item fbx_clean / ffi-clean
389
390Clean any runtime or test libraries already built.
391
392=back
393
394Normally you do not need to build these targets manually, they will be built automatically
395at the appropriate stage.
396
397=head1 AUTHOR
398
399Author: Graham Ollis E<lt>plicease@cpan.orgE<gt>
400
401Contributors:
402
403Bakkiaraj Murugesan (bakkiaraj)
404
405Dylan Cali (calid)
406
407pipcet
408
409Zaki Mughal (zmughal)
410
411Fitz Elliott (felliott)
412
413Vickenty Fesunov (vyf)
414
415Gregor Herrmann (gregoa)
416
417Shlomi Fish (shlomif)
418
419Damyan Ivanov
420
421Ilya Pavlov (Ilya33)
422
423Petr Písař (ppisar)
424
425Mohammad S Anwar (MANWAR)
426
427Håkon Hægland (hakonhagland, HAKONH)
428
429Meredith (merrilymeredith, MHOWARD)
430
431Diab Jerius (DJERIUS)
432
433Eric Brine (IKEGAMI)
434
435szTheory
436
437José Joaquín Atria (JJATRIA)
438
439Pete Houston (openstrike, HOUSTON)
440
441=head1 COPYRIGHT AND LICENSE
442
443This software is copyright (c) 2015,2016,2017,2018,2019,2020 by Graham Ollis.
444
445This is free software; you can redistribute it and/or modify it under
446the same terms as the Perl 5 programming language system itself.
447
448=cut
449