File Coverage

blib/lib/App/hopen.pm
Criterion Covered Total %
statement 317 356 89.0
branch 70 134 52.2
condition 46 120 38.3
subroutine 51 64 79.6
pod 1 1 100.0
total 485 675 71.8


line stmt bran cond sub pod time code
1             # App::hopen: Implementation of the hopen(1) program
2             package App::hopen;
3             our $VERSION = '0.000013'; # TRIAL
4              
5             # Imports {{{1
6 2     2   284648 use strict; use warnings;
  2     2   15  
  2         57  
  2         13  
  2         4  
  2         62  
7 5     2   541 use Data::Hopen::Base;
  5         16578  
  5         21  
8              
9 5     2   3715 use App::hopen::AppUtil ':all';
  5         1983  
  5         269  
10 5     2   2495 use App::hopen::BuildSystemGlobals;
  2         5  
  2         231  
11 5     2   997 use App::hopen::Phases qw(:default phase_idx next_phase);
  2         6  
  2         269  
12 2     2   12 use App::hopen::Util qw(isMYH MYH);
  2         5  
  2         94  
13 2     2   888 use App::hopen::Util::String qw(line_mark_string);
  2         6  
  2         114  
14 2     2   13 use Data::Hopen qw(:default loadfrom *VERBOSE *QUIET);
  2         6  
  2         268  
15 2     2   933 use Data::Hopen::Scope::Hash;
  2         64072  
  2         116  
16 2     2   959 use Data::Hopen::Scope::Environment;
  2         4126  
  2         117  
17 2     2   25 use Data::Hopen::Util::Data qw(dedent forward_opts);
  2         6  
  2         94  
18 2     2   11 use Data::Dumper;
  2         5  
  2         73  
19 2     2   959 use File::Path::Tiny;
  2         2223  
  2         66  
20 2     2   13 use File::stat ();
  2         6  
  2         48  
21 2     2   1604 use Getopt::Long qw(GetOptionsFromArray :config gnu_getopt);
  2         22527  
  2         9  
22 2     2   499 use Hash::Merge;
  2         4  
  2         72  
23 2     2   11 use Path::Class;
  2         8  
  2         119  
24 2     2   19 use Scalar::Util qw(looks_like_number);
  2         5  
  2         165  
25              
26 2     2   145 BEGIN { $Data::Dumper::Indent = 1; } # DEBUG
27              
28             # }}}1
29             # Documentation {{{1
30              
31             =pod
32              
33             =encoding UTF-8
34              
35             =head1 NAME
36              
37             App::hopen - Graph-driven cross-platform build system
38              
39             =head1 CURRENT STATUS
40              
41             Most features are not yet implemented ;) . However it will generate a
42             C<Makefile> or C<build.ninja> file for a C C<Hello, World> program at this
43             point! It can generate command lines for gcc(1) or for Microsoft's C<cl.exe>.
44              
45             =head1 INTRODUCTION
46              
47             hopen is a cross-platform software build generator. It makes files you can
48             pass to Make, Ninja, Visual Studio, or other build tools, to compile and
49             link your software. hopen gives you:
50              
51             =over
52              
53             =item *
54              
55             A full, Turing-complete, robust programming language to write your
56             build scripts (specifically, Perl 5.14+)
57              
58             =item *
59              
60             No hidden magic! All your data is visible and accessible in a build graph
61             (whence "graph-driven").
62              
63             =item *
64              
65             Context-sensitivity. Your users can tweak their own builds for their own
66             platforms without affecting your project.
67              
68             =back
69              
70             See L<App::hopen::Conventions> for details of the input format.
71              
72             Why Perl? Because (1) you probably already have it installed, and
73             (2) it is the original write-once, run-everywhere language!
74              
75             =head2 Example
76              
77             Create a file C<.hopen.pl> in your source tree. Then:
78              
79             $ hopen
80             From ``.'' into ``built''
81             Running Check phase
82              
83             Now C<built/MY.hopen.pl> has been created, and loaded with information about
84             your configuration. You can edit that file if you want to change what will
85             happen next.
86              
87             $ hopen
88             From ``.'' into ``built''
89             Running Gen phase
90              
91             Now C<built/Makefile> has been created.
92              
93             $ hopen --build
94             Building in foo/built
95              
96             And your software is ready to go! C<make> has been run in C<built/>,
97             with output left in C<built/>.
98              
99             See L<App::hopen::Conventions> for information on writing C<.hopen.pl> files.
100              
101             =head1 SYNOPSIS
102              
103             hopen [options] [--] [destination dir [project dir]]
104              
105             If no project directory is specified, the current directory is used.
106              
107             If no destination directory is specified, C<< <project dir>/built >> is used.
108              
109             See L<App::hopen> and L<App::hopen::Conventions> for more details.
110              
111             =head1 OPTIONS
112              
113             =over
114              
115             =item -a C<architecture>
116              
117             Specify the architecture. This is an arbitrary string interpreted by the
118             generator or toolset.
119              
120             =item --build
121              
122             Run the generator to process the blueprint files. Cannot be used
123             with C<--fresh>.
124              
125             =item -e C<Perl code>
126              
127             Add the C<Perl code> as if it were a hopen file. C<-e> files are processed
128             after all other hopen files, so can modify anything that has been set up
129             by those files. Can be specified more than once.
130              
131             =item --fresh
132              
133             Start a fresh build --- ignore any C<MY.hopen.pl> file that may exist in
134             the destination directory. Cannot be used with C<--build>.
135              
136             =item --from C<project dir>
137              
138             Specify the project directory. Overrides a project directory given as a
139             positional argument.
140              
141             =item -g C<generator> (or -G)
142              
143             Specify the generator. The given C<generator> should be either a full package
144             name or the part after C<App::hopen::Gen::>. Also accepts C<-G> to ease
145             the transition from cmake.
146              
147             =item -t C<toolset> (or -T)
148              
149             Specify the toolset. The given C<toolset> should be either a full package
150             name or the part after C<App::hopen::T::>. Also accepts C<-T> to ease
151             the transition from cmake.
152              
153             =item --to C<destination dir>
154              
155             Specify the destination directory. Overrides a destination directory given
156             as a positional argument.
157              
158             =item --phase C<phase>
159              
160             Specify which phase of the process to run. Note that this overrides whatever
161             is specified in any MY.hopen.pl file, so may cause unexpected results!
162              
163             If C<--phase> is given, no other hopen file can set the phase, and hopen will
164             terminate if a file attempts to do so.
165              
166             =item -q
167              
168             Produce no output (quiet). Overrides C<-v>.
169              
170             =item -v, --verbose=n
171              
172             Verbose. Specify more C<v>'s for more verbosity. At present, C<-vv>
173             (equivalently, C<--verbose=2>) gives
174             you detailed traces of the data, and C<-vvv> gives you more detailed
175             code tracebacks on error.
176              
177             =item --version
178              
179             Print the version of hopen and exit
180              
181             =back
182              
183             =head1 INTERNALS
184              
185             After the C<hopen> file is processed, cycles are detected and reported as
186             errors. *(TODO change this to support LaTeX multi-run files?)* Then the DAG
187             is traversed, and each operation writes the necessary information to the
188             file being generated.
189              
190             =head1 INTERNAL DATA
191              
192             =cut
193              
194             # }}}1
195             # Constants {{{1
196              
197 2     2   13 use constant DEBUG => false;
  2         7  
  2         110  
198              
199             # Shell exit codes
200 2     2   14 use constant EXIT_OK => 0; # success
  2         4  
  2         82  
201 2     2   22 use constant EXIT_PROC_ERR => 1; # error during processing
  2         4  
  2         86  
202 2     2   11 use constant EXIT_PARAM_ERR => 2; # couldn't understand the command line
  2         13  
  2         798  
203              
204             # }}}1
205             # Globals (ick) {{{1
206              
207             =head2 C<$RUNNING>
208              
209             Set truthy when a hopen run is in progress. This is so modules don't have
210             to C<die()> if they are being run under C<perl -c>, for example.
211              
212             TODO replace this with a package parameter --- see
213             L<App::hopen::HopenFileKit/_language_import>.
214              
215             =cut
216              
217             our $RUNNING;
218              
219             =head2 C<$_hrData>
220              
221             The hashref of the current data we have built up by processing hopen files.
222              
223             =cut
224              
225             our $_hrData; # the hashref of current data
226              
227             =head2 C<$_did_set_phase>
228              
229             Set to truthy if MY.hopen.pl sets the phase.
230              
231             =cut
232              
233             our $_did_set_phase = false;
234             # Whether the current hopen file called set_phase()
235              
236             =head2 C<$_hf_pkg_idx>
237              
238             Used to give each hopen file or C<-e> a unique package name.
239              
240             =cut
241              
242             my $_hf_pkg_idx = 0; # unique ID for the packages of hopen files
243              
244             # }}}1
245             # === Command line parsing ============================================== {{{1
246              
247             =head2 %CMDLINE_OPTS
248              
249             A hash from internal name to array reference of
250             [getopt-name, getopt-options, optional default-value].
251              
252             If default-value is a reference, it will be the destination for that value.
253              
254             =cut
255              
256             my %CMDLINE_OPTS = (
257             # They are listed in alphabetical order by option name,
258             # lowercase before upper, although the code does not require that order.
259              
260             ARCHITECTURE => ['a','|A|architecture|platform=s'],
261             # -A and --platform are for the comfort of folks migrating from CMake
262              
263             BUILD => ['build'], # If specified, do not
264             # run any phases. Instead, run the
265             # build tool indicated by the generator.
266              
267             #DUMP_VARS => ['d', '|dump-variables', false],
268             #DEBUG => ['debug','', false],
269             DEFINE => ['D',':s%'],
270             EVAL => ['e','|eval=s@'], # Perl source to run as a hopen file
271             #RESTRICTED_EVAL => ['E','|exec=s@'],
272             # TODO add -f to specify additional hopen files
273             FRESH => ['fresh'], # Don't run MY.hopen.pl
274             PROJ_DIR => ['from','=s'],
275              
276             GENERATOR => ['g', '|G|generator=s', 'Make'], # -G is from CMake
277             # *** This is where the default generator is set ***
278             # TODO? add an option to pass parameters to the generator?
279             # E.g., which make(1) to use? Or maybe that should be part of the
280             # ARCHITECTURE string.
281              
282             #GO => ['go'], # TODO implement this --- if specified, run all phases
283             # and invoke the build tool without requiring the user to
284             # re-run hopen.
285              
286             # -h and --help reserved
287             #INCLUDE => ['i','|include=s@'],
288             #LIB => ['l','|load=s@'], # TODO implement this. A separate option
289             # for libs only used for hopen files?
290             #LANGUAGE => ['L','|language:s'],
291             # --man reserved
292             # OUTPUT_FILENAME => ['o','|output=s', ""],
293             # OPTIMIZE => ['O','|optimize'],
294              
295             PHASE => ['phase','=s'], # NO DEFAULT so we can tell if --phase was used
296              
297             QUIET => ['q'],
298             #SANDBOX => ['S','|sandbox',false],
299             #SOURCES reserved
300             TOOLSET => ['t','|T|toolset=s'], # -T is from CMake
301             DEST_DIR => ['to','=s'],
302             # --usage reserved
303             PRINT_VERSION => ['version','', false],
304             VERBOSE => ['v','+', 0],
305             VERBOSE2 => ['verbose',':s'], # --verbose=<n>
306             # -? reserved
307              
308             );
309              
310             =head1 INTERNAL FUNCTIONS
311              
312             =cut
313              
314             sub _parse_command_line { # {{{2
315              
316             =head2 _parse_command_line
317              
318             Takes {into=>hash ref, from=>array ref}. Fills in the hash with the
319             values from the command line, keyed by the keys in L</%CMDLINE_OPTS>.
320              
321             =cut
322              
323 6     6   29 my %params = @_;
324             #local @_Sources;
325              
326 6         16 my $hrOptsOut = $params{into};
327              
328             # Easier syntax for checking whether optional args were provided.
329             # Syntax thanks to http://www.perlmonks.org/?node_id=696592
330 6     18   45 local *have = sub { return exists($hrOptsOut->{ $_[0] }); };
  18         99  
331              
332             # Set defaults so we don't have to test them with exists().
333             %$hrOptsOut = ( # map getopt option name to default value
334 18         69 map { $CMDLINE_OPTS{ $_ }->[0] => $CMDLINE_OPTS{ $_ }[2] }
335 6         39 grep { (scalar @{$CMDLINE_OPTS{ $_ }})==3 }
  84         100  
  84         193  
336             keys %CMDLINE_OPTS
337             );
338              
339             # Get options
340             my $opts_ok = GetOptionsFromArray(
341             $params{from}, # source array
342             $hrOptsOut, # destination hash
343             'usage|?', 'h|help', 'man', # options we handle here
344 6   66     30 map { $_->[0] . ($_->[1] // '') } values %CMDLINE_OPTS, # options strs
  84         282  
345             );
346              
347             # Help, if requested
348 6 50 33     10087 if(!$opts_ok || have('usage') || have('h') || have('man')) {
      66        
      33        
349              
350             # Only pull in the Pod routines if we actually need them.
351              
352             # Terminal formatting, if present.
353             {
354 2     2   18 no warnings 'once';
  2         4  
  2         2334  
  0         0  
355 0         0 eval "require Pod::Text::Termcap";
356 0 50       0 $Pod::Usage::Formatter = 'Pod::Text::Termcap' unless $@;
357             }
358              
359 0         0 require Pod::Usage;
360              
361 0         0 my @in = (-input => __FILE__);
362 0 50       0 Pod::Usage::pod2usage(-verbose => 0, -exitval => EXIT_PARAM_ERR, @in)
363             unless $opts_ok; # unknown opt
364 0 50       0 Pod::Usage::pod2usage(-verbose => 0, -exitval => EXIT_OK, @in)
365             if have('usage');
366 0 0       0 Pod::Usage::pod2usage(-verbose => 1, -exitval => EXIT_OK, @in)
367             if have('h');
368              
369             # --man: suppress "INTERNAL" sections. Note that this does
370             # get rid of the automatic pager we would otherwise get
371             # by virtue of pod2usage's invoking perldoc(1). Oh well.
372              
373 0 50       0 Pod::Usage::pod2usage(
374             -exitval => EXIT_OK, @in, # VVV see Pod::Select for format
375             -verbose => 99, -sections => '!INTERNAL.*'
376             ) if have('man');
377             }
378              
379             # Map the option names from GetOptions back to the internal names we use,
380             # e.g., $hrOptsOut->{EVAL} from $hrOptsOut->{e}.
381 6         31 my %revmap = map { $CMDLINE_OPTS{$_}->[0] => $_ } keys %CMDLINE_OPTS;
  84         193  
382 6         38 for my $optname (keys %$hrOptsOut) {
383 33         87 $hrOptsOut->{ $revmap{$optname} } = $hrOptsOut->{ $optname };
384             }
385              
386             # Process other arguments. The first two non-option arguments are dest
387             # dir and project dir, if --from and --to were not given.
388 6 50 0     12 $hrOptsOut->{DEST_DIR} //= $params{from}->[0] if @{$params{from}};
  6         31  
389 6 50 0     13 $hrOptsOut->{PROJ_DIR} //= $params{from}->[1] if @{$params{from}}>1;
  6         22  
390              
391             # Sanity check VERBOSE2, and give it a default of 0
392 6   50     29 my $v2 = $hrOptsOut->{VERBOSE2} // 0;
393 6 50       22 $v2 = 1 if $v2 eq ''; # --verbose without value === --verbose=1
394 6 50 33     65 die "--verbose requires a positive numeric argument"
      33        
395             if (defined $v2) && ( !looks_like_number($v2) || (int($v2) < 0) );
396 6   50     51 $hrOptsOut->{VERBOSE2} = int($v2 // 0);
397              
398             } #_parse_command_line() }}}2
399              
400             # }}}1
401             # === Main worker code ================================================== {{{1
402              
403              
404             sub _execute_hopen_file { # Load and run a single hopen file {{{2
405              
406             =head2 _execute_hopen_file
407              
408             Execute a single hopen file, but B<do not> run the DAG. Usage:
409              
410             _execute_hopen_file($filename[, options...])
411              
412             This function takes input from L</$_hrData> unless a C<< DATA=>{...} >> option
413             is given. This function updates L</$_hrData> based on the results.
414              
415             Options are:
416              
417             =over
418              
419             =item phase
420              
421             If given, force the phase to be the one specified.
422              
423             =item quiet
424              
425             If truthy, suppress extra output.
426              
427             =item libs
428              
429             If given, it must be an arrayref of directories. Each of those will be
430             turned into a C<use lib> statement (see L<lib>) in the generated source.
431              
432             =back
433              
434             =cut
435              
436 9 50   9   191 my $fn = shift or croak 'Need a file to run';
437 9         31 my %opts = @_;
438 9 50       26 $Phase = $opts{phase} if $opts{phase};
439              
440 9         59 my $merger = Hash::Merge->new('RETAINMENT_PRECEDENT');
441              
442             # == Set up code pieces related to phase control ==
443              
444 9         800 my ($set_phase, $cannot_set_phase, $cannot_set_phase_warn);
445 9         18 my $setting_phase_allowed = false;
446              
447             # Note: all phase-setting functions succeed if there was nothing
448             # for them to do!
449              
450             $set_phase = q(
451             sub can_set_phase { true }
452             sub set_phase {
453             my $new_phase = shift or croak 'Need a phase';
454             return if $App::hopen::BuildSystemGlobals::Phase eq $new_phase;
455             croak "Phase $new_phase is not one of the ones I know about (" .
456             join(', ', @PHASES) . ')'
457             unless defined phase_idx($new_phase);
458             $App::hopen::BuildSystemGlobals::Phase = $new_phase;
459             $App::hopen::_did_set_phase = true;
460             ) .
461 9 50       44 ($opts{quiet} ? '' : 'say "Running $new_phase phase";') . "}\n";
462              
463 9         20 $cannot_set_phase = q(
464             sub can_set_phase { false }
465             sub set_phase {
466             my $new_phase = shift // '';
467             return if $App::hopen::BuildSystemGlobals::Phase eq $new_phase;
468             croak "I'm sorry, but this file (``$FILENAME'') is not allowed to set the phase"
469             }
470             );
471              
472             $cannot_set_phase_warn = q(
473             sub can_set_phase { false }
474             sub set_phase {
475             my $new_phase = shift // '';
476             return if $App::hopen::BuildSystemGlobals::Phase eq $new_phase;
477             ) .
478 9 50       30 ($opts{quiet} ? '' :
479             q(
480             warn "``$FILENAME'': Ignoring attempt to set phase";
481             )
482             ) . "}\n";
483              
484 9         18 my $lib_dirs = '';
485 9 50       26 if($opts{libs}) {
486             $lib_dirs .= "use lib '" . (dir($_)->absolute =~ s/'/\\'/gr) . "';\n"
487 0         0 foreach @{$opts{libs}};
  0         0  
488             }
489              
490             # == Make the hopen file into a package we can eval ==
491              
492 9         15 my ($friendly_name, $pkg_name, $file_text, $phase_text);
493              
494 9         20 $phase_text = q(
495             use App::hopen::Phases ':all';
496             );
497              
498             # -- Load the file
499              
500 9 50       23 if(ref $fn eq 'HASH') { # it's a -e
501 0     0   0 hlog { 'Processing', $fn->{name} };
  0         0  
502 0         0 $file_text = $fn->{text};
503 0         0 $friendly_name = $fn->{name};
504 0         0 $pkg_name = 'CmdLineE' . $fn->{num} . '_' . $_hf_pkg_idx++;
505 0 0       0 $phase_text .= defined($opts{phase}) ? $cannot_set_phase : $set_phase;
506             # -e's can set phase unless --phase was specified
507              
508             } else {
509 9     0   51 hlog { 'Processing', $fn };
  0         0  
510 9         85 $file_text = file($fn)->slurp;
511 9         3319 $pkg_name = ($fn =~ s/[^a-zA-Z0-9]/_/gr) . '_' . $_hf_pkg_idx++;
512 9         231 $friendly_name = $fn;
513              
514 9 100 66     35 if( isMYH($fn) and !defined($opts{phase}) ) {
515             # MY.hopen.pl files can set $Phase unless --phase was given.
516 3         106 $phase_text .= $set_phase;
517 3         8 $setting_phase_allowed = true;
518              
519             } else {
520             # For MY.hopen.pl, when --phase is set, set_phase doesn't croak.
521             # If this were not the case, every second or subsequent run
522             # of hopen(1) would croak if --phase were specified!
523 6 50       26 $phase_text .= isMYH($fn) ? $cannot_set_phase_warn : $cannot_set_phase;
524             # TODO? permit regular hopen files to set the the phase if
525             # neither MYH nor the command line did, and we're at the first
526             # phase. This is so the hopen file can say `set_phase 'Gen';`
527             # if there's nothing to do during Check.
528             }
529             } #endif -e else
530              
531 9         32 $friendly_name =~ s{"}{-}g;
532             # as far as I can tell, #line can't handle embedded quotes.
533              
534             # -- Build the package
535              
536             # TODO move phase-setting to App::hopen::MYhopen
537             # TODO set $App::hopen::MYhopen::IsMYH to indicate whether or not this
538             # file is MY.hopen.pl.
539 9         127 my $src = line_mark_string <<EOT ;
540             {
541             package __Rpkg_$pkg_name;
542 1     1   462 use App::hopen::HopenFileKit "\Q$friendly_name\E";
  1     2   3  
  1     2   7  
  1     2   9  
  1     2   4  
  1     2   8  
  1     1   8  
  1     1   2  
  1     1   6  
  1         9  
  1         3  
  1         9  
  1         9  
  1         2  
  1         7  
  1         7  
  1         2  
  1         6  
  1         8  
  1         2  
  1         10  
  1         8  
  1         3  
  1         7  
  1         7  
  1         3  
  1         7  
543             # \\Q and \\E since, on Windows, \$friendly_name is likely to
544             # include backslashes.
545              
546             # Other lib dirs
547             $lib_dirs
548             # /Other lib dirs
549              
550             # Other phase text
551             $phase_text
552 1     1   133 # /Other phase text
  1     2   3  
  1     2   488  
  1     2   8  
  1     2   8  
  1     2   511  
  1     1   6  
  1     1   3  
  1     1   218  
  1         6  
  1         3  
  1         247  
  1         6  
  1         3  
  1         473  
  1         7  
  1         2  
  1         220  
  1         6  
  1         3  
  1         240  
  1         6  
  1         3  
  1         487  
  1         7  
  1         4  
  1         212  
553             EOT
554              
555             # Now shadow $Phase so the hopen file can't change it without
556   50 33       # really trying! Note that we actually interpolate the current
    50 33        
      33        
      0        
      33        
      33        
557   50         # phase in as a literal so that it's read-only (see perlmod).
    50          
    0          
    50          
    50          
    0          
    50          
558   50          
    50          
559 9 100       30 unless($setting_phase_allowed) {
560 6         28 $src .= line_mark_string <<EOT;
561             our \$Phase;
562             local *Phase = \\"$Phase";
563             EOT
564             }
565              
566             # Run the code given in the hopen file. Wrap it in a named BLOCK so that
567             # Phases::on() will work, but don't rely on the return value of that
568             # BLOCK (per perlsyn).
569              
570 9         57 $src .= line_mark_string <<EOT;
571              
572             sub __Rsub_$pkg_name {
573             my \$__R_retval;
574             __R_DO: {
575             \$__R_retval = do { # return statements in here will exit the Rsub
576             #line 1 "$friendly_name"
577             $file_text
578             }; # do{}
579             } #__R_DO
580             EOT
581              
582             # If the file_text did not expressly return(), control will reach the
583             # following block, where we get the correct return value. If the file_text
584             # ran to completion, we have a defined __R_retval. If the file text exited
585             # via Phases::on(), we have a defined __R_on_result. If either of those
586             # is defined, make sure it's not a DAG or GraphBuilder. Those should not
587             # be put into the return data.
588             #
589             # Also, any defined, non-hash value is ignored, so that we don't
590             # wind up with lots of hashes like { 1 => 1 }.
591             #
592             # If the file_text did expressly return(), whatever it returned will
593             # be used as-is. Like perlref says, we are not totalitarians.
594              
595 9         61 $src .= line_mark_string <<EOT;
596     33       \$__R_retval //= \$__R_on_result;
      33        
      33        
597              
598             ## hlog { '__Rpkg_$pkg_name retval before checks',
599             ## ref \$__R_retval, Dumper \$__R_retval} 3;
600              
601     0       if(defined(\$__R_retval) && ref(\$__R_retval)) {
      0        
      0        
602             die 'Hopen files may not return graphs'
603             if eval { \$__R_retval->DOES('Data::Hopen::G::DAG') };
604             die 'Hopen files may not return graph builders (is a ->goal or ->default_goal missing?)'
605             if eval { \$__R_retval->DOES('Data::Hopen::G::GraphBuilder') };
606              
607             }
608              
609     33       if(defined(\$__R_retval) and ref \$__R_retval ne 'HASH') {
      33        
610     33       warn ('Hopen files must return hashrefs; ignoring ' .
      33        
611             lc(ref(\$__R_retval) || 'scalar')) unless \$QUIET;
612             \$__R_retval = undef;
613             }
614              
615             return \$__R_retval;
616             } #__Rsub_$pkg_name
617              
618             our \$hrNewData = __Rsub_$pkg_name(\$App::hopen::_hrData);
619             } #package
620             EOT
621             # Put the result in a package variable because that way I don't have
622             # to remember the rules for the return value of eval().
623              
624 9     0   75 hlog { "Source for $fn\n", $src, "\n" } 3;
  0         0  
625              
626             # == Run the package ==
627              
628 9         795 eval($src);
629 9 50       69 die "Error in $friendly_name: $@" if $@;
630              
631             # Get the data from the package we just ran
632 9         18 my $hrAddlData = eval {
633 2     2   16 no strict 'refs';
  2         7  
  2         5255  
634 9         14 ${ "__Rpkg_$pkg_name\::hrNewData" }
  9         49  
635             };
636              
637 9     0   67 hlog { 'old data', Dumper($_hrData) } 3;
  0         0  
638 9     0   107 hlog { 'new data', Dumper($hrAddlData) } 2;
  0         0  
639              
640             # TODO? Remove all __R* hash keys from $hrAddlData unless it's a
641             # MY.hopen.pl file?
642              
643             # == Merge in the data ==
644              
645 9 50       95 $_hrData = $merger->merge($_hrData, $hrAddlData) if $hrAddlData;
646 9     0   1365 hlog { 'data after merge', Dumper($_hrData) } 2;
  0         0  
647              
648             } #_execute_hopen_file() }}}2
649              
650             sub _run_phase { # Run a single phase. {{{2
651              
652             =head2 _run_phase
653              
654             Run a phase by executing the hopen files and running the DAG.
655             Reads from and writes to L</$_hrData>, which must be initialized by
656             the caller. Usage:
657              
658             my $hrDagOutput = _run_phase(files=>[...][, options...])
659              
660             Options C<phase>, C<quiet>, and C<libs> are as L</_execute_hopen_file>.
661             Other options are:
662              
663             =over
664              
665             =item files
666              
667             (Required) An arrayref of filenames to run
668              
669             =item norun
670              
671             (Optional) if truthy, do not run the DAG. Note that the DAG will also not
672             be run if it is empty.
673              
674             =back
675              
676             =cut
677              
678 6     6   154 my %opts = @_;
679 6 50       22 $Phase = $opts{phase} if $opts{phase};
680 6         12 my $lrHopenFiles = $opts{files};
681 6 50       21 croak 'Need files=>[...]' unless ref $lrHopenFiles eq 'ARRAY';
682 6     0   30 hlog { Phase => $Phase, Running => Dumper($lrHopenFiles) };
  0         0  
683              
684             # = Process the files ======================================
685              
686 6         49 foreach my $fn (@$lrHopenFiles) {
687 6         19 _execute_hopen_file($fn,
688             forward_opts(\%opts, qw(phase quiet libs))
689             );
690             } # foreach hopen file
691              
692 0 50   0   0 hlog { 'Graph is', ($Build->empty ? 'empty.' : 'not empty.'),
693 6         414 ' Final data is', Dumper($_hrData) } 2;
694              
695 6     0   70 hlog { 'Build graph', '' . $Build->_graph } 5;
  0         0  
696 7     0   59 hlog { Data::Dumper->new([$Build], ['$Build'])->Indent(1)->Dump } 9;
  1         3  
697              
698             # If there is no build graph, just return the data. This is useful
699             # enough for debugging that I am making it documented behaviour.
700              
701 7 100 66     64 return $_hrData if $Build->empty or $opts{norun};
702              
703             # = Execute the resulting build graph ======================
704              
705             # Wrap the final data in a Scope
706 4         289 my $env = Data::Hopen::Scope::Environment->new(name => 'outermost');
707 4         220 my $scope = Data::Hopen::Scope::Hash->new(name => 'from hopen files');
708 4         204 $scope->adopt_hash($_hrData);
709 3         181 $scope->outer($env); # make the environment accessible...
710 3         74 $scope->local(true); # ... but not copied by local-scope calls.
711              
712             # Run the DAG
713 3         46 my $result_data = $Build->run(-context => $scope, -visitor => $Generator);
714 4     0   1744 hlog { Data::Dumper->new([$result_data], ['Build graph result data'])->Indent(1)->Dump } 2;
  1         4  
715 4         38 return $result_data;
716             } #_run_phase() }}}2
717              
718             sub _inner { # Run a single invocation of hopen(1). {{{2
719              
720             =head2 _inner
721              
722             Do the work for one invocation of hopen(1). Dies on failure. Main() then
723             translates the die() into a print and error return.
724              
725             Takes a hash of options.
726              
727             The return value of _inner is unspecified and ignored.
728              
729             =cut
730              
731 7     7   49 my %opts = @_;
732 7         21 local $_hrData = {};
733              
734             # TODO FIXME. This is a bit of a hack: Reset global variables on --fresh.
735             # Instead, App::hopen should be a class, and each instance should have its
736             # own data (I think).
737 7 100       51 if($opts{FRESH}) {
738 3         9 $_did_set_phase = false;
739             }
740              
741 6 50       23 if($opts{PRINT_VERSION}) { # print version, raw and dotted
742 0 50       0 if($App::hopen::VERSION =~ m<^([^\.]+)\.(\d{3})(\d{3})>) {
743 0         0 printf "hopen version %d.%d.%d ($App::hopen::VERSION)\n", $1, $2, $3;
744             } else {
745 0         0 say "hopen $VERSION";
746             }
747 0 50       0 say "App::hopen in: $INC{'App/hopen.pm'}" if $VERBOSE >= 1;
748 0         0 return;
749             }
750              
751             # = Initialize filesystem-related build-system globals ==================
752              
753             # Start with the default phase unless one was specified.
754 6   33     44 $Phase = $opts{PHASE} // $PHASES[0];
755 6 50       29 die "Phase $Phase is not one of the ones I know about (" .
756             join(', ', @PHASES) . ')'
757             unless defined phase_idx($Phase);
758              
759             # Get the project dir
760 6 50       39 my $proj_dir = $opts{PROJ_DIR} ? dir($opts{PROJ_DIR}) : dir; #default=cwd
761 6         344 $ProjDir = $proj_dir;
762              
763             # Get the destination dir
764 6         14 my $dest_dir;
765 7 50       34 if($opts{DEST_DIR}) {
766 7         23 $dest_dir = dir($opts{DEST_DIR});
767             } else {
768 1         4 $dest_dir = $proj_dir->subdir('built');
769             }
770 7         292 $DestDir = $dest_dir;
771              
772             # Prohibit in-source builds
773 7 50       27 die <<EOT if $proj_dir eq $dest_dir;
774             I'm sorry, but I don't support in-source builds (dir ``$proj_dir''). Please
775             specify a different project directory (--from) or destination directory (--to).
776             EOT
777              
778             # Prohibit builds if there's a MY.hopen.pl file in the project directory,
779             # since those are the marker of a destination directory.
780 7 50       283 if(-e $proj_dir->file(MYH)) { die <<EOT; }
  1         1  
781             I'm sorry, but project directory ``$proj_dir'' appears to actually be a
782 1         3 build directory --- it has a @{[MYH]} file. If you really want to build
783 0         0 here, remove or rename @{[MYH]} and run me again.
784             EOT
785              
786             # See if we have hopen files associated with the project dir
787 6         902 my $myhopen = find_myhopen($dest_dir, !!$opts{FRESH});
788 6         194 my $lrHopenFiles = find_hopen_files($proj_dir, $dest_dir, !!$opts{FRESH});
789              
790             # Check the mtimes - we don't use MYH if another hopen file is newer.
791 7 100 66     50 if($myhopen && -e $myhopen) {
792 4         171 my $myhstat = file($myhopen)->stat;
793              
794 4         1041 foreach my $fn (@$lrHopenFiles) {
795 4         36 my $stat = file($fn)->stat;
796              
797 4 50 33     1677 if( $stat->mtime > $myhstat->mtime ||
798             $stat->ctime > $myhstat->ctime)
799             {
800 1 50       10 say "Skipping out-of-date ``$myhopen''" unless $QUIET;
801 1         588 $myhopen = undef;
802 0         0 last;
803             }
804             } #foreach hopen file
805             } #if MYH exists
806              
807             # Add -e's to the list of hopen files
808 6 50       168 if($opts{EVAL}) {
809 1         13 my $which_e = 0;
810             push @$lrHopenFiles,
811             map {
812 1         3 ++$which_e;
813 0         0 +{text=>$_, num=>$which_e, name=>("-e #" . $which_e)}
814 1         2 } @{$opts{EVAL}};
  0         0  
815             }
816              
817             hlog { 'hopen files: ',
818 1 50 0 0   2108 map { ref eq 'HASH' ? "<<$_->{text}>>" : "``$_''" }
  1         23  
819 6         44 ($myhopen // (), @$lrHopenFiles) } 2;
820              
821             # Can't proceed if the only hopen file is $myhopen.
822 7 50       94 unless(@$lrHopenFiles) { die <<EOT; }
  1         12  
823             I can't find any hopen project files (.hopen.pl or *.hopen.pl) for
824             project directory ``$proj_dir''.
825             EOT
826              
827             # Prepare the destination directory if it doesn't exist
828 7 50       757 File::Path::Tiny::mk($dest_dir) or die "Couldn't create $dest_dir: $!";
829              
830             # Create the initial DAG before loading anything so that the
831             # generator and toolset can add initialization operations.
832 7         296 $Build = hnew DAG => '__R_main';
833              
834             # = Load generator and toolset (and run MYH) ============================
835              
836 7 100       88613 say "From ``$proj_dir'' into ``$dest_dir''" unless $QUIET;
837              
838             # Load MY.hopen.pl first so the results of the Probe phase are
839             # available to the generator and toolset.
840 6 100 66     561 if($myhopen && !$opts{BUILD}) {
841 3         64 _execute_hopen_file($myhopen,
842             forward_opts(\%opts, {lc=>1}, qw(PHASE QUIET)),
843             ); # TODO support _e_h_f libs option
844             }
845              
846             # Tell the user the initial phase if MY.hopen.pl didn't change it
847 7 50 66     272 say "Running $Phase phase" unless $opts{BUILD} or $_did_set_phase or $QUIET;
      66        
848              
849             # Load generator
850             {
851 7         21 my ($gen, $gen_class);
852 6         41 $gen_class = loadfrom($opts{GENERATOR}, 'App::hopen::Gen::', '');
853 6 50       414 die "Can't find generator $opts{GENERATOR}" unless $gen_class;
854 6     1   49 hlog { "Generator spec ``$opts{GENERATOR}'' -> using generator $gen_class" };
  1         8  
855              
856             $gen = "$gen_class"->new(proj_dir => $proj_dir, dest_dir => $dest_dir,
857             architecture => $opts{ARCHITECTURE})
858 7 50       147 or die "Can't initialize generator";
859 7         4503 $Generator = $gen;
860             }
861              
862             # Load toolset
863             {
864 7         26 my $toolset_class;
  7         428  
  7         831  
865 7   33     53 $opts{TOOLSET} //= $Generator->default_toolset;
866             $toolset_class = loadfrom($opts{TOOLSET},
867 7         597 'App::hopen::T::', '');
868 6 50       506 die "Can't find toolset $opts{TOOLSET}" unless $toolset_class;
869              
870 6     0   44 hlog { "Toolset spec ``$opts{TOOLSET}'' -> using toolset $toolset_class" };
  1         12  
871 7         57 $Toolset = $toolset_class;
872             }
873              
874             # Handle --build, now that everything's loaded --------------
875 7 50       29 if($opts{BUILD}) {
876             # TODO? make sure we're in the right phase?
877 0         0 $Generator->run_build();
878 0         0 return;
879             }
880              
881             # = Run the hopen files (except MYH, already run) =======================
882              
883 6         13 my $new_data;
884 7 50       2072 if(@$lrHopenFiles) {
885 7         67 $new_data = _run_phase(
886             files => [@$lrHopenFiles],
887             forward_opts(\%opts, {lc=>1}, qw(PHASE QUIET))
888             ); # TODO support _run_phase libs option
889              
890             } else { # No hopen files (other than MYH) => just use the data from MYH
891 1         2 $new_data = $_hrData;
892             }
893              
894 7         475 $Generator->finalize(-dag => $Build, -data => $new_data);
895             # TODO RESUME HERE - figure out how the generator works into this.
896              
897             # = Save state in MY.hopen.pl for the next run ==========================
898              
899             # If we get here, _run_phase succeeded. Therefore, we can move
900             # on to the next phase.
901 7   66     1299 my $new_phase = next_phase($Phase) // $Phase;
902              
903             # TODO? give the generators a way to stash information that will be
904             # written at the top of MY.hopen.pl. This way, the user may only
905             # need to edit right at the top of the file, and not also throughout
906             # the hashref.
907              
908 7         26 my $VAR = '__R_new_data';
909 7         663 my $dumper = Data::Dumper->new([$new_data], [$VAR]);
910 6         324 $dumper->Pad(' ' x 12); # To line up with the do{}
911 6         114 $dumper->Indent(1); # fixed indent size
912 7         132 $dumper->Quotekeys(0);
913 7         75 $dumper->Purity(1);
914 7         73 $dumper->Maxrecurse(0); # no limit
915 6         67 $dumper->Sortkeys(true); # For consistency between runs
916 6         72 $dumper->Sparseseen(true); # We don't use Seen()
917              
918             # Four-space indent instead of two-space. This is using an undocumented
919             # feature of Data::Dumper, whence the eval{}.
920 6         43 eval { $dumper->{xpad} = ' ' x 4 };
  7         25  
921              
922 7         23 my $new_text = dedent [], qq(
923 7         72 # @{[MYH]} generated at @{[scalar gmtime]} GMT
  7         169  
924 7         860 # From ``@{[$proj_dir->absolute]}'' into ``@{[$dest_dir->absolute]}''
  7         1005  
925              
926             set_phase '$new_phase';
927             do {
928             my \$$VAR;
929 7         1369 @{[$dumper->Dump]}
930             \$$VAR
931             }
932             );
933              
934             # Notes on the above $new_text:
935             # - No semi after the Dump line --- Dump adds it automatically.
936             # - Dump() may produce multiple statements, so add the
937             # express $__R_new_data at the end so the do{} will have a
938             # consistent return value.
939             # - The Dump() line is not indented because it does its own indentation.
940              
941 6         3300 $dest_dir->file(MYH)->spew($new_text);
942              
943             } #_inner() }}}2
944              
945             # }}}1
946             # === Command-line runner =============================================== {{{1
947              
948             sub Main {
949              
950             =head2 Main
951              
952             Command-line runner. Call as C<< App::hopen::Main(\@ARGV) >>.
953              
954             =cut
955              
956 6   66 6 1 35902 my $lrArgs = shift // [];
957              
958             # = Process options ================================================= {{{2
959              
960 7         27 my %opts;
961 7         33 _parse_command_line(from => $lrArgs, into => \%opts);
962              
963             # Check for mutually-inconsistent options
964 7 50 100     37 if($opts{FRESH} && $opts{BUILD}) {
965 0         0 print STDERR '--fresh and --build cannot be used together';
966 0         0 return EXIT_PARAM_ERR;
967             }
968              
969             # Verbosity is the max of -v and --verbose
970 6 50       35 $opts{VERBOSE} = $opts{VERBOSE2} if $opts{VERBOSE2} > $opts{VERBOSE};
971              
972             # Option overrides: -q beats -v
973 7 50       2047 $opts{VERBOSE} = 0 if $opts{QUIET};
974 7   33     68 $QUIET = !!($opts{QUIET} // false);
975 7         16 delete $opts{QUIET};
976             # After this, code only refers to $QUIET for consistency.
977              
978             # Implement verbosity
979 7 50 33     54 if(!$QUIET && $opts{VERBOSE}) {
980 1         678 $VERBOSE += $opts{VERBOSE};
981             #hlog { Verbosity => $VERBOSE };
982              
983             # Under -v, keep stdout and stderr lines in order.
984 1         10 STDOUT->autoflush(true);
985 1         573 STDERR->autoflush(true);
986             }
987              
988 6         30 delete @opts{qw(VERBOSE VERBOSE2)};
989             # After this, code only refers to $QUIET for consistency.
990              
991             # Don't print the source of an eval'ed hopen file unless -vvv or higher.
992             # Need 3 for the "..." that Carp prints when truncating.
993 6 50       23 $Carp::MaxEvalLen = 3 unless $VERBOSE >= 3;
994              
995             # }}}2
996             # = Do it, Rockapella! ==================================================
997              
998 7         26 $RUNNING = true;
999 7         13 eval { _inner(%opts); };
  7         297  
1000 7         3029 my $msg = $@;
1001 7         310 $RUNNING = false;
1002              
1003 7 50       38 if($msg) {
1004 1         331 print STDERR $msg;
1005 0         0 return EXIT_PROC_ERR; # eval{} so we can do this (die() exitcode = 2)
1006             }
1007              
1008 6         52 return EXIT_OK;
1009             } #Main()
1010              
1011             # }}}1
1012              
1013             # no import() --- call Main() directly with its fully-qualified name
1014              
1015             1;
1016             __END__
1017             # === Rest of the documentation ========================================= {{{1
1018              
1019             =head1 AUTHOR
1020              
1021             Christopher White, C<cxwembedded at gmail.com>
1022              
1023             =head1 SUPPORT
1024              
1025             You can find documentation for this module with the perldoc command.
1026              
1027             perldoc App::hopen For command-line options
1028             perldoc App::hopen::Conventions For terminology and workflow
1029             perldoc Data::Hopen For the underlying engine
1030              
1031             You can also look for information at:
1032              
1033             =over
1034              
1035             =item * GitHub: The project's main repository and issue tracker
1036              
1037             L<https://github.com/hopenbuild/App-hopen>
1038              
1039             =item * MetaCPAN
1040              
1041             L<https://metacpan.org/pod/App::hopen>
1042              
1043             =item * This distribution
1044              
1045             See the C<eg/> directory distributed with this software for examples.
1046              
1047             =back
1048              
1049             =head1 LICENSE AND COPYRIGHT
1050              
1051             Copyright (c) 2018--2019 Christopher White. All rights reserved.
1052              
1053             This program is free software; you can redistribute it and/or
1054             modify it under the terms of the GNU Lesser General Public
1055             License as published by the Free Software Foundation; either
1056             version 2.1 of the License, or (at your option) any later version.
1057              
1058             This program is distributed in the hope that it will be useful,
1059             but WITHOUT ANY WARRANTY; without even the implied warranty of
1060             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1061             Lesser General Public License for more details.
1062              
1063             You should have received a copy of the GNU Lesser General Public
1064             License along with this program; if not, write to the Free
1065             Software Foundation, Inc.,
1066             51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
1067              
1068             =cut
1069              
1070             # }}}1
1071             # vi: set ts=4 sts=4 sw=4 et ai foldmethod=marker: #