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