File Coverage

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