File Coverage

blib/lib/App/hopen.pm
Criterion Covered Total %
statement 296 332 89.1
branch 60 110 54.5
condition 38 89 42.7
subroutine 46 57 80.7
pod 1 1 100.0
total 441 589 74.8


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