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