File Coverage

blib/lib/App/hopen/Gen.pm
Criterion Covered Total %
statement 81 95 85.2
branch 9 24 37.5
condition 1 4 25.0
subroutine 20 27 74.0
pod 9 9 100.0
total 120 159 75.4


line stmt bran cond sub pod time code
1             # App::hopen::Gen - base class for hopen generators
2             package App::hopen::Gen;
3 1     1   537 use Data::Hopen qw(:default *QUIET);
  1         6  
  1         130  
4 1     1   10 use strict; use warnings;
  1     1   2  
  1         19  
  1         4  
  1         3  
  1         21  
5 1     1   10 use Data::Hopen::Base;
  1         9  
  1         7  
6              
7             our $VERSION = '0.000013'; # TRIAL
8              
9 1     1   1273 use parent 'Data::Hopen::Visitor';
  1         3  
  1         6  
10             use Class::Tiny qw(proj_dir dest_dir), {
11             architecture => '',
12              
13             # private
14             _assets => undef, # A Data::Hopen::G::DAG of the assets
15 3         43 _assetop_by_asset => sub { +{} }, # Indexed by refaddr($asset)
16 1     1   1934 };
  1         2  
  1         10  
17              
18 1     1   1245 use App::hopen::Asset;
  1         4  
  1         41  
19 1     1   7 use App::hopen::BuildSystemGlobals;
  1         2  
  1         137  
20 1     1   8 use App::hopen::Util::String qw(eval_here);
  1         2  
  1         54  
21 1     1   8 use Data::Hopen::G::DAG;
  1         2  
  1         46  
22 1     1   5 use Data::Hopen::Util::Data qw(forward_opts);
  1         2  
  1         48  
23 1     1   574 use File::pushd qw(pushd);
  1         1325  
  1         53  
24 1     1   14 use Path::Class ();
  1         3  
  1         20  
25 1     1   9 use Scalar::Util qw(refaddr);
  1         3  
  1         1168  
26              
27             # Docs {{{1
28              
29             =head1 NAME
30              
31             App::hopen::Gen - Base class for hopen generators
32              
33             =head1 SYNOPSIS
34              
35             The code that generates blueprints for specific build systems
36             lives under C<App::hopen::Gen>. L<App::hopen> calls modules
37             under C<App::hopen::Gen> to create the blueprints. Those modules must
38             implement the interface defined here.
39              
40             =head1 ATTRIBUTES
41              
42             =head2 proj_dir
43              
44             (Required) A L<Path::Class::Dir> instance specifying the root directory of
45             the project.
46              
47             =head2 dest_dir
48              
49             (Required) A L<Path::Class::Dir> instance specifying where the generated output
50             (e.g., blueprint or other files) should be written.
51              
52             =head2 _assets (Internal)
53              
54             A L<Data::Hopen::G::DAG> of L<App::hopen::G::AssetOp> instances representing
55             the L<App::Hopen::Asset>s to be created when a build is run.
56              
57             =head1 FUNCTIONS
58              
59             A generator (C<App::hopen::Gen> subclass) is a Visitor plus some.
60              
61             B<Note>:
62             The generator does not have access to L<Data::Hopen::G::Link> instances.
63             That lack of access is the primary distinction between Ops and Links.
64              
65             =cut
66              
67             # }}}1
68              
69             =head2 asset
70              
71             Called by an Op (L<App::hopen::G::Op> subclass) to add an asset
72             (L<App::hopen::G::AssetOp> instance) to the build. Usage:
73              
74             $Generator->asset([-asset=>]$asset, [-from=>]$from[, [-how=>]$how]);
75              
76             If C<$how> is specified, it will be saved in the C<AssetOp> for use later.
77             Later calls with the same asset and a defined C<$how> will overwrite the
78             C<how> value in the C<AssetOp>. Specify 'UNDEF' as the C<$how> to
79             expressly undefine a C<how>.
80              
81             Returns the C<AssetOp>.
82              
83             =cut
84              
85             sub asset {
86 12     12 1 78 my ($self, %args) = getparameters('self', [qw(asset; how)], @_);
87 12     0   1125 hlog { 'Generator adding asset at',refaddr($args{asset}),$args{asset} } 3;
  0         0  
88              
89 12         350 my $existing_op = $self->_assetop_by_asset->{refaddr($args{asset})};
90              
91             # Update an existing op
92 12 50       82 if(defined $existing_op) {
93 0 0 0     0 if( ($args{how}//'') eq 'UNDEF') {
    0          
94 0         0 $existing_op->how(undef);
95             } elsif(defined $args{how}) {
96 0         0 $existing_op->how($args{how});
97             }
98 0         0 return $existing_op;
99             }
100              
101             # Need to create an op. First, load its class.
102 12         50 my $class = $self->_assetop_class;
103              
104 12         60 eval_here <<EOT;
105             require $class;
106             EOT
107 12 50       66 die "$@" if $@;
108              
109             # Create a new op
110 12         51 my $op = $class->new(name => 'Op:<<' . $args{asset}->target . '>>',
111             forward_opts(\%args, qw(asset how)));
112 12         323 $self->_assetop_by_asset->{refaddr($args{asset})} = $op;
113 12         269 $self->_assets->add($op);
114 12         3013 return $op;
115             } #asset()
116              
117             =head2 connect
118              
119             Add a dependency edge between two assets or goals. Any assets must have already
120             been added using L</asset>. Usage:
121              
122             $Generator->connect([-from=>]$from, [-to=>$to]);
123              
124             TODO add missing assets automatically?
125              
126             TODO rename the asset-graph public interface so it's more clear that it's
127             the asset graph and not the command graph.
128              
129             =cut
130              
131             sub connect {
132 12     12 1 141 my ($self, %args) = getparameters('self', [qw(from to)], @_);
133 12         875 my %nodes;
134              
135             # Get the nodes if we were passed assets.
136 12         31 foreach my $field (qw(from to)) {
137 24 100       110 if(eval { $args{$field}->DOES('App::hopen::Asset') }) {
  24         155  
138 15         273 $nodes{$field} = $self->_assetop_by_asset->{refaddr($args{$field})};
139             } else {
140 9         28 $nodes{$field} = $args{$field};
141             }
142             }
143              
144             # TODO better error messages
145 12 50       78 croak "No From node for asset " . refaddr($args{from}) unless $nodes{from};
146 12 50       195 croak "No To node for asset " . refaddr($args{to}) unless $nodes{to};
147 12         337 $self->_assets->connect($nodes{from}, $nodes{to});
148             } #connect()
149              
150             =head2 asset_default_goal
151              
152             Read-only accessor for the default goal of the asset graph
153              
154             =cut
155              
156 4     4 1 81 sub asset_default_goal () { shift->_assets->default_goal }
157              
158             =head2 run_build
159              
160             Runs the build tool for which this generator has created blueprint files.
161             Runs the tool with the destination directory as the current dir.
162              
163             =cut
164              
165             sub run_build {
166 0 0   0 1 0 my $self = shift or croak 'Need an instance';
167 0         0 my $abs_dir = $DestDir->absolute;
168             # NOTE: You have to call this *before* pushd() or chdir(), because
169             # it may be a relative path, and absolute() converts with respect
170             # to cwd at the time of the call.
171 0         0 my $dir = pushd($abs_dir);
172 0 0       0 say "Building in ${abs_dir}..." unless $QUIET;
173 0         0 $self->_run_build();
174             } #run_build()
175              
176             =head2 BUILD
177              
178             Constructor.
179              
180             =cut
181              
182             sub BUILD {
183 6     6 1 772 my ($self, $args) = @_;
184              
185             # Enforce the required argument types
186             croak "Need a project directory (Path::Class::Dir)"
187 6 50       14 unless eval { $self->proj_dir->DOES('Path::Class::Dir') };
  6         163  
188             croak "Need a destination directory (Path::Class::Dir)"
189 6 50       114 unless eval { $self->dest_dir->DOES('Path::Class::Dir') };
  6         131  
190              
191             # Create the asset graph
192 6         79 $self->_assets(hnew DAG => 'asset graph');
193 6         6203 $self->_assets->goal('__R_asset_default_goal');
194             # Create and set default goal
195             } #BUILD()
196              
197             =head1 FUNCTIONS TO BE IMPLEMENTED BY SUBCLASSES
198              
199             =head2 _assetop_class
200              
201             (Required) Returns the name of the L<App::hopen::G::AssetOp> subclass that
202             should be used to represent assets in the C<_assets> graph.
203              
204             =cut
205              
206 0     0   0 sub _assetop_class { ... }
207              
208             =head2 default_toolset
209              
210             (Required) Returns the package stem of the default toolset for this generator.
211              
212             When a hopen file invokes C<use language "Foo">, hopen will load
213             C<< App::hopen::T::<stem>::Foo >>. C<< <stem> >> is the return
214             value of this function unless the user has specified a different toolset.
215              
216             As a sanity check, hopen will first try to load C<< App::hopen::T::<stem> >>,
217             so make sure that is a valid package.
218              
219             =cut
220              
221 0     0 1 0 sub default_toolset { ... }
222              
223             =head2 finalize
224              
225             (Optional)
226             Do whatever the generator wants to do to finish up. By default, no-op.
227             Is provided the L<Data::Hopen::G::DAG> instance as a parameter. Usage:
228              
229             $generator->finalize(-phase=>$Phase, -graph=>$Build,
230             -data=>$data)
231              
232             C<$dag> is the command graph, and C<$data> is the output from the
233             command graph.
234              
235             C<finalize> is always called with named parameters.
236              
237             =cut
238              
239       0 1   sub finalize { }
240              
241             =head2 _run_build
242              
243             (Optional)
244             Implementation of L</run_build>. The default does not die, but does warn().
245              
246             =cut
247              
248             sub _run_build {
249 0     0   0 warn "This generator is not configured to run a build tool. Sorry!";
250             } #_run_build()
251              
252             =head2 visit_goal
253              
254             Add a target corresponding to the name of the goal. Usage:
255              
256             $Generator->visit_goal($node, $node_inputs);
257              
258             This happens while the command graph is being run.
259              
260             This can be overriden by a generator that wants to handle
261             L<Data::Hopen::G::Goal> nodes differently.
262             For example, the generator may want to change the goal's C<outputs>.
263              
264             =cut
265              
266             sub visit_goal {
267 3     3 1 9710 my ($self, %args) = getparameters('self', [qw(goal node_inputs)], @_);
268              
269             # --- Add the goal to the asset graph ---
270              
271             #my $asset_goal = $self->_assets->goal($args{goal}->name);
272             my $phony_asset = App::hopen::Asset->new(
273             target => $args{goal}->name,
274 3         263 made_by => $self,
275             );
276 3         61 my $phony_node = $self->asset(-asset => $phony_asset, -how => '');
277             # \p how defined but falsy => it's a goal
278 3         32 $self->connect($phony_node, $self->asset_default_goal);
279              
280             # Pull the inputs. TODO refactor out the code in common with
281             # AhG::Cmd::input_assets().
282             my $hrSourceFiles =
283 3   50     1279 $args{node_inputs}->find(-name => 'made',
284             -set => '*', -levels => 'local') // {};
285             die 'No input files to goal ' . $args{goal}->name
286 3 50       669 unless scalar keys %$hrSourceFiles;
287              
288 3         12 my $lrSourceFiles = $hrSourceFiles->{(keys %$hrSourceFiles)[0]};
289 3     0   22 hlog { 'found inputs to goal', $args{goal}->name, Dumper($lrSourceFiles) } 2;
  0         0  
290              
291             # TODO? verify that all the assets are actually in the graph first?
292 3         35 $self->connect($_, $phony_node) foreach @$lrSourceFiles;
293              
294             } #visit_goal()
295              
296             =head2 visit_node
297              
298             (Optional)
299             Do whatever the generator wants to do with a L<Data::Hopen::G::Node> that
300             is not a Goal (see L</visit_goal>). By default, no-op. Usage:
301              
302             $generator->visit_node($node)
303              
304             =cut
305              
306       9 1   sub visit_node { }
307              
308             1;
309             __END__
310             # vi: set fdm=marker: #