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

# File Coverage

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

line | stmt | bran | cond | sub | code
--- | --- | --- | --- | --- | ---
1 |  |  |  |  | package Makefile::Update::MSBuild;
2 |  |  |  |  | # ABSTRACT: Update list of sources and headers in MSBuild projects.
3 |
4 | 10 10 10 |  |  | 10 | use Exporter qw(import);
5 |  |  |  |  | our @EXPORT = qw(update_msbuild_project update_msbuild update_msbuild_filters);
6 |
7 | 10 10 10 |  |  | 10 | use strict;
8 | 10 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 |
29 |  |  |  |  | Update sources and headers in an MSBuild project and filter files.
30 |
31 |  |  |  |  | Pass the path of the project to update or a hash with the same keys as used by
32 |  |  |  |  | C<Makefile::Update::upmake> as the first parameter and the references to the
33 |  |  |  |  | sources and headers arrays as the subsequent ones.
34 |
35 |  |  |  |  | Returns 1 if any changes were made, either to the project itself or to its
36 |  |  |  |  | associated C<.filters> file.
37 |  |  |  |  | =cut
38 |
39 |  |  |  |  | sub update_msbuild_project
40 |  |  |  |  | {
41 | 6 |  |  | 6 | my ($file_or_options, $sources, $headers) = @_;
42 |
43 | 10 10 10 |  |  | 10 | use Makefile::Update;
44 |
45 | 6 | 100 |  |  | 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 | 50 |  |  | 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 |
68 |  |  |  |  | Update sources and headers in an MSBuild project.
69 |
70 |  |  |  |  | Parameters: input and output file handles and array references to the sources
71 |  |  |  |  | and the headers to be used in this project.
72 |
73 |  |  |  |  | Returns 1 if any changes were made.
74 |  |  |  |  | =cut
75 |
76 |  |  |  |  | sub update_msbuild
77 |  |  |  |  | {
78 | 14 |  |  | 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 | 100 100 100 |  |  | if ($line =~ /^\s*<ItemGroup>$/) {
102 | 22 |  |  |  | $in_group = 1;
103 |  |  |  |  | } elsif ($line =~ m{^\s*</ItemGroup>$}) {
104 | 36 | 100 |  |  | if (defined $files) {
105 | 22 | 100 |  |  | 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 | 100 |  |  | 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 | 50 |  |  | if ($line =~ m{^\s*<Cl(?<kind>Compile|Include) Include="(?<file>[^"]+)"\s*(?<slash>/)?>$}) {
128 | 10 10 10 38 |  |  | 10 | my $kind = $+{kind};
129 | 38 | 100 |  |  | if ($kind eq 'Compile') {
130 | 22 | 100 |  |  | 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 | 100 |  |  | 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 | 100 |  |  | if (not exists $files->{$file}) {
147 |  |  |  |  | # This file was removed.
148 | 16 |  |  |  | $changed = 1;
149 |
150 | 16 | 100 |  |  | 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 | 100 |  |  | 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 | 100 |  |  | 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 |
180 |  |  |  |  | Update sources and headers in an MSBuild filters file.
181 |
182 |  |  |  |  | Parameters: input and output file handles, array references to the sources
183 |  |  |  |  | and the headers to be used in this project and a callback used to determine
184 |  |  |  |  | the filter for the new files.
185 |
186 |  |  |  |  | Returns 1 if any changes were made.
187 |  |  |  |  | =cut
188 |
189 |  |  |  |  | sub update_msbuild_filters
190 |  |  |  |  | {
191 | 14 |  |  | 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 | 100 |  |  | if (!defined $filter_cb) {
196 |  |  |  |  | $filter_cb = sub {
197 | 8 |  |  | 8 | my ($file) = @_;
198 |
199 | 8 | 100 |  |  | return 'Source Files' if $file =~ q{\.c(c|pp|xx|\+\+)?$};
200 | 4 | 100 |  |  | 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 | 100 100 100 | 100 |  | if ($line =~ /^\s*<ItemGroup>?$/) {
230 | 34 |  |  |  | $in_group = 1;
231 |  |  |  |  | } elsif ($line =~ m{^\s*</ItemGroup>?$}) {
232 | 34 | 100 |  |  | 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 | 100 |  |  | my $kind = $in_sources ? 'Compile' : 'Include';
237 |
238 | 20 |  |  |  | foreach my $file (sort keys %$files) {
239 | 30 | 100 |  |  | 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 | 100 |  |  | 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 | 100 |  |  | if ($kind eq 'Compile') {
269 | 22 | 100 |  |  | 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 | 100 |  |  | 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 | 100 |  |  | 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 | 100 |  |  | last if m{^\s*</Cl$kind>\r?\n?$};
292 |  |  |  |  | }
293 |  |  |  |  | }
294 |
295 | 36 | 100 |  |  | if (not exists $files->{$file}) {
296 |  |  |  |  | # This file was removed.
297 | 16 |  |  |  | $changed = 1;
298 |  |  |  |  | } else {
299 | 20 | 100 |  |  | 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 |  |  |  | }