File Coverage

blib/lib/Beam/Make.pm
Criterion Covered Total %
statement 104 105 99.0
branch 23 28 82.1
condition 6 7 85.7
subroutine 15 15 100.0
pod 0 1 0.0
total 148 156 94.8


line stmt bran cond sub pod time code
1             package Beam::Make;
2             our $VERSION = '0.001';
3             # ABSTRACT: Recipes to declare and resolve dependencies between things
4              
5             #pod =head1 SYNOPSIS
6             #pod
7             #pod ### container.yml
8             #pod # This Beam::Wire container stores shared objects for our recipes
9             #pod dbh:
10             #pod $class: DBI
11             #pod $method: connect
12             #pod $args:
13             #pod - dbi:SQLite:RECENT.db
14             #pod
15             #pod ### Beamfile
16             #pod # This file contains our recipes
17             #pod # Download a list of recent changes to CPAN
18             #pod RECENT-6h.json:
19             #pod commands:
20             #pod - curl -O https://www.cpan.org/RECENT-6h.json
21             #pod
22             #pod # Parse that JSON file into a CSV using an external program
23             #pod RECENT-6h.csv:
24             #pod requires:
25             #pod - RECENT-6h.json
26             #pod commands:
27             #pod - yfrom json RECENT-6h.json | yq '.recent.[]' | yto csv > RECENT-6h.csv
28             #pod
29             #pod # Build a SQLite database to hold the recent data
30             #pod RECENT.db:
31             #pod $class: Beam::Make::DBI::Schema
32             #pod dbh: { $ref: 'container.yml:dbh' }
33             #pod schema:
34             #pod - table: recent
35             #pod columns:
36             #pod - path: VARCHAR(255)
37             #pod - epoch: DOUBLE
38             #pod - type: VARCHAR(10)
39             #pod
40             #pod # Load the recent data CSV into the SQLite database
41             #pod cpan-recent:
42             #pod $class: Beam::Make::DBI::CSV
43             #pod requires:
44             #pod - RECENT.db
45             #pod - RECENT-6h.csv
46             #pod dbh: { $ref: 'container.yml:dbh' }
47             #pod table: recent
48             #pod file: RECENT-6h.csv
49             #pod
50             #pod ### Load the recent data into our database
51             #pod $ beam make cpan-recent
52             #pod
53             #pod =head1 DESCRIPTION
54             #pod
55             #pod C<Beam::Make> allows an author to describe how to build some thing (a
56             #pod file, some data in a database, an image, a container, etc...) and the
57             #pod relationships between things. This is similar to the classic C<make>
58             #pod program used to build some software packages.
59             #pod
60             #pod Each thing is a C<recipe> and can depend on other recipes. A user runs
61             #pod the C<beam make> command to build the recipes they want, and
62             #pod C<Beam::Make> ensures that the recipe's dependencies are satisfied
63             #pod before building the recipe.
64             #pod
65             #pod This class is a L<Beam::Runnable> object and can be embedded in other
66             #pod L<Beam::Wire> containers.
67             #pod
68             #pod =head2 Recipe Classes
69             #pod
70             #pod Unlike C<make>, C<Beam::Make> recipes can do more than just execute
71             #pod a series of shell scripts. Each recipe is a Perl class that describes
72             #pod how to build the desired thing and how to determine if that thing needs
73             #pod to be rebuilt.
74             #pod
75             #pod These recipe classes come with C<Beam::Make>:
76             #pod
77             #pod =over
78             #pod
79             #pod =item * L<File|Beam::Make::File> - The default recipe class that creates
80             #pod a file using one or more shell commands (a la C<make>)
81             #pod
82             #pod =item * L<DBI|Beam::Make::DBI> - Write data to a database
83             #pod
84             #pod =item * L<DBI::Schema|Beam::Make::DBI::Schema> - Create a database
85             #pod schema
86             #pod
87             #pod =item * L<DBI::CSV|Beam::Make::DBI::CSV> - Load data from a CSV into
88             #pod a database table
89             #pod
90             #pod =back
91             #pod
92             #pod Future recipe class ideas are:
93             #pod
94             #pod =over
95             #pod
96             #pod =item *
97             #pod
98             #pod B<Template rendering>: Files could be generated from a configuration
99             #pod file or database and a template.
100             #pod
101             #pod =item *
102             #pod
103             #pod B<Docker image, container, compose>: A Docker container could depend on
104             #pod a Docker image. When the image is updated, the container would get
105             #pod rebuilt and restarted. The Docker image could depend on a directory and
106             #pod get rebuilt if the directory or its Dockerfile changes.
107             #pod
108             #pod =item *
109             #pod
110             #pod B<System services (init daemon, systemd service, etc...)>: Services
111             #pod could depend on their configuration files (built with a template) and be
112             #pod restarted when their configuration file is updated.
113             #pod
114             #pod =back
115             #pod
116             #pod =head2 Beamfile
117             #pod
118             #pod The C<Beamfile> defines the recipes. To avoid the pitfalls of C<Makefile>, this is
119             #pod a YAML file containing a mapping of recipe names to recipe configuration. Each
120             #pod recipe configuration is a mapping containing the attributes for the recipe class.
121             #pod The C<$class> special configuration key declares the recipe class to use. If no
122             #pod C<$class> is specified, the default L<Beam::Wire::File> recipe class is used.
123             #pod All recipe classes inherit from L<Beam::Class::Recipe> and have the L<name|Beam::Class::Recipe/name>
124             #pod and L<requires|Beam::Class::Recipe/requires> attributes.
125             #pod
126             #pod For examples, see the L<Beam::Wire examples directory on
127             #pod Github|https://github.com/preaction/Beam-Make/tree/master/eg>.
128             #pod
129             #pod =head2 Object Containers
130             #pod
131             #pod For additional configuration, create a L<Beam::Wire> container and
132             #pod reference the objects inside using C<< $ref: "<container>:<service>" >>
133             #pod as the value for a recipe attribute.
134             #pod
135             #pod =head1 TODO
136             #pod
137             #pod =over
138             #pod
139             #pod =item Target names in C<Beamfile> should be regular expressions
140             #pod
141             #pod This would work like Make's wildcard recipes, but with Perl regexp. The
142             #pod recipe object's name is the real name, but the recipe chosen is the one
143             #pod the matches the regexp.
144             #pod
145             #pod =item Environment variables should interpolate into all attributes
146             #pod
147             #pod Right now, the C<< NAME=VALUE >> arguments to C<beam make> only work in
148             #pod recipes that use shell scripts (like L<Beam::Make::File>). It would be
149             #pod nice if they were also interpolated into other recipe attributes.
150             #pod
151             #pod =item Recipes should be able to require wildcards and directories
152             #pod
153             #pod Recipe requirements should be able to depend on patterns, like all
154             #pod C<*.conf> files in a directory. It should also be able to depend on
155             #pod a directory, which would be the same as depending on every file,
156             #pod recursively, in that directory.
157             #pod
158             #pod This would allow rebuilding a ZIP file when something changes, or
159             #pod rebuilding a Docker image when needed.
160             #pod
161             #pod =item Beam::Wire should support the <container>:<service> syntax
162             #pod for references
163             #pod
164             #pod The L<Beam::Wire> class should handle the C<BEAM_PATH> environment
165             #pod variable directly and be able to resolve services from other files
166             #pod without building another C<Beam::Wire> object in the container.
167             #pod
168             #pod =item Beam::Wire should support resolving objects in arbitrary data
169             #pod structures
170             #pod
171             #pod L<Beam::Wire> should have a class method that one can pass in a hash and
172             #pod get back a hash with any C<Beam::Wire> object references resolved,
173             #pod including C<$ref> or C<$class> object.
174             #pod
175             #pod =back
176             #pod
177             #pod =head1 SEE ALSO
178             #pod
179             #pod L<Beam::Wire>
180             #pod
181             #pod =cut
182              
183 2     2   183721 use v5.20;
  2         25  
184 2     2   19 use warnings;
  2         5  
  2         73  
185 2     2   926 use Log::Any qw( $LOG );
  2         16744  
  2         9  
186 2     2   5297 use Moo;
  2         19515  
  2         8  
187 2     2   3916 use experimental qw( signatures postderef );
  2         6916  
  2         18  
188 2     2   1629 use Time::Piece;
  2         19507  
  2         9  
189 2     2   1023 use YAML ();
  2         13962  
  2         52  
190 2     2   1291 use Beam::Wire;
  2         1266848  
  2         92  
191 2     2   20 use Scalar::Util qw( blessed );
  2         4  
  2         104  
192 2     2   12 use List::Util qw( max );
  2         4  
  2         180  
193 2     2   969 use Beam::Make::Cache;
  2         6  
  2         91  
194 2     2   15 use File::stat;
  2         5  
  2         8  
195             with 'Beam::Runnable';
196              
197             has conf => ( is => 'ro', default => sub { YAML::LoadFile( 'Beamfile' ) } );
198             # Beam::Wire container objects
199             has _wire => ( is => 'ro', default => sub { {} } );
200              
201 7     7 0 46814 sub run( $self, @argv ) {
  7         29  
  7         33  
  7         14  
202 7         27 my ( @targets, %vars );
203              
204 7         34 for my $arg ( @argv ) {
205 9 100       53 if ( $arg =~ /^([^=]+)=([^=]+)$/ ) {
206 1         25 $vars{ $1 } = $2;
207             }
208             else {
209 8         26 push @targets, $arg;
210             }
211             }
212              
213 7         35 local @ENV{ keys %vars } = values %vars;
214 7         30 my $conf = $self->conf;
215 7         230 my $cache = Beam::Make::Cache->new;
216              
217             # Targets must be built in order
218             # Prereqs satisfied by original target remain satisfied
219 7         19 my %recipes; # Built recipes
220             my @target_stack;
221             # Build a target (if necessary) and return its last modified date.
222             # Each dependent will be checked against their depencencies' last
223             # modified date to see if they need to be updated
224 15     15   23 my $build = sub( $target ) {
  15         152  
  15         71  
225 15         107 $LOG->debug( "Want to build: $target" );
226 15 50       179 if ( grep { $_ eq $target } @target_stack ) {
  8         32  
227 0         0 die "Recursion at @target_stack";
228             }
229             # If we already have the recipe, it must already have been run
230 15 100       65 if ( $recipes{ $target } ) {
231 1         32 $LOG->debug( "Nothing to do: $target already built" );
232 1         17 return $recipes{ $target }->last_modified;
233             }
234              
235             # If there is no recipe for the target, it must be a source
236             # file. Source files cannot be built, but we do want to know
237             # when they were last modified
238 14 100       45 if ( !$conf->{ $target } ) {
239 1 50       68 $LOG->debug(
240             "$target has no recipe and "
241             . ( -e $target ? 'exists as a file' : 'does not exist as a file' )
242             );
243 1 50       17 return stat( $target )->mtime if -e $target;
244 1         33 die $LOG->errorf( q{No recipe for target "%s" and file does not exist}."\n", $target );
245             }
246              
247             # Resolve any references in the recipe object via Beam::Wire
248             # containers.
249 13         67 my $target_conf = $self->_resolve_ref( $conf->{ $target } );
250 13   100     96 my $class = delete( $target_conf->{ '$class' } ) || 'Beam::Make::File';
251 13         88 $LOG->debug( "Building recipe object $target ($class)" );
252 13         1385 eval "require $class";
253 13         341 my $recipe = $recipes{ $target } = $class->new(
254             $target_conf->%*,
255             name => $target,
256             cache => $cache,
257             );
258              
259 13         1989 my $requires_modified = 0;
260 13 100       84 if ( my @requires = $recipe->requires->@* ) {
261 7         61 $LOG->debug( "Checking requirements for $target: @requires" );
262 7         66 push @target_stack, $target;
263 7         18 for my $require ( @requires ) {
264 7         51 $requires_modified = max $requires_modified, __SUB__->( $require );
265             }
266 7         1166 pop @target_stack;
267             }
268              
269             # Do we need to build this recipe?
270 13 100 100     75 if ( $requires_modified > ( $recipe->last_modified || -1 ) ) {
271 9         544 $LOG->debug( "Building $target" );
272 9         121 $recipe->make( %vars );
273 9         161 $LOG->info( "$target updated" );
274             }
275             else {
276 4         982 $LOG->info( "$target up-to-date" );
277             }
278 13         230 return $recipe->last_modified;
279 7         77 };
280 7         29 $build->( $_ ) for @targets;
281             }
282              
283             # Resolve any references via Beam::Wire container lookups
284 115     115   145 sub _resolve_ref( $self, $conf ) {
  115         157  
  115         159  
  115         128  
285 115 100 66     455 return $conf if !ref $conf || blessed $conf;
286 66 100       224 if ( ref $conf eq 'HASH' ) {
    50          
287 41 100       121 if ( grep { $_ !~ /^\$/ } keys %$conf ) {
  69         214  
288 34         49 my %resolved;
289 34         88 for my $key ( keys %$conf ) {
290 62         12681 $resolved{ $key } = $self->_resolve_ref( $conf->{ $key } );
291             }
292 34         233 return \%resolved;
293             }
294             else {
295             # All keys begin with '$', so this must be a reference
296             # XXX: We should add the 'file:path' syntax to
297             # Beam::Wire directly. We could even call it as a class
298             # method! We should also move BEAM_PATH resolution to
299             # Beam::Wire directly...
300             # A single Beam::Wire->resolve( $conf ) should recursively
301             # resolve the refs in a hash (like this entire subroutine
302             # does), but also allow defining inline objects (with
303             # $class)
304 7         40 my ( $file, $service ) = split /:/, $conf->{ '$ref' }, 2;
305 7         34 my $wire = $self->_wire->{ $file };
306 7 100       18 if ( !$wire ) {
307 1         5 for my $path ( split /:/, $ENV{BEAM_PATH} ) {
308 1 50       43 next unless -e join '/', $path, $file;
309 1         17 $wire = $self->_wire->{ $file } = Beam::Wire->new( file => join '/', $path, $file );
310             }
311             }
312 7         36862 return $wire->get( $service );
313             }
314             }
315             elsif ( ref $conf eq 'ARRAY' ) {
316 25         39 my @resolved;
317 25         74 for my $i ( 0..$#$conf ) {
318 40         124 $resolved[$i] = $self->_resolve_ref( $conf->[$i] );
319             }
320 25         80 return \@resolved;
321             }
322             }
323              
324             1;
325              
326             __END__
327              
328             =pod
329              
330             =head1 NAME
331              
332             Beam::Make - Recipes to declare and resolve dependencies between things
333              
334             =head1 VERSION
335              
336             version 0.001
337              
338             =head1 SYNOPSIS
339              
340             ### container.yml
341             # This Beam::Wire container stores shared objects for our recipes
342             dbh:
343             $class: DBI
344             $method: connect
345             $args:
346             - dbi:SQLite:RECENT.db
347              
348             ### Beamfile
349             # This file contains our recipes
350             # Download a list of recent changes to CPAN
351             RECENT-6h.json:
352             commands:
353             - curl -O https://www.cpan.org/RECENT-6h.json
354              
355             # Parse that JSON file into a CSV using an external program
356             RECENT-6h.csv:
357             requires:
358             - RECENT-6h.json
359             commands:
360             - yfrom json RECENT-6h.json | yq '.recent.[]' | yto csv > RECENT-6h.csv
361              
362             # Build a SQLite database to hold the recent data
363             RECENT.db:
364             $class: Beam::Make::DBI::Schema
365             dbh: { $ref: 'container.yml:dbh' }
366             schema:
367             - table: recent
368             columns:
369             - path: VARCHAR(255)
370             - epoch: DOUBLE
371             - type: VARCHAR(10)
372              
373             # Load the recent data CSV into the SQLite database
374             cpan-recent:
375             $class: Beam::Make::DBI::CSV
376             requires:
377             - RECENT.db
378             - RECENT-6h.csv
379             dbh: { $ref: 'container.yml:dbh' }
380             table: recent
381             file: RECENT-6h.csv
382              
383             ### Load the recent data into our database
384             $ beam make cpan-recent
385              
386             =head1 DESCRIPTION
387              
388             C<Beam::Make> allows an author to describe how to build some thing (a
389             file, some data in a database, an image, a container, etc...) and the
390             relationships between things. This is similar to the classic C<make>
391             program used to build some software packages.
392              
393             Each thing is a C<recipe> and can depend on other recipes. A user runs
394             the C<beam make> command to build the recipes they want, and
395             C<Beam::Make> ensures that the recipe's dependencies are satisfied
396             before building the recipe.
397              
398             This class is a L<Beam::Runnable> object and can be embedded in other
399             L<Beam::Wire> containers.
400              
401             =head2 Recipe Classes
402              
403             Unlike C<make>, C<Beam::Make> recipes can do more than just execute
404             a series of shell scripts. Each recipe is a Perl class that describes
405             how to build the desired thing and how to determine if that thing needs
406             to be rebuilt.
407              
408             These recipe classes come with C<Beam::Make>:
409              
410             =over
411              
412             =item * L<File|Beam::Make::File> - The default recipe class that creates
413             a file using one or more shell commands (a la C<make>)
414              
415             =item * L<DBI|Beam::Make::DBI> - Write data to a database
416              
417             =item * L<DBI::Schema|Beam::Make::DBI::Schema> - Create a database
418             schema
419              
420             =item * L<DBI::CSV|Beam::Make::DBI::CSV> - Load data from a CSV into
421             a database table
422              
423             =back
424              
425             Future recipe class ideas are:
426              
427             =over
428              
429             =item *
430              
431             B<Template rendering>: Files could be generated from a configuration
432             file or database and a template.
433              
434             =item *
435              
436             B<Docker image, container, compose>: A Docker container could depend on
437             a Docker image. When the image is updated, the container would get
438             rebuilt and restarted. The Docker image could depend on a directory and
439             get rebuilt if the directory or its Dockerfile changes.
440              
441             =item *
442              
443             B<System services (init daemon, systemd service, etc...)>: Services
444             could depend on their configuration files (built with a template) and be
445             restarted when their configuration file is updated.
446              
447             =back
448              
449             =head2 Beamfile
450              
451             The C<Beamfile> defines the recipes. To avoid the pitfalls of C<Makefile>, this is
452             a YAML file containing a mapping of recipe names to recipe configuration. Each
453             recipe configuration is a mapping containing the attributes for the recipe class.
454             The C<$class> special configuration key declares the recipe class to use. If no
455             C<$class> is specified, the default L<Beam::Wire::File> recipe class is used.
456             All recipe classes inherit from L<Beam::Class::Recipe> and have the L<name|Beam::Class::Recipe/name>
457             and L<requires|Beam::Class::Recipe/requires> attributes.
458              
459             For examples, see the L<Beam::Wire examples directory on
460             Github|https://github.com/preaction/Beam-Make/tree/master/eg>.
461              
462             =head2 Object Containers
463              
464             For additional configuration, create a L<Beam::Wire> container and
465             reference the objects inside using C<< $ref: "<container>:<service>" >>
466             as the value for a recipe attribute.
467              
468             =head1 TODO
469              
470             =over
471              
472             =item Target names in C<Beamfile> should be regular expressions
473              
474             This would work like Make's wildcard recipes, but with Perl regexp. The
475             recipe object's name is the real name, but the recipe chosen is the one
476             the matches the regexp.
477              
478             =item Environment variables should interpolate into all attributes
479              
480             Right now, the C<< NAME=VALUE >> arguments to C<beam make> only work in
481             recipes that use shell scripts (like L<Beam::Make::File>). It would be
482             nice if they were also interpolated into other recipe attributes.
483              
484             =item Recipes should be able to require wildcards and directories
485              
486             Recipe requirements should be able to depend on patterns, like all
487             C<*.conf> files in a directory. It should also be able to depend on
488             a directory, which would be the same as depending on every file,
489             recursively, in that directory.
490              
491             This would allow rebuilding a ZIP file when something changes, or
492             rebuilding a Docker image when needed.
493              
494             =item Beam::Wire should support the <container>:<service> syntax
495             for references
496              
497             The L<Beam::Wire> class should handle the C<BEAM_PATH> environment
498             variable directly and be able to resolve services from other files
499             without building another C<Beam::Wire> object in the container.
500              
501             =item Beam::Wire should support resolving objects in arbitrary data
502             structures
503              
504             L<Beam::Wire> should have a class method that one can pass in a hash and
505             get back a hash with any C<Beam::Wire> object references resolved,
506             including C<$ref> or C<$class> object.
507              
508             =back
509              
510             =head1 SEE ALSO
511              
512             L<Beam::Wire>
513              
514             =head1 AUTHOR
515              
516             Doug Bell <preaction@cpan.org>
517              
518             =head1 COPYRIGHT AND LICENSE
519              
520             This software is copyright (c) 2020 by Doug Bell.
521              
522             This is free software; you can redistribute it and/or modify it under
523             the same terms as the Perl 5 programming language system itself.
524              
525             =cut