#!/usr/bin/env perl # This chunk of stuff was generated by App::FatPacker. To find the original # file's code, look for the end of this BEGIN block or the string 'FATPACK' BEGIN { my %fatpacked; $fatpacked{"Makefile/Update.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'MAKEFILE_UPDATE'; package Makefile::Update; # ABSTRACT: Update make files. use strict; use warnings; use autodie; use Exporter qw(import); our @EXPORT = qw(read_files_list upmake); our $VERSION = '0.3'; # VERSION sub read_files_list { my ($fh) = @_; my ($var, %vars); while (<$fh>) { chomp; s/#.*$//; s/^\s+//; s/\s+$//; next if !$_; if (/^(\w+)\s*=$/) { $var = $1; } else { die "Unexpected contents outside variable definition at line $.\n" unless defined $var; if (/^\$(\w+)$/) { my $name = $1; die qq{Reference to undefined variable "$name" in the } . qq{assignment to "$var" at line $.\n} unless exists $vars{$name}; my $value = $vars{$name}; push @{$vars{$var}}, $_ for @$value; } else { push @{$vars{$var}}, $_; } } } return \%vars; } sub upmake { my $file_or_options = shift; my ($updater, @args) = @_; my ($fname, $verbose, $quiet, $dryrun); if (ref $file_or_options eq 'HASH') { $fname = $file_or_options->{file}; $verbose = $file_or_options->{verbose}; $quiet = $file_or_options->{quiet}; $dryrun = $file_or_options->{dryrun}; } else { $fname = $file_or_options; $verbose = $quiet = $dryrun = 0; } if ($dryrun) { my $old = do { local $/; open my $f, '<', $fname; <$f> }; my $new = ''; open my $in, '<', \$old; open my $out, '>', \$new; if ($updater->($in, $out, @args)) { print qq{Would update "$fname"}; if ($verbose) { if (eval { require Text::Diff; }) { print " with the following changes:\n"; print Text::Diff::diff(\$old, \$new, { FILENAME_A => $fname, FILENAME_B => "$fname.new" }); } else { print ".\n"; warn qq{Can't display diff of the changes, please install Text::Diff module.\n}; } } else { print ".\n"; } } else { print qq{Wouldn't change the file "$fname".\n}; } return 0; } my $fname_new = "$fname.upmake.new"; # TODO make it more unique open my $in, '<', $fname; open my $out, '>', $fname_new; my $changed = $updater->($in, $out, @args); close $in; close $out; if ($changed) { rename $fname_new, $fname; } else { unlink $fname_new; } if ($changed) { print qq{File "$fname" successfully updated.\n} unless $quiet; return 1; } else { print qq{No changes in the file "$fname".\n} if $verbose; return 0; } } 1; __END__ =pod =encoding UTF-8 =head1 NAME Makefile::Update - Update make files. =head1 VERSION version 0.3 =head1 SYNOPSIS use Makefile::Update; my $vars = read_files_list('files.lst'); upmake('foo.vcxproj', $vars->{sources}, $vars->{headers}); =head1 FUNCTIONS =head2 read_files_list Reads the file containing the file lists definitions and returns a hash ref with variable names as keys and refs to arrays of the file names as values. Takes an (open) file handle as argument. The file contents is supposed to have the following very simple format: # Comments are allowed and ignored. # # The variable definitions must always be in the format shown below, # i.e. whitespace is significant and there should always be a single # file per line. sources = file1.cpp file2.cpp headers = file1.h file2.h # It is also possible to define variables in terms of other variables # defined before it in the file (no forward references): everything = $sources $headers =head2 upmake Update a file in place using the specified function and passing it the rest of the arguments. The first parameter is either just the file path or a hash reference which may contain the following keys: =over =item C The path to the file to be updated, required. =item C If true, give more messages about what is being done. =item C If true, don't output any non-error messages. =item C If true, don't really update the file but just output whether it would have been updated or not. If C is also true, also output the diff of the changes that would have been done. =back This is meant to be used with C defined in different Makefile::Update::Xxx modules. Returns 1 if the file was changed or 0 otherwise. =head1 AUTHOR Vadim Zeitlin =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2015 by Vadim Zeitlin. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut MAKEFILE_UPDATE $fatpacked{"Makefile/Update/Bakefile0.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'MAKEFILE_UPDATE_BAKEFILE0'; package Makefile::Update::Bakefile0; # ABSTRACT: Update bakefile-0.x files list. use Exporter qw(import); our @EXPORT = qw(update_bakefile_0); use strict; use warnings; our $VERSION = '0.3'; # VERSION sub update_bakefile_0 { my ($in, $out, $vars) = @_; # Variable whose contents is being currently replaced. my $var; # Hash with files defined for the specified variable as keys and 0 or 1 # depending on whether we have seen them in the input file as values. my %files; # Set to 1 if we made any changes. my $changed = 0; while (<$in>) { chomp; if (// && exists $vars->{$1}) { $var = $1; %files = map { $_ => 0 } @{$vars->{$var}}; } elsif (defined $var) { local $_ = $_; s///; s/^\s+//; s/\s+$//; if (m{}) { # Check if we have any new files. # # TODO Insert them in alphabetical order. while (my ($file, $seen) = each(%files)) { if (!$seen) { # This file was wasn't present in the input, add it. # TODO Use proper indentation. print $out " $file\n"; $changed = 1; } } undef $var; } elsif ($_) { if (not exists $files{$_}) { # This file was removed. $changed = 1; next; } if ($files{$_}) { warn qq{Duplicate file "$_" in the definition of the } . qq{variable "$var" at line $.\n} } else { $files{$_} = 1; } } } print $out "$_\n"; } $changed } __END__ =pod =encoding UTF-8 =head1 NAME Makefile::Update::Bakefile0 - Update bakefile-0.x files list. =head1 VERSION version 0.3 =head1 SYNOPSIS This is used exclusively to update wxWidgets C and is probably not useful outside of wxWidgets project. use Makefile::Update::Bakefile0; Makefile::Update::upmake('bakefiles/files.bkl', \&update_bakefile_0, $vars); =head1 FUNCTIONS =head2 update_bakefile_0 Update file with variable definitions in bakefile-0 format with the data from the hash ref containing all the file lists. Takes the (open) file handles of the files to read and to write and the file lists hash ref as arguments. Returns 1 if any changes were made. =head1 SEE ALSO Makefile::Update =head1 AUTHOR Vadim Zeitlin =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2015 by Vadim Zeitlin. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut MAKEFILE_UPDATE_BAKEFILE0 $fatpacked{"Makefile/Update/MSBuild.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'MAKEFILE_UPDATE_MSBUILD'; package Makefile::Update::MSBuild; # ABSTRACT: Update list of sources and headers in MSBuild projects. use Exporter qw(import); our @EXPORT = qw(update_msbuild_project update_msbuild update_msbuild_filters); use strict; use warnings; our $VERSION = '0.3'; # VERSION sub update_msbuild_project { my ($file_or_options, $sources, $headers) = @_; use Makefile::Update; if (!Makefile::Update::upmake($file_or_options, \&update_msbuild, $sources, $headers )) { return 0; } my $args; if (ref $file_or_options eq 'HASH') { # Need to make a copy to avoid modifying the callers hash. $args = { %$file_or_options }; $args->{file} .= ".filters" } else { $args = "$file_or_options.filters" } return Makefile::Update::upmake($args, \&update_msbuild_filters, $sources, $headers ); } sub update_msbuild { my ($in, $out, $sources, $headers) = @_; # Hashes mapping the sources/headers names to 1 if they have been seen in # the project or 0 otherwise. my %sources = map { $_ => 0 } @$sources; my %headers = map { $_ => 0 } @$headers; # Reference to the hash corresponding to the files currently being # processed. my $files; # Set to 1 when we are inside any tag. my $in_group = 0; # Set to 1 when we are inside an item group containing sources or headers # respectively. my ($in_sources, $in_headers) = 0; # Set to 1 if we made any changes. my $changed = 0; while (my $line_with_eol = <$in>) { (my $line = $line_with_eol) =~ s/\r?\n?$//; if ($line =~ /^\s*$/) { $in_group = 1; } elsif ($line =~ m{^\s*$}) { if (defined $files) { my $kind = $in_sources ? 'Compile' : 'Include'; # Check if we have any new files. # # TODO Insert them in alphabetical order. while (my ($file, $seen) = each(%$files)) { if (!$seen) { # Convert path separator to the one used by MSBuild. $file =~ s@/@\\@g; print $out qq{ \r\n}; $changed = 1; } } $in_sources = $in_headers = 0; $files = undef; } $in_group = 0; } elsif ($in_group) { if ($line =~ m{^\s*Compile|Include) Include="(?[^"]+)"\s*(?/)?>$}) { my $kind = $+{kind}; if ($kind eq 'Compile') { warn "Mix of sources and headers at line $.\n" if $in_headers; $in_sources = 1; $files = \%sources; } else { warn "Mix of headers and sources at line $.\n" if $in_sources; $in_headers = 1; $files = \%headers; } my $closed_tag = defined $+{slash}; # Normalize the path separator, we always use Unix ones but the # project files use Windows one. my $file = $+{file}; $file =~ s@\\@/@g; if (not exists $files->{$file}) { # This file was removed. $changed = 1; if (!$closed_tag) { # We have just the opening or # tag, ignore everything until the matching closing one. my $tag = "Cl$kind"; while (<$in>) { last if m{^\s*\r?\n$}; } } # In any case skip either this line containing the full # tag or the line with the closing tag. next; } else { if ($files->{$file}) { warn qq{Duplicate file "$file" in the project at line $.\n}; } else { $files->{$file} = 1; } } } } print $out $line_with_eol; } $changed } sub update_msbuild_filters { my ($in, $out, $sources, $headers, $filter_cb) = @_; # Use standard/default classifier for the files if none is explicitly # specified. if (!defined $filter_cb) { $filter_cb = sub { my ($file) = @_; return 'Source Files' if $file =~ q{\.c(c|pp|xx|\+\+)?$}; return 'Header Files' if $file =~ q{\.h(h|pp|xx|\+\+)?$}; warn qq{No filter defined for the file "$file".\n}; undef } } # Hashes mapping the sources/headers names to the text representing them in # the input file if they have been seen in it or nothing otherwise. my %sources = map { $_ => undef } @$sources; my %headers = map { $_ => undef } @$headers; # Reference to the hash corresponding to the files currently being # processed. my $files; # Set to 1 when we are inside any tag. my $in_group = 0; # Set to 1 when we are inside an item group containing sources or headers # respectively. my ($in_sources, $in_headers) = 0; # Set to 1 if we made any changes. my $changed = 0; while (my $line_with_eol = <$in>) { (my $line = $line_with_eol) =~ s/\r?\n?$//; if ($line =~ /^\s*?$/) { $in_group = 1; } elsif ($line =~ m{^\s*?$}) { if (defined $files) { # Output the group contents now, all at once, inserting any new # files: we must do it like this to ensure that they are # inserted in alphabetical order. my $kind = $in_sources ? 'Compile' : 'Include'; foreach my $file (sort keys %$files) { if (defined $files->{$file}) { print $out $files->{$file}; } else { my $filter = $filter_cb->($file); # Convert path separator to the one used by MSBuild. $file =~ s@/@\\@g; my $indent = ' ' x 2; print $out qq{$indent$indent\r\n$indent$indent$indent$filter\r\n$indent$indent\r\n"; } else { print $out " />\r\n"; } $changed = 1; } } $in_sources = $in_headers = 0; $files = undef; } $in_group = 0; } elsif ($in_group && $line =~ m{^\s*Compile|Include) Include="(?[^"]+)"\s*(?/)?>?$}) { my $kind = $+{kind}; if ($kind eq 'Compile') { warn "Mix of sources and headers at line $.\n" if $in_headers; $in_sources = 1; $files = \%sources; } else { warn "Mix of headers and sources at line $.\n" if $in_sources; $in_headers = 1; $files = \%headers; } my $closed_tag = defined $+{slash}; # Normalize the path separator, we always use Unix ones but the # project files use Windows one. my $file = $+{file}; $file =~ s@\\@/@g; my $text = $line_with_eol; if (!$closed_tag) { # We have just the opening tag, get everything # until the next . while (<$in>) { $text .= $_; last if m{^\s*\r?\n?$}; } } if (not exists $files->{$file}) { # This file was removed. $changed = 1; } else { if ($files->{$file}) { warn qq{Duplicate file "$file" in the project at line $.\n}; } else { $files->{$file} = $text; } } # Don't output this line yet, wait until the end of the group. next } print $out $line_with_eol; } $changed } __END__ =pod =encoding UTF-8 =head1 NAME Makefile::Update::MSBuild - Update list of sources and headers in MSBuild projects. =head1 VERSION version 0.3 =head1 SYNOPSIS Given an MSBuild project C and its associated filters file C, 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 FUNCTIONS =head2 update_msbuild_project Update sources and headers in an MSBuild project and filter files. Pass the path of the project to update or a hash with the same keys as used by C as the first parameter and the references to the sources and headers arrays as the subsequent ones. Returns 1 if any changes were made, either to the project itself or to its associated C<.filters> file. =head2 update_msbuild Update sources and headers in an MSBuild project. Parameters: input and output file handles and array references to the sources and the headers to be used in this project. Returns 1 if any changes were made. =head2 update_msbuild_filters Update sources and headers in an MSBuild filters file. Parameters: input and output file handles, array references to the sources and the headers to be used in this project and a callback used to determine the filter for the new files. Returns 1 if any changes were made. =head1 SEE ALSO Makefile::Update, Makefile::Update::VCProj =head1 AUTHOR Vadim Zeitlin =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2015 by Vadim Zeitlin. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut MAKEFILE_UPDATE_MSBUILD $fatpacked{"Makefile/Update/Makefile.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'MAKEFILE_UPDATE_MAKEFILE'; package Makefile::Update::Makefile; # ABSTRACT: Update lists of files in makefile variables. use Exporter qw(import); our @EXPORT = qw(update_makefile); use strict; use warnings; our $VERSION = '0.3'; # VERSION sub update_makefile { my ($in, $out, $vars) = @_; # Variable whose contents is being currently replaced and its original # name in the makefile. my ($var, $makevar); # Hash with files defined for the specified variable as keys and 0 or 1 # depending on whether we have seen them in the input file as values. my %files; # Array of lines in the existing makefile. my @values; # True if the values are in alphabetical order: we use this to add new # entries in alphabetical order too if the existing ones use it, otherwise # we just append them at the end. my $sorted = 1; # Extensions of the files in the files list (they're keys of this hash, # the values are not used), there can be more than one (e.g. ".c" and # ".cpp"). my %src_exts; # Extension of the files in the makefiles: here there can also be more # than one, but in this case we just give up and don't perform any # extensions translation because we don't have enough information to do it # (e.g. which extension should be used for the new files in the makefile?). # Such case is indicated by make_ext being empty (as opposed to its # initial undefined value). my $make_ext; # Helper to get the extension. Note that the "extension" may be a make # variable, e.g. the file could be something like "foo.$(obj)", so don't # restrict it to just word characters. sub _get_ext { $_[0] =~ /(\.\S+)$/ ? $1 : undef } # Indent and the part after the value (typically some amount of spaces and # a backslash) for normal lines and, separately, for the last one, as it # may or not have backslash after it. my ($indent, $tail, $last_tail); # We can't use the usual check for EOF inside while itself because this # wouldn't work for files with no new line after the last line, so check # for the EOF manually. my $eof = 0; # Set to 1 if we made any changes. my $changed = 0; while (1) { my $line = <$in>; if (defined $line) { chomp $line; } else { $line = ''; $eof = 1; } # If we're inside the variable definition, parse the current line as # another file name, if (defined $var) { if ($line =~ /^(?\s*)(?[^ ]+)(?\s*\\?)$/) { if (defined $indent) { warn qq{Inconsistent indent at line $. in the } . qq{definition of the variable "$makevar".\n} if $+{indent} ne $indent; } else { $indent = $+{indent}; } $last_tail = $+{tail}; my $file_orig = $+{file}; $tail = $last_tail if !defined $tail; # Check if we have something with the correct extension and # preserve unchanged all the rest -- we don't want to remove # expansions of other makefile variables from this one, for # example, but such expansions would never be in the files # list as they don't make sense for the other formats. my $file = $file_orig; if (defined (my $file_ext = _get_ext($file))) { if (defined $make_ext) { if ($file_ext ne $make_ext) { # As explained in the comment before make_ext # definition, just don't do anything in this case. $make_ext = ''; } } else { $make_ext = $file_ext; } # We need to try this file with all of the source # extensions we have as it can correspond to any of them. for my $src_ext (keys %src_exts) { if ($file_ext ne $src_ext) { (my $file_try = $file) =~ s/\Q$file_ext\E$/$src_ext/; if (exists $files{$file_try}) { $file = $file_try; last } } } if (!exists $files{$file}) { # This file was removed. $changed = 1; # Don't store this line in @values below. next; } } if (exists $files{$file}) { if ($files{$file}) { warn qq{Duplicate file "$file" in the definition of the } . qq{variable "$makevar" at line $.\n} } else { $files{$file} = 1; } } # Are we still sorted? if (@values && lc $line lt $values[-1]) { $sorted = 0; } push @values, $line; next; } # If the last line had a continuation character, the file list # should only end if there is nothing else on the following line. if ($last_tail =~ /\\$/ && $line =~ /\S/) { warn qq{Expected blank line at line $..\n}; } # End of variable definition, add new lines. # We can only map the extensions if we have a single extension to # map them to (i.e. make_ext is not empty) and we only need to do # it if are using more than one extension in the source files list # or the single extension that we use is different from make_ext. if (defined $make_ext) { if ($make_ext eq '' || (keys %src_exts == 1 && exists $src_exts{$make_ext})) { undef $make_ext } } my $new_files = 0; while (my ($file, $seen) = each(%files)) { next if $seen; # This file was wasn't present in the input, add it. # If this is the first file we add, ensure that the last line # present in the makefile so far has the line continuation # character at the end as this might not have been the case. if (!$new_files) { $new_files = 1; if (@values && $values[-1] !~ /\\$/) { $values[-1] .= $tail; } } # Next give it the right extension. if (defined $make_ext) { $file =~ s/\.\S+$/$make_ext/ } # Finally store it. push @values, "$indent$file$tail"; } if ($new_files) { $changed = 1; # Sort them if necessary using the usual Schwartzian transform. if ($sorted) { @values = map { $_->[0] } sort { $a->[1] cmp $b->[1] } map { [$_, lc $_] } @values; } # Fix up the tail of the last line to be the same as that of # the previous last line. $values[-1] =~ s/\s*\\$/$last_tail/; } undef $var; print $out join("\n", @values), "\n"; } # We're only interested in variable or target declarations, and does # not look like target-specific variable (this would contain an equal # sign after the target). if ($line =~ /^\s*(?\S+)\s*(?::?=|:)(?[^=]*)$/) { $makevar = $+{var}; my $tail = $+{tail}; # And only those of them for which we have values, but this is # where it gets tricky as we try to be smart to accommodate common # use patterns with minimal effort. if (exists $vars->{$makevar}) { $var = $makevar; } else { # Helper: return name if a variable with such name exists or # undef otherwise. my $var_if_exists = sub { exists $vars->{$_[0]} ? $_[0] : undef }; if ($makevar =~ /^objects$/i || $makevar =~ /^obj$/i) { # Special case: map it to "sources" as we work with the # source, not object, files. $var = $var_if_exists->('sources'); } elsif ($makevar =~ /^(\w+)_(objects|obj|sources|src)$/i) { # Here we deal with "foo_sources" typically found in # hand-written makefiles but also "foo_SOURCES" used in # automake ones, but the latter also uses libfoo_a_SOURCES # for static libraries and libfoo_la_SOURCES for the # libtool libraries, be smart about it to allow defining # just "foo" or "foo_sources" variables usable with all # kinds of make/project files. $var = $var_if_exists->($1) || $var_if_exists->("$1_sources"); if (!defined $var && $2 eq 'SOURCES' && $1 =~ /^(\w+)_l?a$/) { $var = $var_if_exists->($1) || $var_if_exists->("$1_sources"); if (!defined $var && $1 =~ /^lib(\w+)$/) { $var = $var_if_exists->($1) || $var_if_exists->("$1_sources"); } } } elsif ($makevar =~ /^(\w+)\$\(\w+\)/) { # This one is meant to catch relatively common makefile # constructions like "target$(exe_ext)". $var = $var_if_exists->($1); } } if (defined $var) { if ($tail !~ /\s*\\$/) { warn qq{Unsupported format for variable "$makevar" at line $..\n}; undef $var; } else { %files = map { $_ => 0 } @{$vars->{$var}}; @values = (); # Find all the extensions used by files in this variable. for my $file (@{$vars->{$var}}) { if (defined (my $src_ext = _get_ext($file))) { $src_exts{$src_ext} = 1; } } # Not known yet. undef $make_ext; undef $indent; $tail = $tail; undef $last_tail; # Not unsorted so far. $sorted = 1; } } } print $out "$line"; # Don't add an extra new line at the EOF if it hadn't been there. last if $eof; print $out "\n"; } $changed } __END__ =pod =encoding UTF-8 =head1 NAME Makefile::Update::Makefile - Update lists of files in makefile variables. =head1 VERSION version 0.3 =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 FUNCTIONS =head2 update_makefile Update variable definitions in a makefile format with the data from the hash ref containing all the file lists. Only most straightforward cases of variable or target definitions are recognized here, i.e. just "var := value", "var = value" or "target: value". In particular we don't support any GNU make extensions such as "export" or "override" without speaking of anything more complex. On top of it, currently the value should contain a single file per line with none at all on the first line (but this restriction could be relaxed later if needed), i.e. the only supported case is var = \ foo \ bar \ baz and it must be followed by an empty line, too. Notice that if any of the "files" in the variable value looks like a makefile variable, i.e. has "$(foo)" form, it is ignored by this function, i.e. not removed even if it doesn't appear in the list of files (which will never be the case normally). Takes the (open) file handles of the files to read and to write and the file lists hash ref as arguments. Returns 1 if any changes were made. =head1 SEE ALSO Makefile::Update =head1 AUTHOR Vadim Zeitlin =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2015 by Vadim Zeitlin. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut MAKEFILE_UPDATE_MAKEFILE $fatpacked{"Makefile/Update/VCProj.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'MAKEFILE_UPDATE_VCPROJ'; package Makefile::Update::VCProj; # ABSTRACT: Update list of sources and headers in Visual C++ projects. use Exporter qw(import); our @EXPORT = qw(update_vcproj); use strict; use warnings; our $VERSION = '0.3'; # VERSION sub update_vcproj { my ($in, $out, $sources, $headers, $filter_cb) = @_; # Use standard/default classifier for the files if none is explicitly # specified. if (!defined $filter_cb) { $filter_cb = sub { my ($file) = @_; return 'Source Files' if $file =~ q{\.c(c|pp|xx|\+\+)?$}; return 'Header Files' if $file =~ q{\.h(h|pp|xx|\+\+)?$}; warn qq{No filter defined for the file "$file".\n}; undef } } # Hash mapping the filter to all the files using it (whether sources or # headers). my %files_by_filter; foreach my $file (@$sources, @$headers) { my $filter = $filter_cb->($file); if (defined $filter) { push @{$files_by_filter{$filter}}, $file } } # Name of the current filter, if any. my $filter; # Hash containing 0 or 1 for each file using the current filter. my %seen; # Indicates whether the closing angle bracket of "" tags is on its # own line (which is how MSVS 2005 and 2008 format their files) or on the # same line as "RelativePath" attribute (which is how MSVS 2003 does it). my $angle_bracket_on_same_line = 0; # Set to 1 if we made any changes. my $changed = 0; while (defined (my $line_with_eol = <$in>)) { (my $line = $line_with_eol) =~ s/\r?\n$//; if ($line =~ /^\s* tag at line $. while parsing filter } . qq{"$filter" is not supported.\n}; next; } print $out $line_with_eol; $line_with_eol = <$in>; if (defined $line_with_eol && $line_with_eol =~ /^\s*Name="(.*)"\r?\n$/) { $filter = $1; if (!exists $files_by_filter{$filter}) { # If we don't have any files for this filter, don't remove # all the files from it, just skip it entirely instead. undef $filter; } else { %seen = map { $_ => 0 } @{$files_by_filter{$filter}}; } } else { warn qq{Unrecognized format for tag at line $..\n}; } } elsif (defined $filter) { if ($line =~ /^\s*; if (defined $line_with_eol && $line_with_eol =~ /^\s*RelativePath="(.*)"(>?)\r?\n$/) { $angle_bracket_on_same_line = $2 eq '>'; # Normalize path separators to Unix and remove the leading # dot which MSVC likes to use for some reason. (my $file = $1) =~ s@\\@/@g; $file =~ s@^\./@@; # Special hack for resource files that sometimes occur in # the "Source Files" section of MSVC projects too: don't # remove them, even if they don't appear in the master # files list, because they are never going to appear in it. if ($file !~ /\.rc$/) { if (!exists $seen{$file}) { # This file is not in the master file list any # more, delete it from the project file as well by # not copying the lines corresponding to it to the # output. $changed = 1; # Skip the next line unless we had already seen # the angle bracket. if (!$angle_bracket_on_same_line) { if (<$in> !~ /^\s*>\r?\n$/) { warn qq{Expected closing '>' on the line $.\n} } } # And skip everything up to and including the # closing tag in any case. while (<$in>) { last if qr{^\s*\r?\n$} } next; } # This file is still in the files list, mark it as seen. if ($seen{$file}) { warn qq{Duplicate file "$file" in the project at line $.\n}; } else { $seen{$file} = 1; } } } else { warn qq{Unrecognized format for tag inside filter } . qq{"$filter" at line $..\n}; } # Don't lose the original line, it won't be printed at the # end of the loop any more. print $out $line_file_start; } elsif ($line =~ qr{^\s*$}) { my $angle_bracket = $angle_bracket_on_same_line ? '>' : "\n\t\t\t\t>"; # Add new files, if any. # # TODO Insert them in alphabetical order. while (my ($file, $seen) = each(%seen)) { if (!$seen) { # Convert path separator to the one used by MSVC. $file =~ s@/@\\@g; # And use path even for the files in this directory. $file = ".\\$file" if $file !~ /\\/; print $out < END ; $changed = 1; } } undef $filter; } } print $out $line_with_eol; } $changed } __END__ =pod =encoding UTF-8 =head1 NAME Makefile::Update::VCProj - Update list of sources and headers in Visual C++ projects. =head1 VERSION version 0.3 =head1 SYNOPSIS The function L can be used to update the list of headers and sources in the given Visual C++ project file C: use Makefile::Update::VCProj; upmake_msbuild_project('project.vcproj', \@sources, \@headers); =head1 FUNCTIONS =head2 update_vcproj Update sources and headers in a VC++ project. Parameters: input and output file handles, array references to the sources and the headers to be used in this project and a callback used to determine the filter for the new files. Returns 1 if any changes were made. =head1 SEE ALSO Makefile::Update, Makefile::Update::MSBuild =head1 AUTHOR Vadim Zeitlin =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2015 by Vadim Zeitlin. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut MAKEFILE_UPDATE_VCPROJ s/^ //mg for values %fatpacked; my $class = 'FatPacked::'.(0+\%fatpacked); no strict 'refs'; *{"${class}::files"} = sub { keys %{$_[0]} }; if ($] < 5.008) { *{"${class}::INC"} = sub { if (my $fat = $_[0]{$_[1]}) { return sub { return 0 unless length $fat; $fat =~ s/^([^\n]*\n?)//; $_ = $1; return 1; }; } return; }; } else { *{"${class}::INC"} = sub { if (my $fat = $_[0]{$_[1]}) { open my $fh, '<', \$fat or die "FatPacker error loading $_[1] (could be a perl installation issue?)"; return $fh; } return; }; } unshift @INC, bless \%fatpacked, $class; } # END OF FATPACK CODE use strict; use warnings; use autodie; use Getopt::Long; use FindBin qw($Bin); use Text::Upmake; use Text::Upmake::Bakefile0; use Text::Upmake::MSBuild; my $verbose = 0; my $quiet = 0; my ($only_bkl, $only_msbuild, $only_project, $only_version); GetOptions( 'verbose|v' => \$verbose, 'quiet|q' => \$quiet, 'only-bkl' => \$only_bkl, 'only-project=s' => sub { $only_msbuild = 1; $only_project = $_[1] }, 'only-version=i' => sub { $only_msbuild = 1; $only_version = $_[1] }, ) or die <] Update the files used by bakefile and MSBuild projects from the master list of files in build/files. If --no-xxx option is specified, the corresponding outputs are not updated. By default everything is. EOF ; if ($only_bkl && $only_msbuild) { die qq{Options --only-bkl and --only-project or --only-version can't be used together.\n} } sub log_upmake { my ($fname, @args) = @_; if (upmake($fname, @args)) { print qq{File "$fname" successfully updated.\n} unless $quiet; return 1; } else { print qq{No changes in the file "$fname".\n} if $verbose; return 0; } } open my $files, '<', "$Bin/files"; my $vars = read_files_list($files); if (!$only_msbuild) { if (log_upmake("$Bin/bakefiles/files.bkl", \&update_bakefile_0, $vars)) { print qq{Don't forget to run "bakefile_gen -b wx.bkl".} if $verbose; } } if (!$only_bkl) { # Path to the project root directory from the directory containing the # projects. my $top_srcdir = '../../'; # The base names of all our MSBuild projects with the list of variables # containing the files that should appear in them. my %projects_vars = ( adv => [qw(ADVANCED_CMN ADVANCED_MSW ADVANCED_MSW_DESKTOP ADVANCED_MSW_NATIVE)], aui => [qw(AUI_CMN)], base => [qw(BASE_CMN BASE_AND_GUI_CMN BASE_WIN32 BASE_AND_GUI_WIN32)], core => [qw(BASE_AND_GUI_CMN BASE_AND_GUI_WIN32 MSW_LOWLEVEL MSW_DESKTOP_LOWLEVEL MSW MSW_DESKTOP GUI_CMN)], gl => [qw(OPENGL_CMN OPENGL_MSW)], html => [qw(HTML_CMN HTML_MSW)], media => [qw(MEDIA_CMN MEDIA_MSW MEDIA_MSW_DESKTOP)], net => [qw(NET_CMN NET_WIN32)], propgrid => [qw(PROPGRID)], qa => [qw(QA)], ribbon => [qw(RIBBON)], stc => [qw(STC)], webview => [qw(WEBVIEW_CMN WEBVIEW_MSW)], xml => [qw(XML)], xrc => [qw(XRC)], ); # Return the "filter" to use for the given file. sub filter_cb { my ($file) = @_; my %filters = ( 'src/common/.*' => 'Common Sources', 'src/gtk/.*' => 'GTK+ Sources', 'src/msw/.*' => 'MSW Sources', 'src/generic/.*' => 'Generic Sources', 'src/univ/.*' => 'wxUniv Sources', 'src/html/.*' => 'wxHTML Sources', 'include/.*/setup.h' => 'Setup Headers', 'include/wx/gtk/.*' => 'GTK+ Headers', 'include/wx/msw/.*' => 'MSW Headers', 'include/wx/generic/.*' => 'Generic Headers', 'include/wx/univ/.*' => 'wxUniv Headers', 'include/wx/html/.*' => 'wxHTML Headers', ); foreach (keys %filters) { return $filters{$_} if $file =~ qr{^${top_srcdir}$_$}; } # Two fall backs which can't be used in the hash as they must be # checked after the other patterns. return 'Source Files' if $file =~ q{src/.*}; return 'Common Headers' if $file =~ q{include/wx/.*}; warn qq{No filter defined for the file "$file".\n}; undef } foreach my $proj (sort keys %projects_vars) { next if defined $only_project && $proj ne $only_project; my (@sources, @headers); # All our projects use the special dummy file for PCH creation, but it's # not included in the file lists. push @sources, "${top_srcdir}src/common/dummy.cpp"; foreach my $var (@{$projects_vars{$proj}}) { # The paths in the files lists are relative to the project root, # so add relative path to it from the projects directory. push @sources, "${top_srcdir}$_" for @{$vars->{"${var}_SRC"}}; # It is possible that we don't have any headers of some kind at all. if (exists $vars->{"${var}_HDR"}) { # Our files lists don't use the full path for the headers, the # common "include/" prefix is omitted, add it back here. push @headers, "${top_srcdir}include/$_" for @{$vars->{"${var}_HDR"}}; } } my @args = (\@sources, \@headers, \&filter_cb); log_upmake("$Bin/msw/wx_${proj}.vcxproj", \&update_msbuild, @args); log_upmake("$Bin/msw/wx_${proj}.vcxproj.filters", \&update_msbuild_filters, @args); } }