File Coverage

blib/lib/App/hopen.pm
Criterion Covered Total %
statement 267 318 83.9
branch 52 110 47.2
condition 31 78 39.7
subroutine 46 54 85.1
pod 1 1 100.0
total 397 561 70.7


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