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 | use Exporter qw(import); | |||
5 | our @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 | |||||
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 | 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 | |||||
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 | 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 | |||||
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 | 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 | } |