File Coverage

blib/lib/App/hopen.pm
Criterion Covered Total %
statement 318 357 89.0
branch 71 134 52.9
condition 46 120 38.3
subroutine 52 64 81.2
pod 1 1 100.0
total 488 676 72.1


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.000015';
4              
5             # Imports {{{1
6 2     2   289205 use strict; use warnings;
  2     2   3  
  2         70  
  2         8  
  2         3  
  2         93  
7 5     2   634 use Data::Hopen::Base;
  5         23435  
  5         26  
8              
9 5     2   3800 use App::hopen::AppUtil ':all';
  5         2509  
  5         258  
10 5     2   2864 use App::hopen::BuildSystemGlobals;
  2         14  
  2         192  
11 5     2   826 use App::hopen::Phases qw(:default phase_idx next_phase);
  2         6  
  2         322  
12 2     2   11 use App::hopen::Util qw(isMYH MYH);
  2         2  
  2         84  
13 2     2   964 use App::hopen::Util::String qw(line_mark_string);
  2         5  
  2         124  
14 2     2   9 use Data::Hopen qw(:default loadfrom *VERBOSE *QUIET);
  2         2  
  2         220  
15 2     2   906 use Data::Hopen::Scope::Hash;
  2         81890  
  2         122  
16 2     2   865 use Data::Hopen::Scope::Environment;
  2         3433  
  2         88  
17 2     2   12 use Data::Hopen::Util::Data qw(dedent forward_opts);
  2         36  
  2         161  
18 2     2   11 use Data::Dumper;
  2         2  
  2         60  
19 2     2   1795 use File::Path::Tiny;
  2         2335  
  2         62  
20 2     2   11 use File::stat ();
  2         3  
  2         36  
21 2     2   1428 use Getopt::Long qw(GetOptionsFromArray :config gnu_getopt);
  2         22973  
  2         8  
22 2     2   418 use Hash::Merge;
  2         4  
  2         77  
23 2     2   9 use Path::Class;
  2         3  
  2         135  
24 2     2   10 use Scalar::Util qw(looks_like_number);
  2         3  
  2         108  
25              
26 2     2   83 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   10 use constant DEBUG => false;
  2         5  
  2         192  
198              
199             # Shell exit codes
200 2     2   9 use constant EXIT_OK => 0; # success
  2         2  
  2         61  
201 2     2   6 use constant EXIT_PROC_ERR => 1; # error during processing
  2         3  
  2         78  
202 2     2   6 use constant EXIT_PARAM_ERR => 2; # couldn't understand the command line
  2         4  
  2         705  
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   36 my %params = @_;
324             #local @_Sources;
325              
326 6         20 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   79 local *have = sub { return exists($hrOptsOut->{ $_[0] }); };
  18         110  
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         97 map { $CMDLINE_OPTS{ $_ }->[0] => $CMDLINE_OPTS{ $_ }[2] }
335 6         71 grep { (scalar @{$CMDLINE_OPTS{ $_ }})==3 }
  84         123  
  84         196  
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     43 map { $_->[0] . ($_->[1] // '') } values %CMDLINE_OPTS, # options strs
  84         333  
345             );
346              
347             # Help, if requested
348 6 50 33     20632 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   11 no warnings 'once';
  2         3  
  2         2160  
  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         61 my %revmap = map { $CMDLINE_OPTS{$_}->[0] => $_ } keys %CMDLINE_OPTS;
  84         244  
382 6         40 for my $optname (keys %$hrOptsOut) {
383 33         91 $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     14 $hrOptsOut->{DEST_DIR} //= $params{from}->[0] if @{$params{from}};
  6         38  
389 6 50 0     17 $hrOptsOut->{PROJ_DIR} //= $params{from}->[1] if @{$params{from}}>1;
  6         28  
390              
391             # Sanity check VERBOSE2, and give it a default of 0
392 6   50     44 my $v2 = $hrOptsOut->{VERBOSE2} // 0;
393 6 50       40 $v2 = 1 if $v2 eq ''; # --verbose without value === --verbose=1
394 6 50 33     76 die "--verbose requires a positive numeric argument"
      33        
395             if (defined $v2) && ( !looks_like_number($v2) || (int($v2) < 0) );
396 6   50     76 $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   253 my $fn = shift or croak 'Need a file to run';
437 9         41 my %opts = @_;
438 9 50       31 $Phase = $opts{phase} if $opts{phase};
439              
440 9         121 my $merger = Hash::Merge->new('RETAINMENT_PRECEDENT');
441              
442             # == Set up code pieces related to phase control ==
443              
444 9         1183 my ($set_phase, $cannot_set_phase, $cannot_set_phase_warn);
445 9         39 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       45 ($opts{quiet} ? '' : 'say "Running $new_phase phase";') . "}\n";
462              
463 9         19 $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       36 ($opts{quiet} ? '' :
479             q(
480             warn "``$FILENAME'': Ignoring attempt to set phase";
481             )
482             ) . "}\n";
483              
484 9         21 my $lib_dirs = '';
485 9 50       31 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         19 my ($friendly_name, $pkg_name, $file_text, $phase_text);
493              
494 9         19 $phase_text = q(
495             use App::hopen::Phases ':all';
496             );
497              
498             # -- Load the file
499              
500 9 50       47 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   69 hlog { 'Processing', $fn };
  0         0  
510 9         114 $file_text = file($fn)->slurp;
511 9         4503 $pkg_name = ($fn =~ s/[^a-zA-Z0-9]/_/gr) . '_' . $_hf_pkg_idx++;
512 9         272 $friendly_name = $fn;
513              
514 9 100 66     46 if( isMYH($fn) and !defined($opts{phase}) ) {
515             # MY.hopen.pl files can set $Phase unless --phase was given.
516 3         120 $phase_text .= $set_phase;
517 3         9 $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       40 $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         31 $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         153 my $src = line_mark_string <<EOT ;
540             {
541             package __Rpkg_$pkg_name;
542 1     1   739 use App::hopen::HopenFileKit "\Q$friendly_name\E";
  1     1   4  
  1     2   8  
  1     2   11  
  1     2   2  
  1     2   12  
  1     1   9  
  1     1   4  
  1     1   9  
  1         9  
  1         2  
  1         10  
  1         9  
  1         2  
  1         11  
  1         8  
  1         2  
  1         8  
  1         9  
  1         4  
  1         11  
  1         11  
  1         2  
  1         11  
  1         9  
  1         2  
  1         9  
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   170 # /Other phase text
  1     2   2  
  1     2   663  
  1     2   7  
  1     2   3  
  1     2   734  
  1     1   6  
  1     1   2  
  1     1   347  
  1         7  
  1         2  
  1         306  
  1         8  
  1         2  
  1         675  
  1         7  
  1         2  
  1         322  
  1         8  
  1         3  
  1         370  
  1         8  
  1         3  
  1         749  
  1         8  
  1         2  
  1         375  
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
    0 33        
    0 33        
      0        
      33        
      33        
557   50         # phase in as a literal so that it's read-only (see perlmod).
    50          
    50          
    50          
    50          
    50          
558   50          
    50          
559 9 100       37 unless($setting_phase_allowed) {
560 6         46 $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         44 $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         42 $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   92 hlog { "Source for $fn\n", $src, "\n" } 3;
  0         0  
625              
626             # == Run the package ==
627              
628 9         1238 eval($src);
629 9 50       98 die "Error in $friendly_name: $@" if $@;
630              
631             # Get the data from the package we just ran
632 9         25 my $hrAddlData = eval {
633 2     2   110 no strict 'refs';
  2         63  
  2         4589  
634 9         19 ${ "__Rpkg_$pkg_name\::hrNewData" }
  9         49  
635             };
636              
637 9     0   77 hlog { 'old data', Dumper($_hrData) } 3;
  0         0  
638 9     0   152 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       119 $_hrData = $merger->merge($_hrData, $hrAddlData) if $hrAddlData;
646 9     0   708 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   180 my %opts = @_;
679 6 50       24 $Phase = $opts{phase} if $opts{phase};
680 6         17 my $lrHopenFiles = $opts{files};
681 6 50       24 croak 'Need files=>[...]' unless ref $lrHopenFiles eq 'ARRAY';
682 6     0   37 hlog { Phase => $Phase, Running => Dumper($lrHopenFiles) };
  0         0  
683              
684             # = Process the files ======================================
685              
686 6         56 foreach my $fn (@$lrHopenFiles) {
687 6         23 _execute_hopen_file($fn,
688             forward_opts(\%opts, qw(phase quiet libs))
689             );
690             } # foreach hopen file
691              
692 1 50   0   6 hlog { 'Graph is', ($Build->empty ? 'empty.' : 'not empty.'),
693 6         612 ' Final data is', Dumper($_hrData) } 2;
694              
695 7     0   73 hlog { 'Build graph', '' . $Build->_graph } 5;
  1         5  
696 7     0   91 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     118 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 3         386 my $env = Data::Hopen::Scope::Environment->new(name => 'outermost');
707 3         242 my $scope = Data::Hopen::Scope::Hash->new(name => 'from hopen files');
708 3         194 $scope->adopt_hash($_hrData);
709 4         223 $scope->outer($env); # make the environment accessible...
710 4         129 $scope->local(true); # ... but not copied by local-scope calls.
711              
712             # Run the DAG
713 4         48 my $result_data = $Build->run(-context => $scope, -visitor => $Generator);
714 4     1   2638 hlog { Data::Dumper->new([$result_data], ['Build graph result data'])->Indent(1)->Dump } 2;
  1         4  
715 4         88 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 6     6   40 my %opts = @_;
732 6         22 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 6 100       24 if($opts{FRESH}) {
738 3         9 $_did_set_phase = false;
739             }
740              
741 6 50       22 if($opts{PRINT_VERSION}) { # print version, raw and dotted
742 0         0 my $ver = $VERSION;
743 1 50       6 if($ver =~ m<^([^\.]+)\.(\d{3})(\d{3})>) {
744 1         4 printf "hopen version %d.%d.%d ($App::hopen::VERSION)\n", $1, $2, $3;
745             } else {
746 1         5 say "hopen $VERSION";
747             }
748 1 50       3 say "App::hopen in: $INC{'App/hopen.pm'}" if $VERBOSE >= 1;
749 1         3 return;
750             }
751              
752             # = Initialize filesystem-related build-system globals ==================
753              
754             # Start with the default phase unless one was specified.
755 7   33     96 $Phase = $opts{PHASE} // $PHASES[0];
756 6 50       39 die "Phase $Phase is not one of the ones I know about (" .
757             join(', ', @PHASES) . ')'
758             unless defined phase_idx($Phase);
759              
760             # Get the project dir
761 6 100       52 my $proj_dir = $opts{PROJ_DIR} ? dir($opts{PROJ_DIR}) : dir; #default=cwd
762 6         485 $ProjDir = $proj_dir;
763              
764             # Get the destination dir
765 6         14 my $dest_dir;
766 6 100       23 if($opts{DEST_DIR}) {
767 6         24 $dest_dir = dir($opts{DEST_DIR});
768             } else {
769 0         0 $dest_dir = $proj_dir->subdir('built');
770             }
771 6         349 $DestDir = $dest_dir;
772              
773             # Prohibit in-source builds
774 6 50       34 die <<EOT if $proj_dir eq $dest_dir;
775             I'm sorry, but I don't support in-source builds (dir ``$proj_dir''). Please
776             specify a different project directory (--from) or destination directory (--to).
777             EOT
778              
779             # Prohibit builds if there's a MY.hopen.pl file in the project directory,
780             # since those are the marker of a destination directory.
781 7 50       268 if(-e $proj_dir->file(MYH)) { die <<EOT; }
  1         3  
782             I'm sorry, but project directory ``$proj_dir'' appears to actually be a
783 0         0 build directory --- it has a @{[MYH]} file. If you really want to build
784 0         0 here, remove or rename @{[MYH]} and run me again.
785             EOT
786              
787             # See if we have hopen files associated with the project dir
788 6         1268 my $myhopen = find_myhopen($dest_dir, !!$opts{FRESH});
789 7         247 my $lrHopenFiles = find_hopen_files($proj_dir, $dest_dir, !!$opts{FRESH});
790              
791             # Check the mtimes - we don't use MYH if another hopen file is newer.
792 7 100 66     34 if($myhopen && -e $myhopen) {
793 4         265 my $myhstat = file($myhopen)->stat;
794              
795 4         1261 foreach my $fn (@$lrHopenFiles) {
796 4         964 my $stat = file($fn)->stat;
797              
798 4 50 33     1097 if( $stat->mtime > $myhstat->mtime ||
799             $stat->ctime > $myhstat->ctime)
800             {
801 1 0       725 say "Skipping out-of-date ``$myhopen''" unless $QUIET;
802 0         0 $myhopen = undef;
803 0         0 last;
804             }
805             } #foreach hopen file
806             } #if MYH exists
807              
808             # Add -e's to the list of hopen files
809 7 50       347 if($opts{EVAL}) {
810 1         2 my $which_e = 0;
811             push @$lrHopenFiles,
812             map {
813 0         0 ++$which_e;
814 0         0 +{text=>$_, num=>$which_e, name=>("-e #" . $which_e)}
815 1         3 } @{$opts{EVAL}};
  0         0  
816             }
817              
818             hlog { 'hopen files: ',
819 1 50 0 1   28 map { ref eq 'HASH' ? "<<$_->{text}>>" : "``$_''" }
  1         2  
820 7         3040 ($myhopen // (), @$lrHopenFiles) } 2;
821              
822             # Can't proceed if the only hopen file is $myhopen.
823 7 50       94 unless(@$lrHopenFiles) { die <<EOT; }
  1         830  
824             I can't find any hopen project files (.hopen.pl or *.hopen.pl) for
825             project directory ``$proj_dir''.
826             EOT
827              
828             # Prepare the destination directory if it doesn't exist
829 7 50       60 File::Path::Tiny::mk($dest_dir) or die "Couldn't create $dest_dir: $!";
830              
831             # Create the initial DAG before loading anything so that the
832             # generator and toolset can add initialization operations.
833 7         1016 $Build = hnew DAG => '__R_main';
834              
835             # = Load generator and toolset (and run MYH) ============================
836              
837 6 100       118674 say "From ``$proj_dir'' into ``$dest_dir''" unless $QUIET;
838              
839             # Load MY.hopen.pl first so the results of the Probe phase are
840             # available to the generator and toolset.
841 6 100 66     731 if($myhopen && !$opts{BUILD}) {
842 4         75 _execute_hopen_file($myhopen,
843             forward_opts(\%opts, {lc=>1}, qw(PHASE QUIET)),
844             ); # TODO support _e_h_f libs option
845             }
846              
847             # Tell the user the initial phase if MY.hopen.pl didn't change it
848 7 50 66     390 say "Running $Phase phase" unless $opts{BUILD} or $_did_set_phase or $QUIET;
      66        
849              
850             # Load generator
851             {
852 6         13 my ($gen, $gen_class);
853 6         34 $gen_class = loadfrom($opts{GENERATOR}, 'App::hopen::Gen::', '');
854 6 50       588 die "Can't find generator $opts{GENERATOR}" unless $gen_class;
855 7     0   68 hlog { "Generator spec ``$opts{GENERATOR}'' -> using generator $gen_class" };
  1         7  
856              
857             $gen = "$gen_class"->new(proj_dir => $proj_dir, dest_dir => $dest_dir,
858             architecture => $opts{ARCHITECTURE})
859 7 50       160 or die "Can't initialize generator";
860 7         6381 $Generator = $gen;
861             }
862              
863             # Load toolset
864             {
865 7         18 my $toolset_class;
  7         1330  
  7         25  
866 7   33     781 $opts{TOOLSET} //= $Generator->default_toolset;
867             $toolset_class = loadfrom($opts{TOOLSET},
868 6         34 'App::hopen::T::', '');
869 6 50       760 die "Can't find toolset $opts{TOOLSET}" unless $toolset_class;
870              
871 7     0   68 hlog { "Toolset spec ``$opts{TOOLSET}'' -> using toolset $toolset_class" };
  1         2  
872 7         62 $Toolset = $toolset_class;
873             }
874              
875             # Handle --build, now that everything's loaded --------------
876 6 50       25 if($opts{BUILD}) {
877             # TODO? make sure we're in the right phase?
878 0         0 $Generator->run_build();
879 0         0 return;
880             }
881              
882             # = Run the hopen files (except MYH, already run) =======================
883              
884 7         2852 my $new_data;
885 7 50       48 if(@$lrHopenFiles) {
886 7         61 $new_data = _run_phase(
887             files => [@$lrHopenFiles],
888             forward_opts(\%opts, {lc=>1}, qw(PHASE QUIET))
889             ); # TODO support _run_phase libs option
890              
891             } else { # No hopen files (other than MYH) => just use the data from MYH
892 1         16 $new_data = $_hrData;
893             }
894              
895 7         1278 $Generator->finalize(-dag => $Build, -data => $new_data);
896             # TODO RESUME HERE - figure out how the generator works into this.
897              
898             # = Save state in MY.hopen.pl for the next run ==========================
899              
900             # If we get here, _run_phase succeeded. Therefore, we can move
901             # on to the next phase.
902 7   66     873 my $new_phase = next_phase($Phase) // $Phase;
903              
904             # TODO? give the generators a way to stash information that will be
905             # written at the top of MY.hopen.pl. This way, the user may only
906             # need to edit right at the top of the file, and not also throughout
907             # the hashref.
908              
909 7         667 my $VAR = '__R_new_data';
910 6         67 my $dumper = Data::Dumper->new([$new_data], [$VAR]);
911 6         356 $dumper->Pad(' ' x 12); # To line up with the do{}
912 7         88 $dumper->Indent(1); # fixed indent size
913 7         137 $dumper->Quotekeys(0);
914 7         62 $dumper->Purity(1);
915 6         56 $dumper->Maxrecurse(0); # no limit
916 6         55 $dumper->Sortkeys(true); # For consistency between runs
917 6         65 $dumper->Sparseseen(true); # We don't use Seen()
918              
919             # Four-space indent instead of two-space. This is using an undocumented
920             # feature of Data::Dumper, whence the eval{}.
921 7         85 eval { $dumper->{xpad} = ' ' x 4 };
  7         28  
922              
923 7         19 my $new_text = dedent [], qq(
924 7         52 # @{[MYH]} generated at @{[scalar gmtime]} GMT
  7         958  
925 7         67 # From ``@{[$proj_dir->absolute]}'' into ``@{[$dest_dir->absolute]}''
  7         1961  
926              
927             set_phase '$new_phase';
928             do {
929             my \$$VAR;
930 6         834 @{[$dumper->Dump]}
931             \$$VAR
932             }
933             );
934              
935             # Notes on the above $new_text:
936             # - No semi after the Dump line --- Dump adds it automatically.
937             # - Dump() may produce multiple statements, so add the
938             # express $__R_new_data at the end so the do{} will have a
939             # consistent return value.
940             # - The Dump() line is not indented because it does its own indentation.
941              
942 6         1625 $dest_dir->file(MYH)->spew($new_text);
943              
944             } #_inner() }}}2
945              
946             # }}}1
947             # === Command-line runner =============================================== {{{1
948              
949             sub Main {
950              
951             =head2 Main
952              
953             Command-line runner. Call as C<< App::hopen::Main(\@ARGV) >>.
954              
955             =cut
956              
957 7   66 7 1 397387 my $lrArgs = shift // [];
958              
959             # = Process options ================================================= {{{2
960              
961 7         19 my %opts;
962 7         34 _parse_command_line(from => $lrArgs, into => \%opts);
963              
964             # Check for mutually-inconsistent options
965 6 50 100     34 if($opts{FRESH} && $opts{BUILD}) {
966 0         0 print STDERR '--fresh and --build cannot be used together';
967 0         0 return EXIT_PARAM_ERR;
968             }
969              
970             # Verbosity is the max of -v and --verbose
971 7 50       4268 $opts{VERBOSE} = $opts{VERBOSE2} if $opts{VERBOSE2} > $opts{VERBOSE};
972              
973             # Option overrides: -q beats -v
974 7 50       80 $opts{VERBOSE} = 0 if $opts{QUIET};
975 7   33     73 $QUIET = !!($opts{QUIET} // false);
976 7         34 delete $opts{QUIET};
977             # After this, code only refers to $QUIET for consistency.
978              
979             # Implement verbosity
980 7 50 33     940 if(!$QUIET && $opts{VERBOSE}) {
981 1         18 $VERBOSE += $opts{VERBOSE};
982             #hlog { Verbosity => $VERBOSE };
983              
984             # Under -v, keep stdout and stderr lines in order.
985 1         731 STDOUT->autoflush(true);
986 0         0 STDERR->autoflush(true);
987             }
988              
989 6         23 delete @opts{qw(VERBOSE VERBOSE2)};
990             # After this, code only refers to $QUIET for consistency.
991              
992             # Don't print the source of an eval'ed hopen file unless -vvv or higher.
993             # Need 3 for the "..." that Carp prints when truncating.
994 7 50       111 $Carp::MaxEvalLen = 3 unless $VERBOSE >= 3;
995              
996             # }}}2
997             # = Do it, Rockapella! ==================================================
998              
999 7         21 $RUNNING = true;
1000 7         376 eval { _inner(%opts); };
  7         41  
1001 7         4949 my $msg = $@;
1002 7         24 $RUNNING = false;
1003              
1004 7 50       388 if($msg) {
1005 0         0 print STDERR $msg;
1006 0         0 return EXIT_PROC_ERR; # eval{} so we can do this (die() exitcode = 2)
1007             }
1008              
1009 6         64 return EXIT_OK;
1010             } #Main()
1011              
1012             # }}}1
1013              
1014             # no import() --- call Main() directly with its fully-qualified name
1015              
1016             1;
1017             __END__
1018             # === Rest of the documentation ========================================= {{{1
1019              
1020             =head1 AUTHOR
1021              
1022             Christopher White, C<cxwembedded at gmail.com>
1023              
1024             =head1 SUPPORT
1025              
1026             You can find documentation for this module with the perldoc command.
1027              
1028             perldoc App::hopen For command-line options
1029             perldoc App::hopen::Conventions For terminology and workflow
1030             perldoc Data::Hopen For the underlying engine
1031              
1032             You can also look for information at:
1033              
1034             =over
1035              
1036             =item * GitHub: The project's main repository and issue tracker
1037              
1038             L<https://github.com/hopenbuild/App-hopen>
1039              
1040             =item * MetaCPAN
1041              
1042             L<https://metacpan.org/pod/App::hopen>
1043              
1044             =item * This distribution
1045              
1046             See the C<eg/> directory distributed with this software for examples.
1047              
1048             =back
1049              
1050             =head1 LICENSE AND COPYRIGHT
1051              
1052             Copyright (c) 2018--2019 Christopher White. All rights reserved.
1053              
1054             This program is free software; you can redistribute it and/or
1055             modify it under the terms of the GNU Lesser General Public
1056             License as published by the Free Software Foundation; either
1057             version 2.1 of the License, or (at your option) any later version.
1058              
1059             This program is distributed in the hope that it will be useful,
1060             but WITHOUT ANY WARRANTY; without even the implied warranty of
1061             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1062             Lesser General Public License for more details.
1063              
1064             You should have received a copy of the GNU Lesser General Public
1065             License along with this program; if not, write to the Free
1066             Software Foundation, Inc.,
1067             51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
1068              
1069             =cut
1070              
1071             # }}}1
1072             # vi: set ts=4 sts=4 sw=4 et ai foldmethod=marker: #