| 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 | } | |||