#!/usr/bin/perl -w # Add source files and headers to Xcode and Visual Studio projects. # THIS IS NOT ROBUST, THIS IS JUST RYAN AVOIDING RUNNING BETWEEN # THREE COMPUTERS AND A BUNCH OF DEVELOPMENT ENVIRONMENTS TO ADD # A STUPID FILE TO THE BUILD. use warnings; use strict; use File::Basename; my %xcode_references = (); sub generate_xcode_id { my @chars = ('0'..'9', 'A'..'F'); my $str; do { my $len = 16; $str = '0000'; # start and end with '0000' so we know we added it. while ($len--) { $str .= $chars[rand @chars] }; $str .= '0000'; # start and end with '0000' so we know we added it. } while (defined($xcode_references{$str})); $xcode_references{$str} = 1; # so future calls can't generate this one. return $str; } sub process_xcode { my $addpath = shift; my $pbxprojfname = shift; my $lineno; %xcode_references = (); my $addfname = basename($addpath); my $addext = ''; if ($addfname =~ /\.(.*?)\Z/) { $addext = $1; } my $is_public_header = ($addpath =~ /\Ainclude\/SDL3\//) ? 1 : 0; my $filerefpath = $is_public_header ? "SDL3/$addfname" : $addfname; my $srcs_or_headers = ''; my $addfiletype = ''; if ($addext eq 'c') { $srcs_or_headers = 'Sources'; $addfiletype = 'sourcecode.c.c'; } elsif ($addext eq 'm') { $srcs_or_headers = 'Sources'; $addfiletype = 'sourcecode.c.objc'; } elsif ($addext eq 'h') { $srcs_or_headers = 'Headers'; $addfiletype = 'sourcecode.c.h'; } else { die("Unexpected file extension '$addext'\n"); } my $fh; open $fh, '<', $pbxprojfname or die("Failed to open '$pbxprojfname': $!\n"); chomp(my @pbxproj = <$fh>); close($fh); # build a table of all ids, in case we duplicate one by some miracle. $lineno = 0; foreach (@pbxproj) { $lineno++; # like "F3676F582A7885080091160D /* SDL3.dmg */ = {" if (/\A\t\t([A-F0-9]{24}) \/\* (.*?) \*\/ \= \{\Z/) { $xcode_references{$1} = $2; } } # build out of a tree of PBXGroup items. my %pbxgroups = (); my $thispbxgroup; my $pbxgroup_children; my $pbxgroup_state = 0; my $pubheaders_group_hash = ''; my $libsrc_group_hash = ''; $lineno = 0; foreach (@pbxproj) { $lineno++; if ($pbxgroup_state == 0) { $pbxgroup_state++ if /\A\/\* Begin PBXGroup section \*\/\Z/; } elsif ($pbxgroup_state == 1) { # like "F3676F582A7885080091160D /* SDL3.dmg */ = {" if (/\A\t\t([A-F0-9]{24}) \/\* (.*?) \*\/ \= \{\Z/) { my %newhash = (); $pbxgroups{$1} = \%newhash; $thispbxgroup = \%newhash; $pubheaders_group_hash = $1 if $2 eq 'Public Headers'; $libsrc_group_hash = $1 if $2 eq 'Library Source'; $pbxgroup_state++; } elsif (/\A\/\* End PBXGroup section \*\/\Z/) { last; } else { die("Expected pbxgroup obj on '$pbxprojfname' line $lineno\n"); } } elsif ($pbxgroup_state == 2) { if (/\A\t\t\tisa \= PBXGroup;\Z/) { $pbxgroup_state++; } else { die("Expected pbxgroup obj's isa field on '$pbxprojfname' line $lineno\n"); } } elsif ($pbxgroup_state == 3) { if (/\A\t\t\tchildren \= \(\Z/) { my %newhash = (); $$thispbxgroup{'children'} = \%newhash; $pbxgroup_children = \%newhash; $pbxgroup_state++; } else { die("Expected pbxgroup obj's children field on '$pbxprojfname' line $lineno\n"); } } elsif ($pbxgroup_state == 4) { if (/\A\t\t\t\t([A-F0-9]{24}) \/\* (.*?) \*\/,\Z/) { $$pbxgroup_children{$1} = $2; } elsif (/\A\t\t\t\);\Z/) { $pbxgroup_state++; } else { die("Expected pbxgroup obj's children element on '$pbxprojfname' line $lineno\n"); } } elsif ($pbxgroup_state == 5) { if (/\A\t\t\t(.*?) \= (.*?);\Z/) { $$thispbxgroup{$1} = $2; } elsif (/\A\t\t\};\Z/) { $pbxgroup_state = 1; } else { die("Expected pbxgroup obj field on '$pbxprojfname' line $lineno\n"); } } else { die("bug in this script."); } } die("Didn't see PBXGroup section in '$pbxprojfname'. Bug?\n") if $pbxgroup_state == 0; die("Didn't see Public Headers PBXGroup in '$pbxprojfname'. Bug?\n") if $pubheaders_group_hash eq ''; die("Didn't see Library Source PBXGroup in '$pbxprojfname'. Bug?\n") if $libsrc_group_hash eq ''; # Some debug log dumping... if (0) { foreach (keys %pbxgroups) { my $k = $_; my $g = $pbxgroups{$k}; print("$_:\n"); foreach (keys %$g) { print(" $_:\n"); if ($_ eq 'children') { my $kids = $$g{$_}; foreach (keys %$kids) { print(" $_ -> " . $$kids{$_} . "\n"); } } else { print(' ' . $$g{$_} . "\n"); } } print("\n"); } } # Get some unique IDs for our new thing. my $fileref = generate_xcode_id(); my $buildfileref = generate_xcode_id(); # Figure out what group to insert this into (or what groups to make) my $add_to_group_fileref = $fileref; my $add_to_group_addfname = $addfname; my $newgrptext = ''; my $grphash = ''; if ($is_public_header) { $grphash = $pubheaders_group_hash; # done! } else { $grphash = $libsrc_group_hash; my @splitpath = split(/\//, dirname($addpath)); if ($splitpath[0] eq 'src') { shift @splitpath; } while (my $elem = shift(@splitpath)) { my $g = $pbxgroups{$grphash}; my $kids = $$g{'children'}; my $found = 0; foreach (keys %$kids) { my $hash = $_; my $fname = $$kids{$hash}; if (uc($fname) eq uc($elem)) { $grphash = $hash; $found = 1; last; } } unshift(@splitpath, $elem), last if (not $found); } if (@splitpath) { # still elements? We need to build groups. my $newgroupref = generate_xcode_id(); $add_to_group_fileref = $newgroupref; $add_to_group_addfname = $splitpath[0]; while (my $elem = shift(@splitpath)) { my $lastelem = @splitpath ? 0 : 1; my $childhash = $lastelem ? $fileref : generate_xcode_id(); my $childpath = $lastelem ? $addfname : $splitpath[0]; $newgrptext .= "\t\t$newgroupref /* $elem */ = {\n"; $newgrptext .= "\t\t\tisa = PBXGroup;\n"; $newgrptext .= "\t\t\tchildren = (\n"; $newgrptext .= "\t\t\t\t$childhash /* $childpath */,\n"; $newgrptext .= "\t\t\t);\n"; $newgrptext .= "\t\t\tpath = $elem;\n"; $newgrptext .= "\t\t\tsourceTree = \"\";\n"; $newgrptext .= "\t\t};\n"; $newgroupref = $childhash; } } } my $tmpfname = "$pbxprojfname.tmp"; open $fh, '>', $tmpfname or die("Failed to open '$tmpfname': $!\n"); my $add_to_this_group = 0; $pbxgroup_state = 0; $lineno = 0; foreach (@pbxproj) { $lineno++; if ($pbxgroup_state == 0) { # Drop in new references at the end of their sections... if (/\A\/\* End PBXBuildFile section \*\/\Z/) { print $fh "\t\t$buildfileref /* $addfname in $srcs_or_headers */ = {isa = PBXBuildFile; fileRef = $fileref /* $addfname */;"; if ($is_public_header) { print $fh " settings = {ATTRIBUTES = (Public, ); };"; } print $fh " };\n"; } elsif (/\A\/\* End PBXFileReference section \*\/\Z/) { print $fh "\t\t$fileref /* $addfname */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = $addfiletype; name = $addfname; path = $filerefpath; sourceTree = \"\"; };\n"; } elsif (/\A\/\* Begin PBXGroup section \*\/\Z/) { $pbxgroup_state = 1; } elsif (/\A\/\* Begin PBXSourcesBuildPhase section \*\/\Z/) { $pbxgroup_state = 5; } } elsif ($pbxgroup_state == 1) { if (/\A\t\t([A-F0-9]{24}) \/\* (.*?) \*\/ \= \{\Z/) { $pbxgroup_state++; $add_to_this_group = $1 eq $grphash; } elsif (/\A\/\* End PBXGroup section \*\/\Z/) { print $fh $newgrptext; $pbxgroup_state = 0; } } elsif ($pbxgroup_state == 2) { if (/\A\t\t\tchildren \= \(\Z/) { $pbxgroup_state++; } } elsif ($pbxgroup_state == 3) { if (/\A\t\t\t\);\Z/) { if ($add_to_this_group) { print $fh "\t\t\t\t$add_to_group_fileref /* $add_to_group_addfname */,\n"; } $pbxgroup_state++; } } elsif ($pbxgroup_state == 4) { if (/\A\t\t\};\Z/) { $add_to_this_group = 0; } $pbxgroup_state = 1; } elsif ($pbxgroup_state == 5) { if (/\A\t\t\t\);\Z/) { if ($srcs_or_headers eq 'Sources') { print $fh "\t\t\t\t$buildfileref /* $addfname in $srcs_or_headers */,\n"; } $pbxgroup_state = 0; } } print $fh "$_\n"; } close($fh); rename($tmpfname, $pbxprojfname); } my %visualc_references = (); sub generate_visualc_id { # these are just standard Windows GUIDs. my @chars = ('0'..'9', 'a'..'f'); my $str; do { my $len = 24; $str = '0000'; # start and end with '0000' so we know we added it. while ($len--) { $str .= $chars[rand @chars] }; $str .= '0000'; # start and end with '0000' so we know we added it. $str =~ s/\A(........)(....)(....)(............)\Z/$1-$2-$3-$4/; # add dashes in the appropriate places. } while (defined($visualc_references{$str})); $visualc_references{$str} = 1; # so future calls can't generate this one. return $str; } sub process_visualstudio { my $addpath = shift; my $vcxprojfname = shift; my $lineno; %visualc_references = (); my $is_public_header = ($addpath =~ /\Ainclude\/SDL3\//) ? 1 : 0; my $addfname = basename($addpath); my $addext = ''; if ($addfname =~ /\.(.*?)\Z/) { $addext = $1; } my $isheader = 0; if ($addext eq 'c') { $isheader = 0; } elsif ($addext eq 'm') { return; # don't add Objective-C files to Visual Studio projects! } elsif ($addext eq 'h') { $isheader = 1; } else { die("Unexpected file extension '$addext'\n"); } my $fh; open $fh, '<', $vcxprojfname or die("Failed to open '$vcxprojfname': $!\n"); chomp(my @vcxproj = <$fh>); close($fh); my $vcxgroup_state; my $rawaddvcxpath = "$addpath"; $rawaddvcxpath =~ s/\//\\/g; # Figure out relative path from vcxproj file... my $addvcxpath = ''; my @subdirs = split(/\//, $vcxprojfname); pop @subdirs; foreach (@subdirs) { $addvcxpath .= "..\\"; } $addvcxpath .= $rawaddvcxpath; my $prevname = undef; my $tmpfname; $tmpfname = "$vcxprojfname.tmp"; open $fh, '>', $tmpfname or die("Failed to open '$tmpfname': $!\n"); my $added = 0; $added = 0; $vcxgroup_state = 0; $prevname = undef; $lineno = 0; foreach (@vcxproj) { $lineno++; if ($vcxgroup_state == 0) { if (/\A \\Z/) { $vcxgroup_state = 1; $prevname = undef; } } elsif ($vcxgroup_state == 1) { if (/\A \\Z/) { $vcxgroup_state = 0; $prevname = undef; } } # Don't do elsif, we need to check this line again. if ($vcxgroup_state == 2) { if (/\A \Z/) { my $nextname = $1; if ((not $added) && (((not defined $prevname) || (uc($prevname) lt uc($addvcxpath))) && (uc($nextname) gt uc($addvcxpath)))) { print $fh " \n"; $vcxgroup_state = 0; $added = 1; } $prevname = $nextname; } elsif (/\A \<\/ItemGroup\>\Z/) { if ((not $added) && ((not defined $prevname) || (uc($prevname) lt uc($addvcxpath)))) { print $fh " \n"; $vcxgroup_state = 0; $added = 1; } } } elsif ($vcxgroup_state == 3) { if (/\A \Z/) { my $nextname = $1; if ((not $added) && (((not defined $prevname) || (uc($prevname) lt uc($addvcxpath))) && (uc($nextname) gt uc($addvcxpath)))) { print $fh " \n"; $vcxgroup_state = 0; $added = 1; } $prevname = $nextname; } elsif (/\A \<\/ItemGroup\>\Z/) { if ((not $added) && ((not defined $prevname) || (uc($prevname) lt uc($addvcxpath)))) { print $fh " \n"; $vcxgroup_state = 0; $added = 1; } } } print $fh "$_\n"; } close($fh); rename($tmpfname, $vcxprojfname); my $vcxfiltersfname = "$vcxprojfname.filters"; open $fh, '<', $vcxfiltersfname or die("Failed to open '$vcxfiltersfname': $!\n"); chomp(my @vcxfilters = <$fh>); close($fh); my $newgrptext = ''; my $filter = ''; if ($is_public_header) { $filter = 'API Headers'; } else { $filter = lc(dirname($addpath)); $filter =~ s/\Asrc\///; # there's no filter for the base "src/" dir, where SDL.c and friends live. $filter =~ s/\//\\/g; $filter = '' if $filter eq 'src'; if ($filter ne '') { # see if the filter already exists, otherwise add it. my %existing_filters = (); my $current_filter = ''; my $found = 0; foreach (@vcxfilters) { # These lines happen to be unique, so we don't have to parse down to find this section. if (/\A \\Z/) { $current_filter = lc($1); if ($current_filter eq $filter) { $found = 1; } } elsif (/\A \\{(.*?)\}\<\/UniqueIdentifier\>\Z/) { $visualc_references{$1} = $current_filter; # gather up existing GUIDs to avoid duplicates. $existing_filters{$current_filter} = $1; } } if (not $found) { # didn't find it? We need to build filters. my $subpath = ''; my @splitpath = split(/\\/, $filter); while (my $elem = shift(@splitpath)) { $subpath .= "\\" if ($subpath ne ''); $subpath .= $elem; if (not $existing_filters{$subpath}) { my $newgroupref = generate_visualc_id(); $newgrptext .= " \n"; $newgrptext .= " {$newgroupref}\n"; $newgrptext .= " \n" } } } } } $tmpfname = "$vcxfiltersfname.tmp"; open $fh, '>', $tmpfname or die("Failed to open '$tmpfname': $!\n"); $added = 0; $vcxgroup_state = 0; $prevname = undef; $lineno = 0; foreach (@vcxfilters) { $lineno++; # We cheat here, because these lines are unique, we don't have to fully parse this file. if ($vcxgroup_state == 0) { if (/\A \\Z/) { if ($newgrptext ne '') { $vcxgroup_state = 1; $prevname = undef; } } elsif (/\A \\Z/) { print $fh $newgrptext; $newgrptext = ''; $vcxgroup_state = 0; } } elsif ($vcxgroup_state == 2) { if (/\A \n"; print $fh " $filter\n"; print $fh " \n"; } else { print $fh " />\n"; } $added = 1; } $prevname = $nextname; } elsif (/\A \<\/ItemGroup\>\Z/) { if ((not $added) && ((not defined $prevname) || (uc($prevname) lt uc($addvcxpath)))) { print $fh " \n"; print $fh " $filter\n"; print $fh " \n"; } else { print $fh " />\n"; } $added = 1; } $vcxgroup_state = 0; } } elsif ($vcxgroup_state == 3) { if (/\A \n"; print $fh " $filter\n"; print $fh " \n"; } else { print $fh " />\n"; } $added = 1; } $prevname = $nextname; } elsif (/\A \<\/ItemGroup\>\Z/) { if ((not $added) && ((not defined $prevname) || (uc($prevname) lt uc($addvcxpath)))) { print $fh " \n"; print $fh " $filter\n"; print $fh " \n"; } else { print $fh " />\n"; } $added = 1; } $vcxgroup_state = 0; } } print $fh "$_\n"; } close($fh); rename($tmpfname, $vcxfiltersfname); } # Mainline! chdir(dirname($0)); # assumed to be in build-scripts chdir('..'); # head to root of source tree. foreach (@ARGV) { s/\A\.\///; # Turn "./path/to/file.txt" into "path/to/file.txt" my $arg = $_; process_xcode($arg, 'Xcode/SDL/SDL.xcodeproj/project.pbxproj'); process_visualstudio($arg, 'VisualC/SDL/SDL.vcxproj'); process_visualstudio($arg, 'VisualC-GDK/SDL/SDL.vcxproj'); process_visualstudio($arg, 'VisualC-WinRT/SDL-UWP.vcxproj'); } print("Done. Please run `git diff` and make sure this looks okay!\n"); exit(0);