This is a live mirror of the Perl 5 development currently hosted at https://github.com/perl/perl5
Module-CoreList prepared for v5.19.7
[perl5.git] / Porting / pod_lib.pl
CommitLineData
d7816c47
NC
1#!/usr/bin/perl -w
2
3use strict;
449e2f79 4use Digest::MD5 'md5';
9bbb230a 5use File::Find;
d7816c47 6
1e62a62d
JK
7=head1 NAME
8
9Porting/pod_lib.pl - functions for building and installing POD
10
11=head1 SYNOPSIS
12
13 require './Porting/pod_lib.pl';
14
15=cut
16
17=head1 DESCRIPTION
18
19This program, when C<require>d into other programs in the Perl 5 core
20distribution, provides functions useful during building and, secondarily,
21testing.
22
23As of this writing, the functions in this program are used in these other
24programs:
25
26 installman
27 installperl
28 pod/buildtoc
29 pod/perl.pod
30 Porting/new-perldelta.pl
31 Porting/pod_rules.pl
32
33Note: Since these functions are used during the Perl build process, they must
34work with F<miniperl>. That necessarily implies that these functions must not
35rely on XS modules, either directly or indirectly (e.g., C<autodie>).
36
37=head1 SUBROUTINES
38
39=head2 C<my_die()>
40
41=over 4
42
43=item * Purpose
44
45Exit from a process with an error code and a message.
46
47=item * Arguments
48
49List of arguments to be passed with the error message. Example:
50
51 close $fh or my_die("close 'utils.lst': $!");
52
53=item * Return Value
54
55Exit code C<255>.
56
57=item * Comment
58
59Prints C<ABORTED> to STDERR.
60
61=back
62
63=cut
d7816c47
NC
64
65sub my_die {
66 print STDERR "$0: ", @_;
67 print STDERR "\n" unless $_[-1] =~ /\n\z/;
68 print STDERR "ABORTED\n";
69 exit 255;
70}
71
1e62a62d
JK
72=head2 C<open_or_die()>
73
74=over 4
75
76=item * Purpose
77
78Opens a file or fails if it cannot.
79
80=item * Arguments
81
82String holding filename to be opened. Example:
83
84 $fh = open_or_die('utils.lst');
85
86=item * Return Value
87
88Handle to opened file.
89
90=back
91
92=cut
93
d7816c47
NC
94sub open_or_die {
95 my $filename = shift;
96 open my $fh, '<', $filename or my_die "Can't open $filename: $!";
97 return $fh;
98}
99
1e62a62d
JK
100=head2 C<slurp_or_die()>
101
102=over 4
103
104=item * Purpose
105
106Read the contents of a file into memory as a single string.
107
108=item * Arguments
109
110String holding name of file to be read into memory.
111
112 $olddelta = slurp_or_die('pod/perldelta.pod');
113
114=item * Return Value
115
116String holding contents of file.
117
118=back
119
120=cut
121
bcfe7366
NC
122sub slurp_or_die {
123 my $filename = shift;
124 my $fh = open_or_die($filename);
125 binmode $fh;
126 local $/;
127 my $contents = <$fh>;
128 die "Can't read $filename: $!" unless defined $contents and close $fh;
129 return $contents;
130}
131
1e62a62d
JK
132=head2 C<write_or_die()>
133
134=over 4
135
136=item * Purpose
137
138Write out a string to a file.
139
140=item * Arguments
141
142List of two arguments: (i) String holding name of file to be written to; (ii)
143String holding contents to be written.
144
145 write_or_die($olddeltaname, $olddelta);
146
147=item * Return Value
148
149Implicitly returns true value upon success.
150
151=back
152
153=cut
154
bcfe7366
NC
155sub write_or_die {
156 my ($filename, $contents) = @_;
157 open my $fh, '>', $filename or die "Can't open $filename for writing: $!";
158 binmode $fh;
159 print $fh $contents or die "Can't write to $filename: $!";
160 close $fh or die "Can't close $filename: $!";
161}
162
93b1bf68
NC
163=head2 C<verify_contiguous()>
164
165=over 4
166
167=item * Purpose
168
169Verify that a file contains exactly one contiguous run of lines which matches
170the passed in pattern. C<croak()>s if the pattern is not found, or found in
171more than one place.
172
173=item * Arguments
174
175=over 4
176
177=item * Name of file
178
179=item * Contents of file
180
181=item * Pattern of interest
182
183=item * Name to report on error
184
185=back
186
187=item * Return Value
188
189The contents of the file, with C<qr/\0+/> substituted for the pattern.
190
191=back
192
193=cut
194
195sub verify_contiguous {
196 my ($name, $content, $re, $what) = @_;
197 require Carp;
198 $content =~ s/$re/\0/g;
199 my $sections = () = $content =~ m/\0+/g;
200 Carp::croak("$0: $name contains no $what") if $sections < 1;
201 Carp::croak("$0: $name contains discontiguous $what") if $sections > 1;
202 return $content;
203}
204
244a7751
NC
205=head2 C<process()>
206
207=over 4
208
209=item * Purpose
210
211Read a file from disk, pass the contents to the callback, and either update
212the file on disk (if changed) or generate TAP output to confirm that the
213version on disk is up to date. C<die>s if the file contains any C<NUL> bytes.
214This permits the callback routine to use C<NUL> bytes as placeholders while
215manipulating the file's contents.
216
217=item * Arguments
218
219=over 4
220
221=item * Description for use in error messages
222
223=item * Name of file
224
225=item * Callback
226
227Passed description and file contents, should return updated file contents.
228
229=item * Test number
230
231If defined, generate TAP output to C<STDOUT>. If defined and false, generate
232an unnumbered test. Otherwise this is the test number in the I<ok> line.
233
234=item * Verbose flag
235
236If true, generate verbose output.
237
238=back
239
240=item * Return Value
241
242Does not return anything.
243
244=back
245
246=cut
247
248sub process {
249 my ($desc, $filename, $callback, $test, $verbose) = @_;
250
251 print "Now processing $filename\n" if $verbose;
252 my $orig = slurp_or_die($filename);
253 my_die "$filename contains NUL bytes" if $orig =~ /\0/;
254
255 my $new = $callback->($desc, $orig);
256
257 if (defined $test) {
258 printf "%s%s # $filename is up to date\n",
259 ($new eq $orig ? 'ok' : 'not ok'), ($test ? " $test" : '');
260 return;
261 } elsif ($new eq $orig) {
262 print "Was not modified\n"
263 if $verbose;
264 return;
265 }
266
267 my $mode = (stat $filename)[2];
268 my_die "Can't stat $filename: $!"
269 unless defined $mode;
270 rename $filename, "$filename.old"
271 or my_die "Can't rename $filename to $filename.old: $!";
272
273 write_or_die($filename, $new);
274 chmod $mode & 0777, $filename or my_die "can't chmod $mode $filename: $!";
275}
276
1e62a62d
JK
277=head2 C<pods_to_install()>
278
279=over 4
280
281=item * Purpose
282
283Create a lookup table holding information about PODs to be installed.
284
285=item * Arguments
286
287None.
288
289=item * Return Value
290
291Reference to a hash with a structure like this:
292
293 $found = {
294 'MODULE' => {
295 'CPAN::Bundle' => 'lib/CPAN/Bundle.pm',
296 'Locale::Codes::Script_Retired' =>
297 'lib/Locale/Codes/Script_Retired.pm',
298 'Pod::Simple::DumpAsText' =>
299 'lib/Pod/Simple/DumpAsText.pm',
300 # ...
301 'Locale::Codes::LangVar' =>
302 'lib/Locale/Codes/LangVar.pod'
303 },
304 'PRAGMA' => {
305 'fields' => 'lib/fields.pm',
306 'subs' => 'lib/subs.pm',
307 # ...
308 },
309
310=item * Comment
311
312Broadly speaking, the function assembles a list of all F<.pm> and F<.pod>
313files in the distribution and then excludes certain files from installation.
314
315=back
316
317=cut
318
9bbb230a
NC
319sub pods_to_install {
320 # manpages not to be installed
321 my %do_not_install = map { ($_ => 1) }
322 qw(Pod::Functions XS::APItest XS::Typemap);
323
324 my (%done, %found);
325
326 File::Find::find({no_chdir=>1,
327 wanted => sub {
65e5b016
NC
328 if (m!/t\z!) {
329 ++$File::Find::prune;
330 return;
331 }
332
9bbb230a
NC
333 # $_ is $File::Find::name when using no_chdir
334 return unless m!\.p(?:m|od)\z! && -f $_;
9bbb230a
NC
335 return if m!lib/Net/FTP/.+\.pm\z!; # Hi, Graham! :-)
336 # Skip .pm files that have corresponding .pod files
337 return if s!\.pm\z!.pod! && -e $_;
338 s!\.pod\z!!;
339 s!\Alib/!!;
340 s!/!::!g;
341
342 my_die("Duplicate files for $_, '$done{$_}' and '$File::Find::name'")
343 if exists $done{$_};
344 $done{$_} = $File::Find::name;
345
346 return if $do_not_install{$_};
347 return if is_duplicate_pod($File::Find::name);
348 $found{/\A[a-z]/ ? 'PRAGMA' : 'MODULE'}{$_}
349 = $File::Find::name;
350 }}, 'lib');
351 return \%found;
352}
9887f448
NC
353
354my %state = (
355 # Don't copy these top level READMEs
356 ignore => {
357 micro => 1,
358 # vms => 1,
359 },
360 );
361
4027e27b
NC
362{
363 my (%Lengths, %MD5s);
364
365 sub is_duplicate_pod {
366 my $file = shift;
75d90f49 367 local $_;
4027e27b
NC
368
369 # Initialise the list of possible source files on the first call.
370 unless (%Lengths) {
371 __prime_state() unless $state{master};
372 foreach (@{$state{master}}) {
2ce3647c 373 next unless $_->[2]{dual};
4027e27b
NC
374 # This is a dual-life perl*.pod file, which will have be copied
375 # to lib/ by the build process, and hence also found there.
376 # These are the only pod files that might become duplicated.
2ce3647c
NC
377 ++$Lengths{-s $_->[1]};
378 ++$MD5s{md5(slurp_or_die($_->[1]))};
4027e27b
NC
379 }
380 }
381
382 # We are a file in lib. Are we a duplicate?
383 # Don't bother calculating the MD5 if there's no interesting file of
384 # this length.
385 return $Lengths{-s $file} && $MD5s{md5(slurp_or_die($file))};
386 }
449e2f79
NC
387}
388
9887f448 389sub __prime_state {
d7816c47
NC
390 my $source = 'perldelta.pod';
391 my $filename = "pod/$source";
64587559 392 my $contents = slurp_or_die($filename);
d7816c47 393 my @want =
bcfe7366 394 $contents =~ /perldelta - what is new for perl v(5)\.(\d+)\.(\d+)\n/;
d7816c47 395 die "Can't extract version from $filename" unless @want;
0aef0fe5
NC
396 my $delta_leaf = join '', 'perl', @want, 'delta';
397 $state{delta_target} = "$delta_leaf.pod";
bcfe7366 398 $state{delta_version} = \@want;
d7816c47
NC
399
400 # This way round so that keys can act as a MANIFEST skip list
401 # Targets will always be in the pod directory. Currently we can only cope
402 # with sources being in the same directory.
403 $state{copies}{$state{delta_target}} = $source;
404
0aef0fe5
NC
405 # The default flags if none explicitly set for the current file.
406 my $current_flags = '';
407 my (%flag_set, @paths);
d7816c47 408
0aef0fe5
NC
409 my $master = open_or_die('pod/perl.pod');
410
411 while (<$master>) {
412 last if /^=begin buildtoc$/;
413 }
414 die "Can't find '=begin buildtoc':" if eof $master;
415
416 while (<$master>) {
417 next if /^$/ or /^#/;
418 last if /^=end buildtoc/;
419 my ($command, @args) = split ' ';
420 if ($command eq 'flag') {
421 # For the named pods, use these flags, instead of $current_flags
422 my $flags = shift @args;
423 my_die("Malformed flag $flags")
424 unless $flags =~ /\A=([a-z]*)\z/;
425 $flag_set{$_} = $1 foreach @args;
426 } elsif ($command eq 'path') {
427 # If the pod's name matches the regex, prepend the given path.
428 my_die("Malformed path for /$args[0]/")
429 unless @args == 2;
430 push @paths, [qr/\A$args[0]\z/, $args[1]];
431 } elsif ($command eq 'aux') {
432 # The contents of perltoc.pod's "AUXILIARY DOCUMENTATION" section
4e604983 433 $state{aux} = [sort @args];
0aef0fe5
NC
434 } else {
435 my_die("Unknown buildtoc command '$command'");
436 }
437 }
d7816c47
NC
438
439 foreach (<$master>) {
0aef0fe5
NC
440 next if /^$/ or /^#/;
441 next if /^=head2/;
442 last if /^=for buildtoc __END__$/;
443
444 if (my ($action, $flags) = /^=for buildtoc flag ([-+])([a-z]+)/) {
445 if ($action eq '+') {
446 $current_flags .= $flags;
447 } else {
448 my_die("Attempt to unset [$flags] failed - flags are '$current_flags")
449 unless $current_flags =~ s/[\Q$flags\E]//g;
450 }
451 } elsif (my ($leafname, $desc) = /^\s+(\S+)\s+(.*)/) {
452 my $podname = $leafname;
453 my $filename = "pod/$podname.pod";
454 foreach (@paths) {
455 my ($re, $path) = @$_;
456 if ($leafname =~ $re) {
457 $podname = $path . $leafname;
458 $filename = "$podname.pod";
459 last;
460 }
461 }
462
463 # Keep this compatible with pre-5.10
464 my $flags = delete $flag_set{$leafname};
465 $flags = $current_flags unless defined $flags;
466
467 my %flags;
d7816c47 468 $flags{toc_omit} = 1 if $flags =~ tr/o//d;
dc437300 469 $flags{dual} = $podname ne $leafname;
d7816c47
NC
470
471 $state{generated}{"$podname.pod"}++ if $flags =~ tr/g//d;
472
473 if ($flags =~ tr/r//d) {
474 my $readme = $podname;
475 $readme =~ s/^perl//;
64587559 476 $state{readmes}{$readme} = $desc;
d7816c47 477 $flags{readme} = 1;
d7816c47
NC
478 } else {
479 $state{pods}{$podname} = $desc;
480 }
481 my_die "Unknown flag found in section line: $_" if length $flags;
449e2f79 482
d7816c47 483 push @{$state{master}},
2ce3647c 484 [$leafname, $filename, \%flags];
0aef0fe5
NC
485
486 if ($podname eq 'perldelta') {
487 local $" = '.';
0aef0fe5 488 push @{$state{master}},
2ce3647c 489 [$delta_leaf, "pod/$state{delta_target}"];
dc437300 490 $state{pods}{$delta_leaf} = "Perl changes in version @want";
0aef0fe5
NC
491 }
492
d7816c47 493 } else {
0aef0fe5 494 my_die("Malformed line: $_");
d7816c47
NC
495 }
496 }
0aef0fe5 497 close $master or my_die("close pod/perl.pod: $!");
e1aae8e4
NC
498 # This has to be special-cased somewhere. Turns out this is cleanest:
499 push @{$state{master}}, ['a2p', 'x2p/a2p.pod', {toc_omit => 1}];
0aef0fe5
NC
500
501 my_die("perl.pod sets flags for unknown pods: "
502 . join ' ', sort keys %flag_set)
503 if keys %flag_set;
9887f448
NC
504}
505
1e62a62d
JK
506=head2 C<get_pod_metadata()>
507
508=over 4
509
510=item * Purpose
511
512=item * Arguments
513
514List of one or more arguments.
515
516=over 4
517
518=item * Boolean true or false
519
730ad6b9 520=item * Reference to a subroutine.
1e62a62d
JK
521
522=item * Various other arguments.
523
524=back
525
526Example:
527
528 $state = get_pod_metadata(
529 0, sub { warn @_ if @_ }, 'pod/perltoc.pod');
530
531 get_pod_metadata(
532 1, sub { warn @_ if @_ }, values %Build);
533
534=item * Return Value
535
536Hash reference; each element provides either a list or a lookup table for
537information about various types of POD files.
538
539 'aux' => [ # utility programs like
540 'h2xs' and 'perlbug' ]
541 'generated' => { # lookup table for generated POD files
542 like 'perlapi.pod' }
543 'ignore' => { # lookup table for files to be ignored }
544 'pods' => { # lookup table in "name" =>
545 "short description" format }
546 'readmes' => { # lookup table for OS-specific
547 and other READMEs }
548 'delta_version' => [ # major version number, minor no.,
549 patch no. ]
550 'delta_target' => 'perl<Mmmpp>delta.pod',
551 'master' => [ # list holding entries for files callable
552 by 'perldoc' ]
553 'copies' => { # patch version perldelta =>
554 minor version perldelta }
555
556=back
557
558=cut
559
9887f448
NC
560sub get_pod_metadata {
561 # Do we expect to find generated pods on disk?
562 my $permit_missing_generated = shift;
563 # Do they want a consistency report?
564 my $callback = shift;
75d90f49 565 local $_;
d7816c47 566
9887f448 567 __prime_state() unless $state{master};
d4c6b7ae
NC
568 return \%state unless $callback;
569
9887f448
NC
570 my %BuildFiles;
571
572 foreach my $path (@_) {
573 $path =~ m!([^/]+)$!;
574 ++$BuildFiles{$1};
575 }
576
d7816c47
NC
577 # Sanity cross check
578
0aef0fe5 579 my (%disk_pods, %manipods, %manireadmes);
852355fc 580 my (%cpanpods, %cpanpods_leaf);
d7816c47
NC
581 my (%our_pods);
582
6ca94f41
RGS
583 # There are files that we don't want to list in perl.pod.
584 # Maybe the various stub manpages should be listed there.
585 my %ignoredpods = map { ( "$_.pod" => 1 ) } qw( );
d7816c47
NC
586
587 # Convert these to a list of filenames.
64587559
NC
588 ++$our_pods{"$_.pod"} foreach keys %{$state{pods}};
589 foreach (@{$state{master}}) {
2ce3647c
NC
590 ++$our_pods{"$_->[0].pod"}
591 if $_->[2]{readme};
d7816c47
NC
592 }
593
594 opendir my $dh, 'pod';
595 while (defined ($_ = readdir $dh)) {
596 next unless /\.pod\z/;
d7816c47
NC
597 ++$disk_pods{$_};
598 }
599
600 # Things we copy from won't be in perl.pod
601 # Things we copy to won't be in MANIFEST
602
603 my $mani = open_or_die('MANIFEST');
604 while (<$mani>) {
605 chomp;
606 s/\s+.*$//;
607 if (m!^pod/([^.]+\.pod)!i) {
6fdb59a5 608 ++$manipods{$1};
d7816c47
NC
609 } elsif (m!^README\.(\S+)!i) {
610 next if $state{ignore}{$1};
6fdb59a5 611 ++$manireadmes{"perl$1.pod"};
d7816c47 612 } elsif (exists $our_pods{$_}) {
852355fc
NC
613 ++$cpanpods{$_};
614 m!([^/]+)$!;
615 ++$cpanpods_leaf{$1};
d7816c47
NC
616 $disk_pods{$_}++
617 if -e $_;
618 }
619 }
620 close $mani or my_die "close MANIFEST: $!\n";
621
c26a697b
NC
622 # Are we running before known generated files have been generated?
623 # (eg in a clean checkout)
624 my %not_yet_there;
625 if ($permit_missing_generated) {
626 # If so, don't complain if these files aren't yet in place
627 %not_yet_there = (%manireadmes, %{$state{generated}}, %{$state{copies}})
628 }
629
d7816c47
NC
630 my @inconsistent;
631 foreach my $i (sort keys %disk_pods) {
632 push @inconsistent, "$0: $i exists but is unknown by buildtoc\n"
6ca94f41 633 unless $our_pods{$i} || $ignoredpods{$i};
d7816c47 634 push @inconsistent, "$0: $i exists but is unknown by MANIFEST\n"
9e241592
NC
635 if !$BuildFiles{'MANIFEST'} # Ignore if we're rebuilding MANIFEST
636 && !$manipods{$i} && !$manireadmes{$i} && !$state{copies}{$i}
637 && !$state{generated}{$i} && !$cpanpods{$i};
d7816c47
NC
638 }
639 foreach my $i (sort keys %our_pods) {
640 push @inconsistent, "$0: $i is known by buildtoc but does not exist\n"
c26a697b 641 unless $disk_pods{$i} or $BuildFiles{$i} or $not_yet_there{$i};
d7816c47 642 }
9e241592
NC
643 unless ($BuildFiles{'MANIFEST'}) {
644 # Again, ignore these if we're about to rebuild MANIFEST
645 foreach my $i (sort keys %manipods) {
646 push @inconsistent, "$0: $i is known by MANIFEST but does not exist\n"
647 unless $disk_pods{$i};
648 push @inconsistent, "$0: $i is known by MANIFEST but is marked as generated\n"
649 if $state{generated}{$i};
650 }
d7816c47 651 }
d4c6b7ae 652 &$callback(@inconsistent);
d7816c47
NC
653 return \%state;
654}
655
6561;
657
658# Local variables:
659# cperl-indent-level: 4
660# indent-tabs-mode: nil
661# End:
662#
663# ex: set ts=8 sts=4 sw=4 et: