File Coverage

File:lib/Makefile/Update/MSBuild.pm
Coverage:98.6%

linestmtbrancondsubcode
1package Makefile::Update::MSBuild;
2# ABSTRACT: Update list of sources and headers in MSBuild projects.
3
4
10
10
10
use Exporter qw(import);
5our @EXPORT = qw(update_msbuild_project update_msbuild update_msbuild_filters);
6
7
10
10
10
use strict;
8
10
10
10
use warnings;
9
10# VERSION
11
12 - 25
=head1 SYNOPSIS

Given an MSBuild project C<project.vcxproj> and its associated filters file
C<projects.vcxproj.filters>, the functions in this module can be used to update
the list of files in them to correspond to the given ones.

    use Makefile::Update::MSBuild;
    upmake_msbuild_project('project.vcxproj', \@sources, \@headers);

=head1 SEE ALSO

Makefile::Update, Makefile::Update::VCProj

=cut
26
27=func update_msbuild_project
28
29Update sources and headers in an MSBuild project and filter files.
30
31Pass the path of the project to update or a hash with the same keys as used by
32C<Makefile::Update::upmake> as the first parameter and the references to the
33sources and headers arrays as the subsequent ones.
34
35Returns 1 if any changes were made, either to the project itself or to its
36associated C<.filters> file.
37=cut
38
39sub update_msbuild_project
40{
41
6
    my ($file_or_options, $sources, $headers) = @_;
42
43
10
10
10
    use Makefile::Update;
44
45
6
    if (!Makefile::Update::upmake($file_or_options,
46                \&update_msbuild, $sources, $headers
47            )) {
48
2
        return 0;
49    }
50
51
4
    my $args;
52
4
    if (ref $file_or_options eq 'HASH') {
53        # Need to make a copy to avoid modifying the callers hash.
54
4
        $args = { %$file_or_options };
55
4
        $args->{file} .= ".filters"
56    } else {
57
0
        $args = "$file_or_options.filters"
58    }
59
60
4
    return Makefile::Update::upmake($args,
61                \&update_msbuild_filters, $sources, $headers
62            );
63}
64
65
66=func update_msbuild
67
68Update sources and headers in an MSBuild project.
69
70Parameters: input and output file handles and array references to the sources
71and the headers to be used in this project.
72
73Returns 1 if any changes were made.
74=cut
75
76sub update_msbuild
77{
78
14
    my ($in, $out, $sources, $headers) = @_;
79
80    # Hashes mapping the sources/headers names to 1 if they have been seen in
81    # the project or 0 otherwise.
82
14
18
    my %sources = map { $_ => 0 } @$sources;
83
14
12
    my %headers = map { $_ => 0 } @$headers;
84
85    # Reference to the hash corresponding to the files currently being
86    # processed.
87
14
    my $files;
88
89    # Set to 1 when we are inside any <ItemGroup> tag.
90
14
    my $in_group = 0;
91
92    # Set to 1 when we are inside an item group containing sources or headers
93    # respectively.
94
14
    my ($in_sources, $in_headers) = 0;
95
96    # Set to 1 if we made any changes.
97
14
    my $changed = 0;
98
14
    while (my $line_with_eol = <$in>) {
99
256
        (my $line = $line_with_eol) =~ s/\r?\n?$//;
100
101
256
        if ($line =~ /^\s*<ItemGroup>$/) {
102
22
            $in_group = 1;
103        } elsif ($line =~ m{^\s*</ItemGroup>$}) {
104
36
            if (defined $files) {
105
22
                my $kind = $in_sources ? 'Compile' : 'Include';
106
107                # Check if we have any new files.
108                #
109                # TODO Insert them in alphabetical order.
110
22
                while (my ($file, $seen) = each(%$files)) {
111
28
                    if (!$seen) {
112                        # Convert path separator to the one used by MSBuild.
113
10
                        $file =~ s@/@\\@g;
114
115
10
                        print $out qq{    <Cl$kind Include="$file" />\r\n};
116
117
10
                        $changed = 1;
118                    }
119                }
120
121
22
                $in_sources = $in_headers = 0;
122
22
                $files = undef;
123            }
124
125
36
            $in_group = 0;
126        } elsif ($in_group) {
127
38
            if ($line =~ m{^\s*<Cl(?<kind>Compile|Include) Include="(?<file>[^"]+)"\s*(?<slash>/)?>$}) {
128
10
10
10
38
                my $kind = $+{kind};
129
38
                if ($kind eq 'Compile') {
130
22
                    warn "Mix of sources and headers at line $.\n" if $in_headers;
131
22
                    $in_sources = 1;
132
22
                    $files = \%sources;
133                } else {
134
16
                    warn "Mix of headers and sources at line $.\n" if $in_sources;
135
16
                    $in_headers = 1;
136
16
                    $files = \%headers;
137                }
138
139
38
                my $closed_tag = defined $+{slash};
140
141                # Normalize the path separator, we always use Unix ones but the
142                # project files use Windows one.
143
38
                my $file = $+{file};
144
38
                $file =~ s@\\@/@g;
145
146
38
                if (not exists $files->{$file}) {
147                    # This file was removed.
148
16
                    $changed = 1;
149
150
16
                    if (!$closed_tag) {
151                        # We have just the opening <ClCompile> or <ClInclude>
152                        # tag, ignore everything until the matching closing one.
153
2
                        my $tag = "Cl$kind";
154
2
                        while (<$in>) {
155
4
                            last if m{^\s*</$tag>\r?\n$};
156                        }
157                    }
158
159                    # In any case skip either this line containing the full
160                    # <ClCompile/> tag or the line with the closing tag.
161
16
                    next;
162                } else {
163
22
                    if ($files->{$file}) {
164
2
                        warn qq{Duplicate file "$file" in the project at line $.\n};
165                    } else {
166
20
                        $files->{$file} = 1;
167                    }
168                }
169            }
170        }
171
172
240
        print $out $line_with_eol;
173    }
174
175    $changed
176
14
}
177
178=func update_msbuild_filters
179
180Update sources and headers in an MSBuild filters file.
181
182Parameters: input and output file handles, array references to the sources
183and the headers to be used in this project and a callback used to determine
184the filter for the new files.
185
186Returns 1 if any changes were made.
187=cut
188
189sub update_msbuild_filters
190{
191
14
    my ($in, $out, $sources, $headers, $filter_cb) = @_;
192
193    # Use standard/default classifier for the files if none is explicitly
194    # specified.
195
14
    if (!defined $filter_cb) {
196        $filter_cb = sub {
197
8
            my ($file) = @_;
198
199
8
            return 'Source Files' if $file =~ q{\.c(c|pp|xx|\+\+)?$};
200
4
            return 'Header Files' if $file =~ q{\.h(h|pp|xx|\+\+)?$};
201
202
2
            warn qq{No filter defined for the file "$file".\n};
203
204            undef
205
2
        }
206
12
    }
207
208    # Hashes mapping the sources/headers names to the text representing them in
209    # the input file if they have been seen in it or nothing otherwise.
210
14
22
    my %sources = map { $_ => undef } @$sources;
211
14
10
    my %headers = map { $_ => undef } @$headers;
212
213    # Reference to the hash corresponding to the files currently being
214    # processed.
215
14
    my $files;
216
217    # Set to 1 when we are inside any <ItemGroup> tag.
218
14
    my $in_group = 0;
219
220    # Set to 1 when we are inside an item group containing sources or headers
221    # respectively.
222
14
    my ($in_sources, $in_headers) = 0;
223
224    # Set to 1 if we made any changes.
225
14
    my $changed = 0;
226
14
    while (my $line_with_eol = <$in>) {
227
238
        (my $line = $line_with_eol) =~ s/\r?\n?$//;
228
229
238
        if ($line =~ /^\s*<ItemGroup>?$/) {
230
34
            $in_group = 1;
231        } elsif ($line =~ m{^\s*</ItemGroup>?$}) {
232
34
            if (defined $files) {
233                # Output the group contents now, all at once, inserting any new
234                # files: we must do it like this to ensure that they are
235                # inserted in alphabetical order.
236
20
                my $kind = $in_sources ? 'Compile' : 'Include';
237
238
20
                foreach my $file (sort keys %$files) {
239
30
                    if (defined $files->{$file}) {
240
16
                        print $out $files->{$file};
241                    } else {
242
14
                        my $filter = $filter_cb->($file);
243
244                        # Convert path separator to the one used by MSBuild.
245
14
                        $file =~ s@/@\\@g;
246
247
14
                        my $indent = ' ' x 2;
248
249
14
                        print $out qq{$indent$indent<Cl$kind Include="$file"};
250
14
                        if (defined $filter) {
251
10
                            print $out ">\r\n$indent$indent$indent<Filter>$filter</Filter>\r\n$indent$indent</Cl$kind>\r\n";
252                        } else {
253
4
                            print $out " />\r\n";
254                        }
255
256
14
                        $changed = 1;
257                    }
258                }
259
260
20
                $in_sources = $in_headers = 0;
261
20
                $files = undef;
262            }
263
264
34
            $in_group = 0;
265        } elsif ($in_group &&
266                 $line =~ m{^\s*<Cl(?<kind>Compile|Include) Include="(?<file>[^"]+)"\s*(?<slash>/)?>?$}) {
267
36
            my $kind = $+{kind};
268
36
            if ($kind eq 'Compile') {
269
22
                warn "Mix of sources and headers at line $.\n" if $in_headers;
270
22
                $in_sources = 1;
271
22
                $files = \%sources;
272            } else {
273
14
                warn "Mix of headers and sources at line $.\n" if $in_sources;
274
14
                $in_headers = 1;
275
14
                $files = \%headers;
276            }
277
278
36
            my $closed_tag = defined $+{slash};
279
280            # Normalize the path separator, we always use Unix ones but the
281            # project files use Windows one.
282
36
            my $file = $+{file};
283
36
            $file =~ s@\\@/@g;
284
285
36
            my $text = $line_with_eol;
286
36
            if (!$closed_tag) {
287                # We have just the opening <ClCompile> tag, get everything
288                # until the next </ClCompile>.
289
28
                while (<$in>) {
290
56
                    $text .= $_;
291
56
                    last if m{^\s*</Cl$kind>\r?\n?$};
292                }
293            }
294
295
36
            if (not exists $files->{$file}) {
296                # This file was removed.
297
16
                $changed = 1;
298            } else {
299
20
                if ($files->{$file}) {
300
2
                    warn qq{Duplicate file "$file" in the project at line $.\n};
301                } else {
302
18
                    $files->{$file} = $text;
303                }
304            }
305
306            # Don't output this line yet, wait until the end of the group.
307            next
308
36
        }
309
310
202
        print $out $line_with_eol;
311    }
312
313    $changed
314
14
}