xref: /openbsd/gnu/usr.bin/perl/regen/regen_lib.pl (revision 3d61058a)
1#!/usr/bin/perl -w
2use strict;
3our (@Changed, $TAP);
4use File::Compare;
5use Symbol;
6use Carp;
7use Text::Wrap();
8
9# Common functions needed by the regen scripts
10
11our $Needs_Write = $^O eq 'cygwin' || $^O eq 'os2' || $^O eq 'MSWin32';
12
13our $Verbose = 0;
14@ARGV = grep { not($_ eq '-q' and $Verbose = -1) }
15  grep { not($_ eq '--tap' and $TAP = 1) }
16  grep { not($_ eq '-v' and $Verbose = 1) } @ARGV;
17
18END {
19  print STDOUT "Changed: @Changed\n" if @Changed;
20}
21
22sub safer_unlink {
23  my @names = @_;
24  my $cnt = 0;
25
26  my $name;
27  foreach $name (@names) {
28    next unless -e $name;
29    chmod 0777, $name if $Needs_Write;
30    ( CORE::unlink($name) and ++$cnt
31      or warn "Couldn't unlink $name: $!\n" );
32  }
33  return $cnt;
34}
35
36# Open a new file.
37sub open_new {
38    my ($final_name, $mode, $header, $force) = @_;
39    my $name = $final_name . '-new';
40    my $lang =
41        $final_name =~ /\.pod\z/ ? 'Pod' :
42        $final_name =~ /\.(?:c|h|inc|tab|act)\z/ ? 'C' :
43        $final_name =~ /\.gitignore\z/ ? 'None' :
44        'Perl';
45    if ($force && -e $final_name) {
46        chmod 0777, $name if $Needs_Write;
47        CORE::unlink $final_name
48                or die "Couldn't unlink $final_name: $!\n";
49    }
50    my $fh = gensym;
51    if (!defined $mode or $mode eq '>') {
52        if (-f $name) {
53            unlink $name or die "$name exists but can't unlink: $!";
54        }
55        open $fh, '>', $name or die "Can't create $name: $!";
56    } elsif ($mode eq '>>') {
57        open $fh, '>>', $name or die "Can't append to $name: $!";
58    } else {
59        die "Unhandled open mode '$mode'";
60    }
61    @{*$fh}{qw(name final_name lang force)}
62        = ($name, $final_name, $lang, $force);
63    binmode $fh;
64    print {$fh} read_only_top(lang => $lang, %$header) if $header;
65    $fh;
66}
67
68sub close_and_rename {
69    my $fh = shift;
70    my ($name, $final_name, $force) = @{*{$fh}}{qw(name final_name force)};
71    close $fh or die "Error closing $name: $!";
72
73    if ($TAP) {
74        # Don't use compare because if there are errors it doesn't give any
75        # way to generate diagnostics about what went wrong.
76        # These files are small enough to read into memory.
77        local $/;
78        # This is the file we just closed, so it should open cleanly:
79        open $fh, '<', $name
80            or die "Can't open '$name': $!";
81        my $want = <$fh>;
82        die "Can't read '$name': $!"
83            unless defined $want;
84        close $fh
85            or die "Can't close '$name': $!";
86
87        my $fail;
88        if (!open $fh, '<', $final_name) {
89            $fail = "Can't open '$final_name': $!";
90        } else {
91            my $have = <$fh>;
92            if (!defined $have) {
93                $fail = "Can't read '$final_name': $!";
94                close $fh;
95            } elsif (!close $fh) {
96                $fail = "Can't close '$final_name': $!";
97            } elsif ($want ne $have) {
98                $fail = "'$name' and '$final_name' differ";
99            }
100        }
101        # If someone wants to run t/porting/regen.t and keep the
102        # changes then they can set this env var, otherwise we
103        # unlink the generated file regardless.
104        my $keep_changes= $ENV{"REGEN_T_KEEP_CHANGES"};
105        safer_unlink($name) unless $keep_changes;
106        if ($fail) {
107            print STDOUT "not ok - $0 $final_name\n";
108            die "$fail\n";
109        } else {
110            print STDOUT "ok - $0 $final_name\n";
111        }
112        # If we get here then the file hasn't changed, and we should
113        # delete the new version if they have requested we keep changes
114        # as we wont have deleted it above like we would normally.
115        safer_unlink($name) if $keep_changes;
116        return;
117    }
118    unless ($force) {
119        if (compare($name, $final_name) == 0) {
120            warn "no changes between '$name' & '$final_name'\n" if $Verbose > 0;
121            safer_unlink($name);
122            return;
123        }
124        warn "changed '$name' to '$final_name'\n" if $Verbose > 0;
125        push @Changed, $final_name unless $Verbose < 0;
126    }
127
128    # Some DOSish systems can't rename over an existing file:
129    safer_unlink $final_name;
130    chmod 0600, $name if $Needs_Write;
131    rename $name, $final_name or die "renaming $name to $final_name: $!";
132}
133
134my %lang_opener = (
135    Perl => '# ',
136    Pod  => '',
137    C    => '/* ',
138    None => '# ',
139);
140
141sub read_only_top {
142    my %args = @_;
143    my $lang = $args{lang};
144    die "Missing language argument" unless defined $lang;
145    die "Unknown language argument '$lang'"
146        unless exists $lang_opener{$lang};
147    my $style = $args{style} ? " $args{style} " : '   ';
148
149    # Generate the "modeline" for syntax highlighting based on the language
150    my $raw = "-*- " . ($lang eq 'None' ? "" : "mode: $lang; ") . "buffer-read-only: t -*-\n";
151
152    if ($args{file}) {
153        $raw .= "\n   $args{file}\n";
154    }
155    if ($args{copyright}) {
156        local $" = ', ';
157         $raw .= wrap(75, '   ', '   ', <<"EOM") . "\n";
158
159Copyright (C) @{$args{copyright}} by\0Larry\0Wall\0and\0others
160
161You may distribute under the terms of either the GNU General Public
162License or the Artistic License, as specified in the README file.
163EOM
164    }
165
166    $raw .= "!!!!!!!   DO NOT EDIT THIS FILE   !!!!!!!\n";
167
168    if ($args{by}) {
169        $raw .= "This file is built by $args{by}";
170        if ($args{from}) {
171            my @from = ref $args{from} eq 'ARRAY' ? @{$args{from}} : $args{from};
172            my $last = pop @from;
173            if (@from) {
174                $raw .= ' from ' . join (', ', @from) . " and $last";
175            } else {
176                $raw .= " from $last";
177            }
178        }
179        $raw .= ".\n";
180    }
181    $raw .= "Any changes made here will be lost!\n";
182    $raw .= $args{final} if $args{final};
183
184    my $cooked = $lang eq 'C'
185        ? wrap(78, '/* ', $style, $raw) . " */\n\n"
186        : wrap(78, $lang_opener{$lang}, $lang_opener{$lang}, $raw) . "\n";
187    $cooked =~ tr/\0/ /; # Don't break Larry's name etc
188    $cooked =~ s/ +$//mg; # Remove all trailing spaces
189    $cooked =~ s! \*/\n!$args{quote}!s if $args{quote};
190    return $cooked;
191}
192
193sub read_only_bottom_close_and_rename {
194    my ($fh, $sources) = @_;
195    my ($name, $lang, $final_name) = @{*{$fh}}{qw(name lang final_name)};
196    confess "bad fh in read_only_bottom_close_and_rename" unless $name;
197    die "No final name specified at open time for $name"
198        unless $final_name;
199
200    my $comment;
201    if ($sources) {
202        $comment = "Generated from:\n";
203        foreach my $file (sort @$sources) {
204            my $digest = (-e $file)
205                         ? digest($file)
206                           # Use a random number that won't match the real
207                           # digest, so will always show as out-of-date, so
208                           # Porting tests likely will fail drawing attention
209                           # to the problem.
210                         : int(rand(1_000_000));
211            $comment .= "$digest $file\n";
212        }
213    }
214    $comment .= "ex: set ro" . ($lang eq 'None' ? "" : " ft=\L$lang\E") . ":";
215
216    if ($lang eq 'Pod') {
217        # nothing
218    } elsif ($lang eq 'C') {
219        $comment =~ s/^/ * /mg;
220        $comment =~ s! \* !/* !;
221        $comment .= " */";
222    } else {
223        $comment =~ s/^/# /mg;
224    }
225    print $fh "\n$comment\n";
226
227    close_and_rename($fh);
228
229    return;
230}
231
232sub tab {
233    no warnings 'numeric';
234    my ($l, $t) = @_;
235    $t .= "\t" x ($l - (length($t) + 1) / 8);
236    $t;
237}
238
239sub digest {
240    my $file = shift;
241    # Need to defer loading this, as the main regen scripts work back to 5.004,
242    # and likely we don't even have this module on every 5.8 install yet:
243    require Digest::SHA;
244
245    local ($/, *FH);
246    open FH, '<', $file or die "Can't open $file: $!";
247    my $raw = <FH>;
248    close FH or die "Can't close $file: $!";
249    return Digest::SHA::sha256_hex($raw);
250};
251
252sub wrap {
253    local $Text::Wrap::columns = shift;
254    local $Text::Wrap::unexpand = 0;
255    Text::Wrap::wrap(@_);
256}
257
258sub columnarize_list {
259    my $listp = shift;
260    my $max_width = shift;
261
262    # Returns the list (pointed to by 'listp') of text items, but arranged
263    # tabularly, like the default output of the 'ls' command.  The first few
264    # items of the list will be in the first column; the next batch in the
265    # second, etc, for as many columns as can fit in 'maxwidth' bytes, and as
266    # many rows as necessary.
267
268    use integer;
269
270    # Real data is unlikely to be able to fit more columns than this in a
271    # typical 80 byte window.  But obviously this could be changed or passed
272    # in.
273    my $max_columns = 7;
274
275    my $min_spacer = 2;     # Need this much space between columns
276    my $columns;
277    my $rows;
278    my @col_widths;
279
280  COLUMN:
281    # We start with more columns, and work down until we find a number that
282    # can accommodate all the data.  This algorithm doesn't require the
283    # resulting columns to all have the same width.  This can allow for
284    # as tight of packing as the data will possibly allow.
285    for ($columns = 7; $columns >= 1; $columns--) {
286
287        # For this many columns, we will need this many rows (final row might
288        # not be completely filled)
289        $rows = ($listp->@* + $columns - 1) / $columns;
290
291        # We only need to execute this final iteration to calculate the number
292        # of rows, as we can't get fewer than a single column.
293        last if $columns == 1;
294
295        my $row_width = 0;
296        my $i = 0;  # Which element of the input list
297
298        # For each column ...
299        for my $col (0 .. $columns - 1) {
300
301            # Calculate how wide the column needs to be, which is based on the
302            # widest element in it
303            $col_widths[$col] = 0;
304
305            # Look through all the rows to find the widest element
306            for my $row (0 .. $rows - 1) {
307
308                # Skip if this row doesn't have an entry for this column
309                last if $i >= $listp->@*;
310
311                # This entry occupies this many bytes.
312                my $this_width = length $listp->[$i];
313
314                # All but the final column need a spacer between it and the
315                # next column over.
316                $this_width += $min_spacer if $col < $columns - 1;
317
318
319                # This column will need to have enough width to accommodate
320                # this element
321                if ($this_width > $col_widths[$col]) {
322
323                    # We can't have this many columns if the total width
324                    # exceeds the available; bail now and try fewer columns
325                    next COLUMN if $row_width + $this_width > $max_width;
326
327                    $col_widths[$col] = $this_width;
328                }
329
330                $i++;   # The next row will contain the next item
331            }
332
333            $row_width += $col_widths[$col];
334            next COLUMN if $row_width > $max_width;
335        }
336
337        # If we get this far, this many columns works
338        last;
339    }
340
341    # Assemble the output
342    my $text = "";
343    for my $row (0 .. $rows - 1) {
344        for my $col (0 .. $columns - 1) {
345            my $index = $row + $rows * $col;  # Convert 2 dimensions to 1
346
347            # Skip if this row doesn't have an entry for this column
348            next if $index >= $listp->@*;
349
350            my $element = $listp->[$index];
351            $text .= $element;
352
353            # Add alignment spaces for all but final column
354            $text .= " " x ($col_widths[$col] - length $element)
355                                                        if $col < $columns - 1;
356        }
357
358        $text .= "\n";  # End of row
359    }
360
361    return $text;
362}
363
364# return the perl version as defined in patchlevel.h.
365# (we may be being run by another perl, so $] won't be right)
366# return e.g. (5, 14, 3, "5.014003")
367
368sub perl_version {
369    my $plh = 'patchlevel.h';
370    open my $fh, "<", $plh or die "can't open '$plh': $!\n";
371    my ($v1,$v2,$v3);
372    while (<$fh>) {
373        $v1 = $1 if /PERL_REVISION\s+(\d+)/;
374        $v2 = $1 if /PERL_VERSION\s+(\d+)/;
375        $v3 = $1 if /PERL_SUBVERSION\s+(\d+)/;
376    }
377    die "can't locate PERL_REVISION in '$plh'"   unless defined $v1;
378    die "can't locate PERL_VERSION in '$plh'"    unless defined $v2;
379    die "can't locate PERL_SUBVERSION in '$plh'" unless defined $v3;
380    return ($v1,$v2,$v3, sprintf("%d.%03d%03d", $v1, $v2, $v3));
381}
382
383
3841;
385