File Coverage

File:lib/Makefile/Update/Makefile.pm
Coverage:91.1%

linestmtbrancondsubcode
1package Makefile::Update::Makefile;
2# ABSTRACT: Update lists of files in makefile variables.
3
4
12
12
12
use Exporter qw(import);
5our @EXPORT = qw(update_makefile);
6
7
12
12
12
use strict;
8
12
12
12
use warnings;
9
10# VERSION
11
12 - 24
=head1 SYNOPSIS

This can be used to update the contents of a variable containing a list of
files in a makefile.

    use Makefile::Update::Makefile;
    Makefile::Update::upmake('GNUmakefile', \&update_makefile, $vars);

=head1 SEE ALSO

Makefile::Update

=cut
25
26=func update_makefile
27
28Update variable definitions in a makefile format with the data from the hash
29ref containing all the file lists.
30
31Only most straightforward cases of variable or target definitions are
32recognized here, i.e. just "var := value", "var = value" or "target: value".
33In particular we don't support any GNU make extensions such as "export" or
34"override" without speaking of anything more complex.
35
36On top of it, currently the value should contain a single file per line with
37none at all on the first line (but this restriction could be relaxed later if
38needed), i.e. the only supported case is
39
40    var = \
41          foo \
42          bar \
43          baz
44
45and it must be followed by an empty line, too.
46
47Notice that if any of the "files" in the variable value looks like a makefile
48variable, i.e. has "$(foo)" form, it is ignored by this function, i.e. not
49removed even if it doesn't appear in the list of files (which will never be
50the case normally).
51
52Takes the (open) file handles of the files to read and to write and the file
53lists hash ref as arguments.
54
55Returns 1 if any changes were made.
56=cut
57
58sub update_makefile
59{
60
60
    my ($in, $out, $vars) = @_;
61
62    # Variable whose contents is being currently replaced and its original
63    # name in the makefile.
64
60
    my ($var, $makevar);
65
66    # Hash with files defined for the specified variable as keys and 0 or 1
67    # depending on whether we have seen them in the input file as values.
68
0
    my %files;
69
70    # Array of lines in the existing makefile.
71
0
    my @values;
72
73    # True if the values are in alphabetical order: we use this to add new
74    # entries in alphabetical order too if the existing ones use it, otherwise
75    # we just append them at the end.
76
60
    my $sorted = 1;
77
78    # Extensions of the files in the files list (they're keys of this hash,
79    # the values are not used), there can be more than one (e.g. ".c" and
80    # ".cpp").
81
60
    my %src_exts;
82
83    # Extension of the files in the makefiles: here there can also be more
84    # than one, but in this case we just give up and don't perform any
85    # extensions translation because we don't have enough information to do it
86    # (e.g. which extension should be used for the new files in the makefile?).
87    # Such case is indicated by make_ext being empty (as opposed to its
88    # initial undefined value).
89    my $make_ext;
90
91    # Helper to get the extension. Note that the "extension" may be a make
92    # variable, e.g. the file could be something like "foo.$(obj)", so don't
93    # restrict it to just word characters.
94
264
    sub _get_ext { $_[0] =~ /(\.\S+)$/ ? $1 : undef }
95
96    # Indent and the part after the value (typically some amount of spaces and
97    # a backslash) for normal lines and, separately, for the last one, as it
98    # may or not have backslash after it.
99
60
    my ($indent, $tail, $last_tail);
100
101    # We can't use the usual check for EOF inside while itself because this
102    # wouldn't work for files with no new line after the last line, so check
103    # for the EOF manually.
104
60
    my $eof = 0;
105
106    # Set to 1 if we made any changes.
107
60
    my $changed = 0;
108
60
    while (1) {
109
339
        my $line = <$in>;
110
339
        if (defined $line) {
111
279
            chomp $line;
112        } else {
113
60
            $line = '';
114
60
            $eof = 1;
115        }
116
117        # If we're inside the variable definition, parse the current line as
118        # another file name,
119
339
        if (defined $var) {
120
204
            if ($line =~ /^(?<indent>\s*)(?<file>[^ ]+)(?<tail>\s*\\?)$/) {
121
135
                if (defined $indent) {
122                    warn qq{Inconsistent indent at line $. in the } .
123                         qq{definition of the variable "$makevar".\n}
124
12
12
12
66
                        if $+{indent} ne $indent;
125                } else {
126
69
                    $indent = $+{indent};
127                }
128
129
135
                $last_tail = $+{tail};
130
135
                my $file_orig = $+{file};
131
132
135
                $tail = $last_tail if !defined $tail;
133
134                # Check if we have something with the correct extension and
135                # preserve unchanged all the rest -- we don't want to remove
136                # expansions of other makefile variables from this one, for
137                # example, but such expansions would never be in the files
138                # list as they don't make sense for the other formats.
139
135
                my $file = $file_orig;
140
135
                if (defined (my $file_ext = _get_ext($file))) {
141
132
                    if (defined $make_ext) {
142
63
                        if ($file_ext ne $make_ext) {
143                            # As explained in the comment before make_ext
144                            # definition, just don't do anything in this case.
145
0
                            $make_ext = '';
146                        }
147                    } else {
148
69
                        $make_ext = $file_ext;
149                    }
150
151                    # We need to try this file with all of the source
152                    # extensions we have as it can correspond to any of them.
153
132
                    for my $src_ext (keys %src_exts) {
154
147
                        if ($file_ext ne $src_ext) {
155
51
                            (my $file_try = $file) =~ s/\Q$file_ext\E$/$src_ext/;
156
51
                            if (exists $files{$file_try}) {
157
21
                                $file = $file_try;
158                                last
159
21
                            }
160                        }
161                    }
162
163
132
                    if (!exists $files{$file}) {
164                        # This file was removed.
165
42
                        $changed = 1;
166
167                        # Don't store this line in @values below.
168
42
                        next;
169                    }
170                }
171
172
93
                if (exists $files{$file}) {
173
90
                    if ($files{$file}) {
174
3
                        warn qq{Duplicate file "$file" in the definition of the } .
175                             qq{variable "$makevar" at line $.\n}
176                    } else {
177
87
                        $files{$file} = 1;
178                    }
179                }
180
181                # Are we still sorted?
182
93
                if (@values && lc $line lt $values[-1]) {
183
9
                    $sorted = 0;
184                }
185
186
93
                push @values, $line;
187
93
                next;
188            }
189
190            # If the last line had a continuation character, the file list
191            # should only end if there is nothing else on the following line.
192
69
            if ($last_tail =~ /\\$/ && $line =~ /\S/) {
193
3
                warn qq{Expected blank line at line $..\n};
194            }
195
196            # End of variable definition, add new lines.
197
198            # We can only map the extensions if we have a single extension to
199            # map them to (i.e. make_ext is not empty) and we only need to do
200            # it if are using more than one extension in the source files list
201            # or the single extension that we use is different from make_ext.
202
69
            if (defined $make_ext) {
203
69
                if ($make_ext eq '' ||
204                        (keys %src_exts == 1 && exists $src_exts{$make_ext})) {
205
57
                    undef $make_ext
206                }
207            }
208
209
69
            my $new_files = 0;
210
69
            while (my ($file, $seen) = each(%files)) {
211
129
                next if $seen;
212
213                # This file was wasn't present in the input, add it.
214
215                # If this is the first file we add, ensure that the last line
216                # present in the makefile so far has the line continuation
217                # character at the end as this might not have been the case.
218
42
                if (!$new_files) {
219
36
                    $new_files = 1;
220
221
36
                    if (@values && $values[-1] !~ /\\$/) {
222
12
                        $values[-1] .= $tail;
223                    }
224                }
225
226                # Next give it the right extension.
227
42
                if (defined $make_ext) {
228
9
                    $file =~ s/\.\S+$/$make_ext/
229                }
230
231                # Finally store it.
232
42
                push @values, "$indent$file$tail";
233            }
234
235
69
            if ($new_files) {
236
36
                $changed = 1;
237
238                # Sort them if necessary using the usual Schwartzian transform.
239
36
                if ($sorted) {
240
78
                    @values = map { $_->[0] }
241
72
                              sort { $a->[1] cmp $b->[1] }
242
36
78
                              map { [$_, lc $_] } @values;
243                }
244
245                # Fix up the tail of the last line to be the same as that of
246                # the previous last line.
247
36
                $values[-1] =~ s/\s*\\$/$last_tail/;
248            }
249
250
69
            undef $var;
251
252
69
            print $out join("\n", @values), "\n";
253        }
254
255        # We're only interested in variable or target declarations, and does
256        # not look like target-specific variable (this would contain an equal
257        # sign after the target).
258
204
        if ($line =~ /^\s*(?<var>\S+)\s*(?::?=|:)(?<tail>[^=]*)$/) {
259
72
            $makevar = $+{var};
260
72
            my $tail = $+{tail};
261
262            # And only those of them for which we have values, but this is
263            # where it gets tricky as we try to be smart to accommodate common
264            # use patterns with minimal effort.
265
72
            if (exists $vars->{$makevar}) {
266
18
                $var = $makevar;
267            } else {
268                # Helper: return name if a variable with such name exists or
269                # undef otherwise.
270
54
81
                my $var_if_exists = sub { exists $vars->{$_[0]} ? $_[0] : undef };
271
272
54
                if ($makevar =~ /^objects$/i || $makevar =~ /^obj$/i) {
273                    # Special case: map it to "sources" as we work with the
274                    # source, not object, files.
275
3
                    $var = $var_if_exists->('sources');
276                } elsif ($makevar =~ /^(\w+)_(objects|obj|sources|src)$/i) {
277                    # Here we deal with "foo_sources" typically found in
278                    # hand-written makefiles but also "foo_SOURCES" used in
279                    # automake ones, but the latter also uses libfoo_a_SOURCES
280                    # for static libraries and libfoo_la_SOURCES for the
281                    # libtool libraries, be smart about it to allow defining
282                    # just "foo" or "foo_sources" variables usable with all
283                    # kinds of make/project files.
284
48
                    $var = $var_if_exists->($1) || $var_if_exists->("$1_sources");
285
48
                    if (!defined $var && $2 eq 'SOURCES' && $1 =~ /^(\w+)_l?a$/) {
286
9
                        $var = $var_if_exists->($1) || $var_if_exists->("$1_sources");
287
9
                        if (!defined $var && $1 =~ /^lib(\w+)$/) {
288
3
                            $var = $var_if_exists->($1) || $var_if_exists->("$1_sources");
289                        }
290                    }
291                } elsif ($makevar =~ /^(\w+)\$\(\w+\)/) {
292                    # This one is meant to catch relatively common makefile
293                    # constructions like "target$(exe_ext)".
294
3
                    $var = $var_if_exists->($1);
295                }
296            }
297
298
72
            if (defined $var) {
299
72
                if ($tail !~ /\s*\\$/) {
300
3
                    warn qq{Unsupported format for variable "$makevar" at line $..\n};
301
3
                    undef $var;
302                } else {
303
69
129
69
                    %files = map { $_ => 0 } @{$vars->{$var}};
304
305
69
                    @values = ();
306
307                    # Find all the extensions used by files in this variable.
308
69
69
                    for my $file (@{$vars->{$var}}) {
309
129
                        if (defined (my $src_ext = _get_ext($file))) {
310
129
                            $src_exts{$src_ext} = 1;
311                        }
312                    }
313
314                    # Not known yet.
315
69
                    undef $make_ext;
316
317
69
                    undef $indent;
318
69
                    $tail = $tail;
319
69
                    undef $last_tail;
320
321                    # Not unsorted so far.
322
69
                    $sorted = 1;
323                }
324            }
325        }
326
327
204
        print $out "$line";
328
329        # Don't add an extra new line at the EOF if it hadn't been there.
330
204
        last if $eof;
331
332
144
        print $out "\n";
333    }
334
335    $changed
336
60
}