diff options
Diffstat (limited to 'Tools/Scripts')
-rw-r--r-- | Tools/Scripts/VCSUtils.pm | 2433 | ||||
-rwxr-xr-x | Tools/Scripts/run-gtk-tests | 494 | ||||
-rwxr-xr-x | Tools/Scripts/webkit-build-directory | 84 | ||||
-rwxr-xr-x | Tools/Scripts/webkitdirs.pm | 2613 |
4 files changed, 5624 insertions, 0 deletions
diff --git a/Tools/Scripts/VCSUtils.pm b/Tools/Scripts/VCSUtils.pm new file mode 100644 index 000000000..c5c44c530 --- /dev/null +++ b/Tools/Scripts/VCSUtils.pm @@ -0,0 +1,2433 @@ +# Copyright (C) 2007-2013, 2015 Apple Inc. All rights reserved. +# Copyright (C) 2009, 2010 Chris Jerdonek (chris.jerdonek@gmail.com) +# Copyright (C) 2010, 2011 Research In Motion Limited. All rights reserved. +# Copyright (C) 2012 Daniel Bates (dbates@intudata.com) +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of Apple Inc. ("Apple") nor the names of +# its contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# Module to share code to work with various version control systems. +package VCSUtils; + +use strict; +use warnings; + +use Cwd qw(); # "qw()" prevents warnings about redefining getcwd() with "use POSIX;" +use English; # for $POSTMATCH, etc. +use File::Basename; +use File::Spec; +use POSIX; +use Term::ANSIColor qw(colored); + +BEGIN { + use Exporter (); + our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS); + $VERSION = 1.00; + @ISA = qw(Exporter); + @EXPORT = qw( + &applyGitBinaryPatchDelta + &callSilently + &canonicalizePath + &changeLogEmailAddress + &changeLogName + &chdirReturningRelativePath + &decodeGitBinaryChunk + &decodeGitBinaryPatch + &determineSVNRoot + &determineVCSRoot + &escapeSubversionPath + &exitStatus + &fixChangeLogPatch + &fixSVNPatchForAdditionWithHistory + &gitBranch + &gitDirectory + &gitTreeDirectory + &gitdiff2svndiff + &isGit + &isGitSVN + &isGitBranchBuild + &isGitDirectory + &isGitSVNDirectory + &isSVN + &isSVNDirectory + &isSVNVersion16OrNewer + &makeFilePathRelative + &mergeChangeLogs + &normalizePath + &parseChunkRange + &parseDiffStartLine + &parseFirstEOL + &parsePatch + &pathRelativeToSVNRepositoryRootForPath + &possiblyColored + &prepareParsedPatch + &removeEOL + &runCommand + &runPatchCommand + &scmMoveOrRenameFile + &scmToggleExecutableBit + &setChangeLogDateAndReviewer + &svnIdentifierForPath + &svnInfoForPath + &svnRepositoryRootForPath + &svnRevisionForDirectory + &svnStatus + &svnURLForPath + &toWindowsLineEndings + &gitCommitForSVNRevision + &listOfChangedFilesBetweenRevisions + &unixPath + ); + %EXPORT_TAGS = ( ); + @EXPORT_OK = (); +} + +our @EXPORT_OK; + +my $gitBranch; +my $gitRoot; +my $isGit; +my $isGitSVN; +my $isGitBranchBuild; +my $isSVN; +my $svnVersion; + +# Project time zone for Cupertino, CA, US +my $changeLogTimeZone = "PST8PDT"; + +my $unifiedDiffStartRegEx = qr#^--- ([abc]\/)?([^\r\n]+)#; +my $gitDiffStartRegEx = qr#^diff --git [^\r\n]+#; +my $gitDiffStartWithPrefixRegEx = qr#^diff --git \w/(.+) \w/([^\r\n]+)#; # We suppose that --src-prefix and --dst-prefix don't contain a non-word character (\W) and end with '/'. +my $gitDiffStartWithoutPrefixNoSpaceRegEx = qr#^diff --git (\S+) (\S+)$#; +my $svnDiffStartRegEx = qr#^Index: ([^\r\n]+)#; +my $gitDiffStartWithoutPrefixSourceDirectoryPrefixRegExp = qr#^diff --git ([^/]+/)#; +my $svnPropertiesStartRegEx = qr#^Property changes on: ([^\r\n]+)#; # $1 is normally the same as the index path. +my $svnPropertyStartRegEx = qr#^(Modified|Name|Added|Deleted): ([^\r\n]+)#; # $2 is the name of the property. +my $svnPropertyValueStartRegEx = qr#^\s*(\+|-|Merged|Reverse-merged)\s*([^\r\n]+)#; # $2 is the start of the property's value (which may span multiple lines). +my $svnPropertyValueNoNewlineRegEx = qr#\ No newline at end of property#; + +# This method is for portability. Return the system-appropriate exit +# status of a child process. +# +# Args: pass the child error status returned by the last pipe close, +# for example "$?". +sub exitStatus($) +{ + my ($returnvalue) = @_; + if (isWindows()) { + return $returnvalue >> 8; + } + if (!WIFEXITED($returnvalue)) { + return 254; + } + return WEXITSTATUS($returnvalue); +} + +# Call a function while suppressing STDERR, and return the return values +# as an array. +sub callSilently($@) { + my ($func, @args) = @_; + + # The following pattern was taken from here: + # http://www.sdsc.edu/~moreland/courses/IntroPerl/docs/manual/pod/perlfunc/open.html + # + # Also see this Perl documentation (search for "open OLDERR"): + # http://perldoc.perl.org/functions/open.html + open(OLDERR, ">&STDERR"); + close(STDERR); + my @returnValue = &$func(@args); + open(STDERR, ">&OLDERR"); + close(OLDERR); + + return @returnValue; +} + +sub toWindowsLineEndings +{ + my ($text) = @_; + $text =~ s/\n/\r\n/g; + return $text; +} + +# Note, this method will not error if the file corresponding to the $source path does not exist. +sub scmMoveOrRenameFile +{ + my ($source, $destination) = @_; + return if ! -e $source; + if (isSVN()) { + my $escapedDestination = escapeSubversionPath($destination); + my $escapedSource = escapeSubversionPath($source); + system("svn", "move", $escapedSource, $escapedDestination); + } elsif (isGit()) { + system("git", "mv", $source, $destination); + } +} + +# Note, this method will not error if the file corresponding to the path does not exist. +sub scmToggleExecutableBit +{ + my ($path, $executableBitDelta) = @_; + return if ! -e $path; + if ($executableBitDelta == 1) { + scmAddExecutableBit($path); + } elsif ($executableBitDelta == -1) { + scmRemoveExecutableBit($path); + } +} + +sub scmAddExecutableBit($) +{ + my ($path) = @_; + + if (isSVN()) { + my $escapedPath = escapeSubversionPath($path); + system("svn", "propset", "svn:executable", "on", $escapedPath) == 0 or die "Failed to run 'svn propset svn:executable on $escapedPath'."; + } elsif (isGit()) { + chmod(0755, $path); + } +} + +sub scmRemoveExecutableBit($) +{ + my ($path) = @_; + + if (isSVN()) { + my $escapedPath = escapeSubversionPath($path); + system("svn", "propdel", "svn:executable", $escapedPath) == 0 or die "Failed to run 'svn propdel svn:executable $escapedPath'."; + } elsif (isGit()) { + chmod(0664, $path); + } +} + +sub isGitDirectory($) +{ + my ($dir) = @_; + return system("cd $dir && git rev-parse > " . File::Spec->devnull() . " 2>&1") == 0; +} + +sub isGit() +{ + return $isGit if defined $isGit; + + $isGit = isGitDirectory("."); + return $isGit; +} + +sub isGitSVNDirectory($) +{ + my ($directory) = @_; + + my $savedWorkingDirectory = Cwd::getcwd(); + chdir($directory); + + # There doesn't seem to be an officially documented way to determine + # if you're in a git-svn checkout. The best suggestions seen so far + # all use something like the following: + my $output = `git config --get svn-remote.svn.fetch 2>& 1`; + $isGitSVN = exitStatus($?) == 0 && $output ne ""; + chdir($savedWorkingDirectory); + return $isGitSVN; +} + +sub isGitSVN() +{ + return $isGitSVN if defined $isGitSVN; + + $isGitSVN = isGitSVNDirectory("."); + return $isGitSVN; +} + +sub gitDirectory() +{ + chomp(my $result = `git rev-parse --git-dir`); + return $result; +} + +sub gitTreeDirectory() +{ + chomp(my $result = `git rev-parse --show-toplevel`); + return $result; +} + +sub gitBisectStartBranch() +{ + my $bisectStartFile = File::Spec->catfile(gitDirectory(), "BISECT_START"); + if (!-f $bisectStartFile) { + return ""; + } + open(BISECT_START, $bisectStartFile) or die "Failed to open $bisectStartFile: $!"; + chomp(my $result = <BISECT_START>); + close(BISECT_START); + return $result; +} + +sub gitBranch() +{ + unless (defined $gitBranch) { + chomp($gitBranch = `git symbolic-ref -q HEAD`); + my $hasDetachedHead = exitStatus($?); + if ($hasDetachedHead) { + # We may be in a git bisect session. + $gitBranch = gitBisectStartBranch(); + } + $gitBranch =~ s#^refs/heads/##; + $gitBranch = "" if $gitBranch eq "master"; + } + + return $gitBranch; +} + +sub isGitBranchBuild() +{ + my $branch = gitBranch(); + chomp(my $override = `git config --bool branch.$branch.webKitBranchBuild`); + return 1 if $override eq "true"; + return 0 if $override eq "false"; + + unless (defined $isGitBranchBuild) { + chomp(my $gitBranchBuild = `git config --bool core.webKitBranchBuild`); + $isGitBranchBuild = $gitBranchBuild eq "true"; + } + + return $isGitBranchBuild; +} + +sub isSVNDirectory($) +{ + my ($dir) = @_; + return system("cd $dir && svn info > " . File::Spec->devnull() . " 2>&1") == 0; +} + +sub isSVN() +{ + return $isSVN if defined $isSVN; + + $isSVN = isSVNDirectory("."); + return $isSVN; +} + +sub svnVersion() +{ + return $svnVersion if defined $svnVersion; + + if (!isSVN()) { + $svnVersion = 0; + } else { + chomp($svnVersion = `svn --version --quiet`); + } + return $svnVersion; +} + +sub isSVNVersion16OrNewer() +{ + my $version = svnVersion(); + return "v$version" ge v1.6; +} + +sub chdirReturningRelativePath($) +{ + my ($directory) = @_; + my $previousDirectory = Cwd::getcwd(); + chdir $directory; + my $newDirectory = Cwd::getcwd(); + return "." if $newDirectory eq $previousDirectory; + return File::Spec->abs2rel($previousDirectory, $newDirectory); +} + +sub determineSVNRoot() +{ + my $last = ''; + my $path = '.'; + my $parent = '..'; + my $repositoryRoot; + my $repositoryUUID; + while (1) { + my $thisRoot; + my $thisUUID; + my $escapedPath = escapeSubversionPath($path); + # Ignore error messages in case we've run past the root of the checkout. + open INFO, "svn info '$escapedPath' 2> " . File::Spec->devnull() . " |" or die; + while (<INFO>) { + if (/^Repository Root: (.+)/) { + $thisRoot = $1; + } + if (/^Repository UUID: (.+)/) { + $thisUUID = $1; + } + if ($thisRoot && $thisUUID) { + local $/ = undef; + <INFO>; # Consume the rest of the input. + } + } + close INFO; + + # It's possible (e.g. for developers of some ports) to have a WebKit + # checkout in a subdirectory of another checkout. So abort if the + # repository root or the repository UUID suddenly changes. + last if !$thisUUID; + $repositoryUUID = $thisUUID if !$repositoryUUID; + last if $thisUUID ne $repositoryUUID; + + last if !$thisRoot; + $repositoryRoot = $thisRoot if !$repositoryRoot; + last if $thisRoot ne $repositoryRoot; + + $last = $path; + $path = File::Spec->catdir($parent, $path); + } + + return File::Spec->rel2abs($last); +} + +sub determineVCSRoot() +{ + if (isGit()) { + # This is the working tree root. If WebKit is a submodule, + # then the relevant metadata directory is somewhere else. + return gitTreeDirectory(); + } + + if (!isSVN()) { + # Some users have a workflow where svn-create-patch, svn-apply and + # svn-unapply are used outside of multiple svn working directores, + # so warn the user and assume Subversion is being used in this case. + warn "Unable to determine VCS root for '" . Cwd::getcwd() . "'; assuming Subversion"; + $isSVN = 1; + } + + return determineSVNRoot(); +} + +sub isWindows() +{ + return ($^O eq "MSWin32") || 0; +} + +sub svnRevisionForDirectory($) +{ + my ($dir) = @_; + my $revision; + + if (isSVNDirectory($dir)) { + my $escapedDir = escapeSubversionPath($dir); + my $command = "svn info $escapedDir | grep Revision:"; + $command = "LC_ALL=C $command" if !isWindows(); + my $svnInfo = `$command`; + ($revision) = ($svnInfo =~ m/Revision: (\d+).*/g); + } elsif (isGitDirectory($dir)) { + my $command = "git log --grep=\"git-svn-id: \" -n 1 | grep git-svn-id:"; + $command = "LC_ALL=C $command" if !isWindows(); + $command = "cd $dir && $command"; + my $gitLog = `$command`; + ($revision) = ($gitLog =~ m/ +git-svn-id: .+@(\d+) /g); + } + if (!defined($revision)) { + $revision = "unknown"; + warn "Unable to determine current SVN revision in $dir"; + } + return $revision; +} + +sub svnInfoForPath($) +{ + my ($file) = @_; + my $relativePath = File::Spec->abs2rel($file); + + my $svnInfo; + if (isSVNDirectory($file)) { + my $escapedRelativePath = escapeSubversionPath($relativePath); + my $command = "svn info $escapedRelativePath"; + $command = "LC_ALL=C $command" if !isWindows(); + $svnInfo = `$command`; + } elsif (isGitDirectory($file)) { + my $command = "git svn info"; + $command = "LC_ALL=C $command" if !isWindows(); + $svnInfo = `cd $relativePath && $command`; + } + + return $svnInfo; +} + +sub svnURLForPath($) +{ + my ($file) = @_; + my $svnInfo = svnInfoForPath($file); + + $svnInfo =~ /.*^URL: (.*?)$/m; + return $1; +} + +sub svnRepositoryRootForPath($) +{ + my ($file) = @_; + my $svnInfo = svnInfoForPath($file); + + $svnInfo =~ /.*^Repository Root: (.*?)$/m; + return $1; +} + +sub pathRelativeToSVNRepositoryRootForPath($) +{ + my ($file) = @_; + + my $svnURL = svnURLForPath($file); + my $svnRepositoryRoot = svnRepositoryRootForPath($file); + + $svnURL =~ s/$svnRepositoryRoot\///; + return $svnURL; +} + +sub svnIdentifierForPath($) +{ + my ($file) = @_; + my $path = pathRelativeToSVNRepositoryRootForPath($file); + + $path =~ /^(trunk)|tags\/([\w\.\-]*)|branches\/([\w\.\-]*).*$/m; + return $1 || $2 || $3; +} + +sub makeFilePathRelative($) +{ + my ($path) = @_; + return $path unless isGit(); + + unless (defined $gitRoot) { + chomp($gitRoot = `git rev-parse --show-cdup`); + } + return $gitRoot . $path; +} + +sub normalizePath($) +{ + my ($path) = @_; + if (isWindows()) { + $path =~ s/\//\\/g; + } else { + $path =~ s/\\/\//g; + } + return $path; +} + +sub unixPath($) +{ + my ($path) = @_; + $path =~ s/\\/\//g; + return $path; +} + +sub possiblyColored($$) +{ + my ($colors, $string) = @_; + + if (-t STDOUT) { + return colored([$colors], $string); + } else { + return $string; + } +} + +sub adjustPathForRecentRenamings($) +{ + my ($fullPath) = @_; + + $fullPath =~ s|WebCore/webaudio|WebCore/Modules/webaudio|g; + $fullPath =~ s|JavaScriptCore/wtf|WTF/wtf|g; + $fullPath =~ s|test_expectations.txt|TestExpectations|g; + + return $fullPath; +} + +sub canonicalizePath($) +{ + my ($file) = @_; + + # Remove extra slashes and '.' directories in path + $file = File::Spec->canonpath($file); + + # Remove '..' directories in path + my @dirs = (); + foreach my $dir (File::Spec->splitdir($file)) { + if ($dir eq '..' && $#dirs >= 0 && $dirs[$#dirs] ne '..') { + pop(@dirs); + } else { + push(@dirs, $dir); + } + } + return ($#dirs >= 0) ? File::Spec->catdir(@dirs) : "."; +} + +sub removeEOL($) +{ + my ($line) = @_; + return "" unless $line; + + $line =~ s/[\r\n]+$//g; + return $line; +} + +sub parseFirstEOL($) +{ + my ($fileHandle) = @_; + + # Make input record separator the new-line character to simplify regex matching below. + my $savedInputRecordSeparator = $INPUT_RECORD_SEPARATOR; + $INPUT_RECORD_SEPARATOR = "\n"; + my $firstLine = <$fileHandle>; + $INPUT_RECORD_SEPARATOR = $savedInputRecordSeparator; + + return unless defined($firstLine); + + my $eol; + if ($firstLine =~ /\r\n/) { + $eol = "\r\n"; + } elsif ($firstLine =~ /\r/) { + $eol = "\r"; + } elsif ($firstLine =~ /\n/) { + $eol = "\n"; + } + return $eol; +} + +sub firstEOLInFile($) +{ + my ($file) = @_; + my $eol; + if (open(FILE, $file)) { + $eol = parseFirstEOL(*FILE); + close(FILE); + } + return $eol; +} + +# Parses a chunk range line into its components. +# +# A chunk range line has the form: @@ -L_1,N_1 +L_2,N_2 @@, where the pairs (L_1, N_1), +# (L_2, N_2) are ranges that represent the starting line number and line count in the +# original file and new file, respectively. +# +# Note, some versions of GNU diff may omit the comma and trailing line count (e.g. N_1), +# in which case the omitted line count defaults to 1. For example, GNU diff may output +# @@ -1 +1 @@, which is equivalent to @@ -1,1 +1,1 @@. +# +# This subroutine returns undef if given an invalid or malformed chunk range. +# +# Args: +# $line: the line to parse. +# $chunkSentinel: the sentinel that surrounds the chunk range information (defaults to "@@"). +# +# Returns $chunkRangeHashRef +# $chunkRangeHashRef: a hash reference representing the parts of a chunk range, as follows-- +# startingLine: the starting line in the original file. +# lineCount: the line count in the original file. +# newStartingLine: the new starting line in the new file. +# newLineCount: the new line count in the new file. +sub parseChunkRange($;$) +{ + my ($line, $chunkSentinel) = @_; + $chunkSentinel = "@@" if !$chunkSentinel; + my $chunkRangeRegEx = qr#^\Q$chunkSentinel\E -(\d+)(,(\d+))? \+(\d+)(,(\d+))? \Q$chunkSentinel\E#; + if ($line !~ /$chunkRangeRegEx/) { + return; + } + my %chunkRange; + $chunkRange{startingLine} = $1; + $chunkRange{lineCount} = defined($2) ? $3 : 1; + $chunkRange{newStartingLine} = $4; + $chunkRange{newLineCount} = defined($5) ? $6 : 1; + return \%chunkRange; +} + +sub svnStatus($) +{ + my ($fullPath) = @_; + my $escapedFullPath = escapeSubversionPath($fullPath); + my $svnStatus; + open SVN, "svn status --non-interactive --non-recursive '$escapedFullPath' |" or die; + if (-d $fullPath) { + # When running "svn stat" on a directory, we can't assume that only one + # status will be returned (since any files with a status below the + # directory will be returned), and we can't assume that the directory will + # be first (since any files with unknown status will be listed first). + my $normalizedFullPath = File::Spec->catdir(File::Spec->splitdir($fullPath)); + while (<SVN>) { + # Input may use a different EOL sequence than $/, so avoid chomp. + $_ = removeEOL($_); + my $normalizedStatPath = File::Spec->catdir(File::Spec->splitdir(substr($_, 7))); + if ($normalizedFullPath eq $normalizedStatPath) { + $svnStatus = "$_\n"; + last; + } + } + # Read the rest of the svn command output to avoid a broken pipe warning. + local $/ = undef; + <SVN>; + } + else { + # Files will have only one status returned. + $svnStatus = removeEOL(<SVN>) . "\n"; + } + close SVN; + return $svnStatus; +} + +# Return whether the given file mode is executable in the source control +# sense. We make this determination based on whether the executable bit +# is set for "others" rather than the stronger condition that it be set +# for the user, group, and others. This is sufficient for distinguishing +# the default behavior in Git and SVN. +# +# Args: +# $fileMode: A number or string representing a file mode in octal notation. +sub isExecutable($) +{ + my $fileMode = shift; + + return $fileMode % 2; +} + +# Parses an SVN or Git diff header start line. +# +# Args: +# $line: "Index: " line or "diff --git" line +# +# Returns the path of the target file or undef if the $line is unrecognized. +sub parseDiffStartLine($) +{ + my ($line) = @_; + return $1 if $line =~ /$svnDiffStartRegEx/; + return parseGitDiffStartLine($line) if $line =~ /$gitDiffStartRegEx/; +} + +# Parse the Git diff header start line. +# +# Args: +# $line: "diff --git" line. +# +# Returns the path of the target file. +sub parseGitDiffStartLine($) +{ + my $line = shift; + $_ = $line; + if (/$gitDiffStartWithPrefixRegEx/ || /$gitDiffStartWithoutPrefixNoSpaceRegEx/) { + return $2; + } + # Assume the diff was generated with --no-prefix (e.g. git diff --no-prefix). + if (!/$gitDiffStartWithoutPrefixSourceDirectoryPrefixRegExp/) { + # FIXME: Moving top directory file is not supported (e.g diff --git A.txt B.txt). + die("Could not find '/' in \"diff --git\" line: \"$line\"; only non-prefixed git diffs (i.e. not generated with --no-prefix) that move a top-level directory file are supported."); + } + my $pathPrefix = $1; + if (!/^diff --git \Q$pathPrefix\E.+ (\Q$pathPrefix\E.+)$/) { + # FIXME: Moving a file through sub directories of top directory is not supported (e.g diff --git A/B.txt C/B.txt). + die("Could not find '/' in \"diff --git\" line: \"$line\"; only non-prefixed git diffs (i.e. not generated with --no-prefix) that move a file between top-level directories are supported."); + } + return $1; +} + +# Parse the next Git diff header from the given file handle, and advance +# the handle so the last line read is the first line after the header. +# +# This subroutine dies if given leading junk. +# +# Args: +# $fileHandle: advanced so the last line read from the handle is the first +# line of the header to parse. This should be a line +# beginning with "diff --git". +# $line: the line last read from $fileHandle +# +# Returns ($headerHashRef, $lastReadLine): +# $headerHashRef: a hash reference representing a diff header, as follows-- +# copiedFromPath: the path from which the file was copied or moved if +# the diff is a copy or move. +# executableBitDelta: the value 1 or -1 if the executable bit was added or +# removed, respectively. New and deleted files have +# this value only if the file is executable, in which +# case the value is 1 and -1, respectively. +# indexPath: the path of the target file. +# isBinary: the value 1 if the diff is for a binary file. +# isDeletion: the value 1 if the diff is a file deletion. +# isCopyWithChanges: the value 1 if the file was copied or moved and +# the target file was changed in some way after being +# copied or moved (e.g. if its contents or executable +# bit were changed). +# isNew: the value 1 if the diff is for a new file. +# shouldDeleteSource: the value 1 if the file was copied or moved and +# the source file was deleted -- i.e. if the copy +# was actually a move. +# svnConvertedText: the header text with some lines converted to SVN +# format. Git-specific lines are preserved. +# $lastReadLine: the line last read from $fileHandle. +sub parseGitDiffHeader($$) +{ + my ($fileHandle, $line) = @_; + + $_ = $line; + + my $indexPath; + if (/$gitDiffStartRegEx/) { + # Use $POSTMATCH to preserve the end-of-line character. + my $eol = $POSTMATCH; + + # The first and second paths can differ in the case of copies + # and renames. We use the second file path because it is the + # destination path. + $indexPath = adjustPathForRecentRenamings(parseGitDiffStartLine($_)); + + $_ = "Index: $indexPath$eol"; # Convert to SVN format. + } else { + die("Could not parse leading \"diff --git\" line: \"$line\"."); + } + + my $copiedFromPath; + my $foundHeaderEnding; + my $isBinary; + my $isDeletion; + my $isNew; + my $newExecutableBit = 0; + my $oldExecutableBit = 0; + my $shouldDeleteSource = 0; + my $similarityIndex = 0; + my $svnConvertedText; + while (1) { + # Temporarily strip off any end-of-line characters to simplify + # regex matching below. + s/([\n\r]+)$//; + my $eol = $1; + + if (/^(deleted file|old) mode (\d+)/) { + $oldExecutableBit = (isExecutable($2) ? 1 : 0); + $isDeletion = 1 if $1 eq "deleted file"; + } elsif (/^new( file)? mode (\d+)/) { + $newExecutableBit = (isExecutable($2) ? 1 : 0); + $isNew = 1 if $1; + } elsif (/^similarity index (\d+)%/) { + $similarityIndex = $1; + } elsif (/^copy from ([^\t\r\n]+)/) { + $copiedFromPath = $1; + } elsif (/^rename from ([^\t\r\n]+)/) { + # FIXME: Record this as a move rather than as a copy-and-delete. + # This will simplify adding rename support to svn-unapply. + # Otherwise, the hash for a deletion would have to know + # everything about the file being deleted in order to + # support undoing itself. Recording as a move will also + # permit us to use "svn move" and "git move". + $copiedFromPath = $1; + $shouldDeleteSource = 1; + } elsif (/^--- \S+/) { + # Convert to SVN format. + # We emit the suffix "\t(revision 0)" to handle $indexPath which contains a space character. + # The patch(1) command thinks a file path is characters before a tab. + # This suffix make our diff more closely match the SVN diff format. + $_ = "--- $indexPath\t(revision 0)"; + } elsif (/^\+\+\+ \S+/) { + # Convert to SVN format. + # We emit the suffix "\t(working copy)" to handle $indexPath which contains a space character. + # The patch(1) command thinks a file path is characters before a tab. + # This suffix make our diff more closely match the SVN diff format. + $_ = "+++ $indexPath\t(working copy)"; + $foundHeaderEnding = 1; + } elsif (/^GIT binary patch$/ ) { + $isBinary = 1; + $foundHeaderEnding = 1; + # The "git diff" command includes a line of the form "Binary files + # <path1> and <path2> differ" if the --binary flag is not used. + } elsif (/^Binary files / ) { + die("Error: the Git diff contains a binary file without the binary data in ". + "line: \"$_\". Be sure to use the --binary flag when invoking \"git diff\" ". + "with diffs containing binary files."); + } + + $svnConvertedText .= "$_$eol"; # Also restore end-of-line characters. + + $_ = <$fileHandle>; # Not defined if end-of-file reached. + + last if (!defined($_) || /$gitDiffStartRegEx/ || $foundHeaderEnding); + } + + my $executableBitDelta = $newExecutableBit - $oldExecutableBit; + + my %header; + + $header{copiedFromPath} = $copiedFromPath if $copiedFromPath; + $header{executableBitDelta} = $executableBitDelta if $executableBitDelta; + $header{indexPath} = $indexPath; + $header{isBinary} = $isBinary if $isBinary; + $header{isCopyWithChanges} = 1 if ($copiedFromPath && ($similarityIndex != 100 || $executableBitDelta)); + $header{isDeletion} = $isDeletion if $isDeletion; + $header{isNew} = $isNew if $isNew; + $header{shouldDeleteSource} = $shouldDeleteSource if $shouldDeleteSource; + $header{svnConvertedText} = $svnConvertedText; + + return (\%header, $_); +} + +# Parse the next SVN diff header from the given file handle, and advance +# the handle so the last line read is the first line after the header. +# +# This subroutine dies if given leading junk or if it could not detect +# the end of the header block. +# +# Args: +# $fileHandle: advanced so the last line read from the handle is the first +# line of the header to parse. This should be a line +# beginning with "Index:". +# $line: the line last read from $fileHandle +# +# Returns ($headerHashRef, $lastReadLine): +# $headerHashRef: a hash reference representing a diff header, as follows-- +# copiedFromPath: the path from which the file was copied if the diff +# is a copy. +# indexPath: the path of the target file, which is the path found in +# the "Index:" line. +# isBinary: the value 1 if the diff is for a binary file. +# isNew: the value 1 if the diff is for a new file. +# sourceRevision: the revision number of the source, if it exists. This +# is the same as the revision number the file was copied +# from, in the case of a file copy. +# svnConvertedText: the header text converted to a header with the paths +# in some lines corrected. +# $lastReadLine: the line last read from $fileHandle. +sub parseSvnDiffHeader($$) +{ + my ($fileHandle, $line) = @_; + + $_ = $line; + + my $indexPath; + if (/$svnDiffStartRegEx/) { + $indexPath = adjustPathForRecentRenamings($1); + } else { + die("First line of SVN diff does not begin with \"Index \": \"$_\""); + } + + my $copiedFromPath; + my $foundHeaderEnding; + my $isBinary; + my $isNew; + my $sourceRevision; + my $svnConvertedText; + while (1) { + # Temporarily strip off any end-of-line characters to simplify + # regex matching below. + s/([\n\r]+)$//; + my $eol = $1; + + # Fix paths on "---" and "+++" lines to match the leading + # index line. + if (s/^--- [^\t\n\r]+/--- $indexPath/) { + # --- + if (/^--- .+\(revision (\d+)\)/) { + $sourceRevision = $1; + $isNew = 1 if !$sourceRevision; # if revision 0. + if (/\(from (\S+):(\d+)\)$/) { + # The "from" clause is created by svn-create-patch, in + # which case there is always also a "revision" clause. + $copiedFromPath = $1; + die("Revision number \"$2\" in \"from\" clause does not match " . + "source revision number \"$sourceRevision\".") if ($2 != $sourceRevision); + } + } + } elsif (s/^\+\+\+ [^\t\n\r]+/+++ $indexPath/ || $isBinary && /^$/) { + $foundHeaderEnding = 1; + } elsif (/^Cannot display: file marked as a binary type.$/) { + $isBinary = 1; + # SVN 1.7 has an unusual display format for a binary diff. It repeats the first + # two lines of the diff header. For example: + # Index: test_file.swf + # =================================================================== + # Cannot display: file marked as a binary type. + # svn:mime-type = application/octet-stream + # Index: test_file.swf + # =================================================================== + # --- test_file.swf + # +++ test_file.swf + # + # ... + # Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA== + # Therefore, we continue reading the diff header until we either encounter a line + # that begins with "+++" (SVN 1.7 or greater) or an empty line (SVN version less + # than 1.7). + } + + $svnConvertedText .= "$_$eol"; # Also restore end-of-line characters. + + $_ = <$fileHandle>; # Not defined if end-of-file reached. + + last if (!defined($_) || !$isBinary && /$svnDiffStartRegEx/ || $foundHeaderEnding); + } + + if (!$foundHeaderEnding) { + die("Did not find end of header block corresponding to index path \"$indexPath\"."); + } + + my %header; + + $header{copiedFromPath} = $copiedFromPath if $copiedFromPath; + $header{indexPath} = $indexPath; + $header{isBinary} = $isBinary if $isBinary; + $header{isNew} = $isNew if $isNew; + $header{sourceRevision} = $sourceRevision if $sourceRevision; + $header{svnConvertedText} = $svnConvertedText; + + return (\%header, $_); +} + +# Parse the next Unified diff header from the given file handle, and advance +# the handle so the last line read is the first line after the header. +# +# This subroutine dies if given leading junk. +# +# Args: +# $fileHandle: advanced so the last line read from the handle is the first +# line of the header to parse. This should be a line +# beginning with "Index:". +# $line: the line last read from $fileHandle +# +# Returns ($headerHashRef, $lastReadLine): +# $headerHashRef: a hash reference representing a diff header, as follows-- +# indexPath: the path of the target file, which is the path found in +# the "Index:" line. +# isNew: the value 1 if the diff is for a new file. +# isDeletion: the value 1 if the diff is a file deletion. +# svnConvertedText: the header text converted to a header with the paths +# in some lines corrected. +# $lastReadLine: the line last read from $fileHandle. +sub parseUnifiedDiffHeader($$) +{ + my ($fileHandle, $line) = @_; + + $_ = $line; + + my $currentPosition = tell($fileHandle); + my $indexLine; + my $indexPath; + if (/$unifiedDiffStartRegEx/) { + # Use $POSTMATCH to preserve the end-of-line character. + my $eol = $POSTMATCH; + + $indexPath = $2; + + # In the case of an addition, we look at the next line for the index path + if ($indexPath eq "/dev/null") { + $_ = <$fileHandle>; + if (/^\+\+\+ ([abc]\/)?([^\t\n\r]+)/) { + $indexPath = $2; + } else { + die "Unrecognized unified diff format."; + } + $_ = $line; + } + + $indexLine = "Index: $indexPath$eol"; # Convert to SVN format. + } else { + die("Could not parse leading \"---\" line: \"$line\"."); + } + + seek($fileHandle, $currentPosition, 0); + + my $isDeletion; + my $isHeaderEnding; + my $isNew; + my $svnConvertedText = $indexLine; + while (1) { + # Temporarily strip off any end-of-line characters to simplify + # regex matching below. + s/([\n\r]+)$//; + my $eol = $1; + + if (/^--- \/dev\/null/) { + $isNew = 1; + } elsif (/^\+\+\+ \/dev\/null/) { + $isDeletion = 1; + } + + if (/^(---|\+\+\+) ([abc]\/)?([^\t\n\r]+)/) { + if ($1 eq "---") { + my $prependText = ""; + $prependText = "new file mode 100644\n" if $isNew; + $_ = "${prependText}index 0000000..0000000\n$1 $3"; + } else { + $_ = "$1 $3"; + $isHeaderEnding = 1; + } + } + + $svnConvertedText .= "$_$eol"; # Also restore end-of-line characters. + + $currentPosition = tell($fileHandle); + $_ = <$fileHandle>; # Not defined if end-of-file reached. + last if (!defined($_) || /$unifiedDiffStartRegEx/ || $isHeaderEnding); + } + + my %header; + + $header{indexPath} = $indexPath; + $header{isDeletion} = $isDeletion if $isDeletion; + $header{isNew} = $isNew if $isNew; + $header{svnConvertedText} = $svnConvertedText; + + return (\%header, $_); +} + +# Parse the next diff header from the given file handle, and advance +# the handle so the last line read is the first line after the header. +# +# This subroutine dies if given leading junk or if it could not detect +# the end of the header block. +# +# Args: +# $fileHandle: advanced so the last line read from the handle is the first +# line of the header to parse. For SVN-formatted diffs, this +# is a line beginning with "Index:". For Git, this is a line +# beginning with "diff --git". +# $line: the line last read from $fileHandle +# +# Returns ($headerHashRef, $lastReadLine): +# $headerHashRef: a hash reference representing a diff header +# copiedFromPath: the path from which the file was copied if the diff +# is a copy. +# executableBitDelta: the value 1 or -1 if the executable bit was added or +# removed, respectively. New and deleted files have +# this value only if the file is executable, in which +# case the value is 1 and -1, respectively. +# indexPath: the path of the target file. +# isBinary: the value 1 if the diff is for a binary file. +# isGit: the value 1 if the diff is Git-formatted. +# isSvn: the value 1 if the diff is SVN-formatted. +# sourceRevision: the revision number of the source, if it exists. This +# is the same as the revision number the file was copied +# from, in the case of a file copy. +# svnConvertedText: the header text with some lines converted to SVN +# format. Git-specific lines are preserved. +# $lastReadLine: the line last read from $fileHandle. +sub parseDiffHeader($$) +{ + my ($fileHandle, $line) = @_; + + my $header; # This is a hash ref. + my $isGit; + my $isSvn; + my $isUnified; + my $lastReadLine; + + if ($line =~ $svnDiffStartRegEx) { + $isSvn = 1; + ($header, $lastReadLine) = parseSvnDiffHeader($fileHandle, $line); + } elsif ($line =~ $gitDiffStartRegEx) { + $isGit = 1; + ($header, $lastReadLine) = parseGitDiffHeader($fileHandle, $line); + } elsif ($line =~ $unifiedDiffStartRegEx) { + $isUnified = 1; + ($header, $lastReadLine) = parseUnifiedDiffHeader($fileHandle, $line); + } else { + die("First line of diff does not begin with \"Index:\" or \"diff --git\": \"$line\""); + } + + $header->{isGit} = $isGit if $isGit; + $header->{isSvn} = $isSvn if $isSvn; + $header->{isUnified} = $isUnified if $isUnified; + + return ($header, $lastReadLine); +} + +# FIXME: The %diffHash "object" should not have an svnConvertedText property. +# Instead, the hash object should store its information in a +# structured way as properties. This should be done in a way so +# that, if necessary, the text of an SVN or Git patch can be +# reconstructed from the information in those hash properties. +# +# A %diffHash is a hash representing a source control diff of a single +# file operation (e.g. a file modification, copy, or delete). +# +# These hashes appear, for example, in the parseDiff(), parsePatch(), +# and prepareParsedPatch() subroutines of this package. +# +# The corresponding values are-- +# +# copiedFromPath: the path from which the file was copied if the diff +# is a copy. +# executableBitDelta: the value 1 or -1 if the executable bit was added or +# removed from the target file, respectively. +# indexPath: the path of the target file. For SVN-formatted diffs, +# this is the same as the path in the "Index:" line. +# isBinary: the value 1 if the diff is for a binary file. +# isDeletion: the value 1 if the diff is known from the header to be a deletion. +# isGit: the value 1 if the diff is Git-formatted. +# isNew: the value 1 if the dif is known from the header to be a new file. +# isSvn: the value 1 if the diff is SVN-formatted. +# sourceRevision: the revision number of the source, if it exists. This +# is the same as the revision number the file was copied +# from, in the case of a file copy. +# svnConvertedText: the diff with some lines converted to SVN format. +# Git-specific lines are preserved. + +# Parse one diff from a patch file created by svn-create-patch, and +# advance the file handle so the last line read is the first line +# of the next header block. +# +# This subroutine preserves any leading junk encountered before the header. +# +# Composition of an SVN diff +# +# There are three parts to an SVN diff: the header, the property change, and +# the binary contents, in that order. Either the header or the property change +# may be ommitted, but not both. If there are binary changes, then you always +# have all three. +# +# Args: +# $fileHandle: a file handle advanced to the first line of the next +# header block. Leading junk is okay. +# $line: the line last read from $fileHandle. +# $optionsHashRef: a hash reference representing optional options to use +# when processing a diff. +# shouldNotUseIndexPathEOL: whether to use the line endings in the diff instead +# instead of the line endings in the target file; the +# value of 1 if svnConvertedText should use the line +# endings in the diff. +# +# Returns ($diffHashRefs, $lastReadLine): +# $diffHashRefs: A reference to an array of references to %diffHash hashes. +# See the %diffHash documentation above. +# $lastReadLine: the line last read from $fileHandle +sub parseDiff($$;$) +{ + # FIXME: Adjust this method so that it dies if the first line does not + # match the start of a diff. This will require a change to + # parsePatch() so that parsePatch() skips over leading junk. + my ($fileHandle, $line, $optionsHashRef) = @_; + + my $headerStartRegEx = $svnDiffStartRegEx; # SVN-style header for the default + + my $headerHashRef; # Last header found, as returned by parseDiffHeader(). + my $svnPropertiesHashRef; # Last SVN properties diff found, as returned by parseSvnDiffProperties(). + my $svnText; + my $indexPathEOL; + my $numTextChunks = 0; + while (defined($line)) { + if (!$headerHashRef && ($line =~ $gitDiffStartRegEx)) { + # Then assume all diffs in the patch are Git-formatted. This + # block was made to be enterable at most once since we assume + # all diffs in the patch are formatted the same (SVN or Git). + $headerStartRegEx = $gitDiffStartRegEx; + } + + if (!$headerHashRef && ($line =~ $unifiedDiffStartRegEx)) { + $headerStartRegEx = $unifiedDiffStartRegEx; + } + + if ($line =~ $svnPropertiesStartRegEx) { + my $propertyPath = $1; + if ($svnPropertiesHashRef || $headerHashRef && ($propertyPath ne $headerHashRef->{indexPath})) { + # This is the start of the second diff in the while loop, which happens to + # be a property diff. If $svnPropertiesHasRef is defined, then this is the + # second consecutive property diff, otherwise it's the start of a property + # diff for a file that only has property changes. + last; + } + ($svnPropertiesHashRef, $line) = parseSvnDiffProperties($fileHandle, $line); + next; + } + if ($line !~ $headerStartRegEx) { + # Then we are in the body of the diff. + my $isChunkRange = defined(parseChunkRange($line)); + $numTextChunks += 1 if $isChunkRange; + my $nextLine = <$fileHandle>; + my $willAddNewLineAtEndOfFile = defined($nextLine) && $nextLine =~ /^\\ No newline at end of file$/; + if ($willAddNewLineAtEndOfFile) { + # Diff(1) always emits a LF character preceeding the line "\ No newline at end of file". + # We must preserve both the added LF character and the line ending of this sentinel line + # or patch(1) will complain. + $svnText .= $line . $nextLine; + $line = <$fileHandle>; + next; + } + if ($indexPathEOL && !$isChunkRange) { + # The chunk range is part of the body of the diff, but its line endings should't be + # modified or patch(1) will complain. So, we only modify non-chunk range lines. + $line =~ s/\r\n|\r|\n/$indexPathEOL/g; + } + $svnText .= $line; + $line = $nextLine; + next; + } # Otherwise, we found a diff header. + + if ($svnPropertiesHashRef || $headerHashRef) { + # Then either we just processed an SVN property change or this + # is the start of the second diff header of this while loop. + last; + } + + ($headerHashRef, $line) = parseDiffHeader($fileHandle, $line); + if (!$optionsHashRef || !$optionsHashRef->{shouldNotUseIndexPathEOL}) { + # FIXME: We shouldn't query the file system (via firstEOLInFile()) to determine the + # line endings of the file indexPath. Instead, either the caller to parseDiff() + # should provide this information or parseDiff() should take a delegate that it + # can use to query for this information. + $indexPathEOL = firstEOLInFile($headerHashRef->{indexPath}) if !$headerHashRef->{isNew} && !$headerHashRef->{isBinary}; + } + + $svnText .= $headerHashRef->{svnConvertedText}; + } + + my @diffHashRefs; + + if ($headerHashRef->{shouldDeleteSource}) { + my %deletionHash; + $deletionHash{indexPath} = $headerHashRef->{copiedFromPath}; + $deletionHash{isDeletion} = 1; + push @diffHashRefs, \%deletionHash; + } + if ($headerHashRef->{copiedFromPath}) { + my %copyHash; + $copyHash{copiedFromPath} = $headerHashRef->{copiedFromPath}; + $copyHash{indexPath} = $headerHashRef->{indexPath}; + $copyHash{sourceRevision} = $headerHashRef->{sourceRevision} if $headerHashRef->{sourceRevision}; + if ($headerHashRef->{isSvn}) { + $copyHash{executableBitDelta} = $svnPropertiesHashRef->{executableBitDelta} if $svnPropertiesHashRef->{executableBitDelta}; + } + push @diffHashRefs, \%copyHash; + } + + # Note, the order of evaluation for the following if conditional has been explicitly chosen so that + # it evaluates to false when there is no headerHashRef (e.g. a property change diff for a file that + # only has property changes). + if ($headerHashRef->{isCopyWithChanges} || (%$headerHashRef && !$headerHashRef->{copiedFromPath})) { + # Then add the usual file modification. + my %diffHash; + # FIXME: We should expand this code to support other properties. In the future, + # parseSvnDiffProperties may return a hash whose keys are the properties. + if ($headerHashRef->{isSvn}) { + # SVN records the change to the executable bit in a separate property change diff + # that follows the contents of the diff, except for binary diffs. For binary + # diffs, the property change diff follows the diff header. + $diffHash{executableBitDelta} = $svnPropertiesHashRef->{executableBitDelta} if $svnPropertiesHashRef->{executableBitDelta}; + } elsif ($headerHashRef->{isGit}) { + # Git records the change to the executable bit in the header of a diff. + $diffHash{executableBitDelta} = $headerHashRef->{executableBitDelta} if $headerHashRef->{executableBitDelta}; + } + $diffHash{indexPath} = $headerHashRef->{indexPath}; + $diffHash{isBinary} = $headerHashRef->{isBinary} if $headerHashRef->{isBinary}; + $diffHash{isDeletion} = $headerHashRef->{isDeletion} if $headerHashRef->{isDeletion}; + $diffHash{isGit} = $headerHashRef->{isGit} if $headerHashRef->{isGit}; + $diffHash{isNew} = $headerHashRef->{isNew} if $headerHashRef->{isNew}; + $diffHash{isSvn} = $headerHashRef->{isSvn} if $headerHashRef->{isSvn}; + if (!$headerHashRef->{copiedFromPath}) { + # If the file was copied, then we have already incorporated the + # sourceRevision information into the change. + $diffHash{sourceRevision} = $headerHashRef->{sourceRevision} if $headerHashRef->{sourceRevision}; + } + # FIXME: Remove the need for svnConvertedText. See the %diffHash + # code comments above for more information. + # + # Note, we may not always have SVN converted text since we intend + # to deprecate it in the future. For example, a property change + # diff for a file that only has property changes will not return + # any SVN converted text. + $diffHash{svnConvertedText} = $svnText if $svnText; + $diffHash{numTextChunks} = $numTextChunks if $svnText && !$headerHashRef->{isBinary}; + push @diffHashRefs, \%diffHash; + } + + if (!%$headerHashRef && $svnPropertiesHashRef) { + # A property change diff for a file that only has property changes. + my %propertyChangeHash; + $propertyChangeHash{executableBitDelta} = $svnPropertiesHashRef->{executableBitDelta} if $svnPropertiesHashRef->{executableBitDelta}; + $propertyChangeHash{indexPath} = $svnPropertiesHashRef->{propertyPath}; + $propertyChangeHash{isSvn} = 1; + push @diffHashRefs, \%propertyChangeHash; + } + + return (\@diffHashRefs, $line); +} + +# Parse an SVN property change diff from the given file handle, and advance +# the handle so the last line read is the first line after this diff. +# +# For the case of an SVN binary diff, the binary contents will follow the +# the property changes. +# +# This subroutine dies if the first line does not begin with "Property changes on" +# or if the separator line that follows this line is missing. +# +# Args: +# $fileHandle: advanced so the last line read from the handle is the first +# line of the footer to parse. This line begins with +# "Property changes on". +# $line: the line last read from $fileHandle. +# +# Returns ($propertyHashRef, $lastReadLine): +# $propertyHashRef: a hash reference representing an SVN diff footer. +# propertyPath: the path of the target file. +# executableBitDelta: the value 1 or -1 if the executable bit was added or +# removed from the target file, respectively. +# $lastReadLine: the line last read from $fileHandle. +sub parseSvnDiffProperties($$) +{ + my ($fileHandle, $line) = @_; + + $_ = $line; + + my %footer; + if (/$svnPropertiesStartRegEx/) { + $footer{propertyPath} = $1; + } else { + die("Failed to find start of SVN property change, \"Property changes on \": \"$_\""); + } + + # We advance $fileHandle two lines so that the next line that + # we process is $svnPropertyStartRegEx in a well-formed footer. + # A well-formed footer has the form: + # Property changes on: FileA + # ___________________________________________________________________ + # Added: svn:executable + # + * + $_ = <$fileHandle>; # Not defined if end-of-file reached. + my $separator = "_" x 67; + if (defined($_) && /^$separator[\r\n]+$/) { + $_ = <$fileHandle>; + } else { + die("Failed to find separator line: \"$_\"."); + } + + # FIXME: We should expand this to support other SVN properties + # (e.g. return a hash of property key-values that represents + # all properties). + # + # Notice, we keep processing until we hit end-of-file or some + # line that does not resemble $svnPropertyStartRegEx, such as + # the empty line that precedes the start of the binary contents + # of a patch, or the start of the next diff (e.g. "Index:"). + my $propertyHashRef; + while (defined($_) && /$svnPropertyStartRegEx/) { + ($propertyHashRef, $_) = parseSvnProperty($fileHandle, $_); + if ($propertyHashRef->{name} eq "svn:executable") { + # Notice, for SVN properties, propertyChangeDelta is always non-zero + # because a property can only be added or removed. + $footer{executableBitDelta} = $propertyHashRef->{propertyChangeDelta}; + } + } + + return(\%footer, $_); +} + +# Parse the next SVN property from the given file handle, and advance the handle so the last +# line read is the first line after the property. +# +# This subroutine dies if the first line is not a valid start of an SVN property, +# or the property is missing a value, or the property change type (e.g. "Added") +# does not correspond to the property value type (e.g. "+"). +# +# Args: +# $fileHandle: advanced so the last line read from the handle is the first +# line of the property to parse. This should be a line +# that matches $svnPropertyStartRegEx. +# $line: the line last read from $fileHandle. +# +# Returns ($propertyHashRef, $lastReadLine): +# $propertyHashRef: a hash reference representing a SVN property. +# name: the name of the property. +# value: the last property value. For instance, suppose the property is "Modified". +# Then it has both a '-' and '+' property value in that order. Therefore, +# the value of this key is the value of the '+' property by ordering (since +# it is the last value). +# propertyChangeDelta: the value 1 or -1 if the property was added or +# removed, respectively. +# $lastReadLine: the line last read from $fileHandle. +sub parseSvnProperty($$) +{ + my ($fileHandle, $line) = @_; + + $_ = $line; + + my $propertyName; + my $propertyChangeType; + if (/$svnPropertyStartRegEx/) { + $propertyChangeType = $1; + $propertyName = $2; + } else { + die("Failed to find SVN property: \"$_\"."); + } + + $_ = <$fileHandle>; # Not defined if end-of-file reached. + + if (defined($_) && defined(parseChunkRange($_, "##"))) { + # FIXME: We should validate the chunk range line that is part of an SVN 1.7 + # property diff. For now, we ignore this line. + $_ = <$fileHandle>; + } + + # The "svn diff" command neither inserts newline characters between property values + # nor between successive properties. + # + # As of SVN 1.7, "svn diff" may insert "\ No newline at end of property" after a + # property value that doesn't end in a newline. + # + # FIXME: We do not support property values that contain tailing newline characters + # as it is difficult to disambiguate these trailing newlines from the empty + # line that precedes the contents of a binary patch. + my $propertyValue; + my $propertyValueType; + while (defined($_) && /$svnPropertyValueStartRegEx/) { + # Note, a '-' property may be followed by a '+' property in the case of a "Modified" + # or "Name" property. We only care about the ending value (i.e. the '+' property) + # in such circumstances. So, we take the property value for the property to be its + # last parsed property value. + # + # FIXME: We may want to consider strictly enforcing a '-', '+' property ordering or + # add error checking to prevent '+', '+', ..., '+' and other invalid combinations. + $propertyValueType = $1; + ($propertyValue, $_) = parseSvnPropertyValue($fileHandle, $_); + $_ = <$fileHandle> if defined($_) && /$svnPropertyValueNoNewlineRegEx/; + } + + if (!$propertyValue) { + die("Failed to find the property value for the SVN property \"$propertyName\": \"$_\"."); + } + + my $propertyChangeDelta; + if ($propertyValueType eq "+" || $propertyValueType eq "Merged") { + $propertyChangeDelta = 1; + } elsif ($propertyValueType eq "-" || $propertyValueType eq "Reverse-merged") { + $propertyChangeDelta = -1; + } else { + die("Not reached."); + } + + # We perform a simple validation that an "Added" or "Deleted" property + # change type corresponds with a "+" and "-" value type, respectively. + my $expectedChangeDelta; + if ($propertyChangeType eq "Added") { + $expectedChangeDelta = 1; + } elsif ($propertyChangeType eq "Deleted") { + $expectedChangeDelta = -1; + } + + if ($expectedChangeDelta && $propertyChangeDelta != $expectedChangeDelta) { + die("The final property value type found \"$propertyValueType\" does not " . + "correspond to the property change type found \"$propertyChangeType\"."); + } + + my %propertyHash; + $propertyHash{name} = $propertyName; + $propertyHash{propertyChangeDelta} = $propertyChangeDelta; + $propertyHash{value} = $propertyValue; + return (\%propertyHash, $_); +} + +# Parse the value of an SVN property from the given file handle, and advance +# the handle so the last line read is the first line after the property value. +# +# This subroutine dies if the first line is an invalid SVN property value line +# (i.e. a line that does not begin with " +" or " -"). +# +# Args: +# $fileHandle: advanced so the last line read from the handle is the first +# line of the property value to parse. This should be a line +# beginning with " +" or " -". +# $line: the line last read from $fileHandle. +# +# Returns ($propertyValue, $lastReadLine): +# $propertyValue: the value of the property. +# $lastReadLine: the line last read from $fileHandle. +sub parseSvnPropertyValue($$) +{ + my ($fileHandle, $line) = @_; + + $_ = $line; + + my $propertyValue; + my $eol; + if (/$svnPropertyValueStartRegEx/) { + $propertyValue = $2; # Does not include the end-of-line character(s). + $eol = $POSTMATCH; + } else { + die("Failed to find property value beginning with '+', '-', 'Merged', or 'Reverse-merged': \"$_\"."); + } + + while (<$fileHandle>) { + if (/^[\r\n]+$/ || /$svnPropertyValueStartRegEx/ || /$svnPropertyStartRegEx/ || /$svnPropertyValueNoNewlineRegEx/ || /$svnDiffStartRegEx/) { + # Note, we may encounter an empty line before the contents of a binary patch. + # Also, we check for $svnPropertyValueStartRegEx because a '-' property may be + # followed by a '+' property in the case of a "Modified" or "Name" property. + # We check for $svnPropertyStartRegEx because it indicates the start of the + # next property to parse. + last; + } + + # Temporarily strip off any end-of-line characters. We add the end-of-line characters + # from the previously processed line to the start of this line so that the last line + # of the property value does not end in end-of-line characters. + s/([\n\r]+)$//; + $propertyValue .= "$eol$_"; + $eol = $1; + } + + return ($propertyValue, $_); +} + +# Parse a patch file created by svn-create-patch. +# +# Args: +# $fileHandle: A file handle to the patch file that has not yet been +# read from. +# $optionsHashRef: a hash reference representing optional options to use +# when processing a diff. +# shouldNotUseIndexPathEOL: whether to use the line endings in the diff instead +# instead of the line endings in the target file; the +# value of 1 if svnConvertedText should use the line +# endings in the diff. +# +# Returns: +# @diffHashRefs: an array of diff hash references. +# See the %diffHash documentation above. +sub parsePatch($;$) +{ + my ($fileHandle, $optionsHashRef) = @_; + + my $newDiffHashRefs; + my @diffHashRefs; # return value + + my $line = <$fileHandle>; + + while (defined($line)) { # Otherwise, at EOF. + + ($newDiffHashRefs, $line) = parseDiff($fileHandle, $line, $optionsHashRef); + + push @diffHashRefs, @$newDiffHashRefs; + } + + return @diffHashRefs; +} + +# Prepare the results of parsePatch() for use in svn-apply and svn-unapply. +# +# Args: +# $shouldForce: Whether to continue processing if an unexpected +# state occurs. +# @diffHashRefs: An array of references to %diffHashes. +# See the %diffHash documentation above. +# +# Returns $preparedPatchHashRef: +# copyDiffHashRefs: A reference to an array of the $diffHashRefs in +# @diffHashRefs that represent file copies. The original +# ordering is preserved. +# nonCopyDiffHashRefs: A reference to an array of the $diffHashRefs in +# @diffHashRefs that do not represent file copies. +# The original ordering is preserved. +# sourceRevisionHash: A reference to a hash of source path to source +# revision number. +sub prepareParsedPatch($@) +{ + my ($shouldForce, @diffHashRefs) = @_; + + my %copiedFiles; + + # Return values + my @copyDiffHashRefs = (); + my @nonCopyDiffHashRefs = (); + my %sourceRevisionHash = (); + for my $diffHashRef (@diffHashRefs) { + my $copiedFromPath = $diffHashRef->{copiedFromPath}; + my $indexPath = $diffHashRef->{indexPath}; + my $sourceRevision = $diffHashRef->{sourceRevision}; + my $sourcePath; + + if (defined($copiedFromPath)) { + # Then the diff is a copy operation. + $sourcePath = $copiedFromPath; + + # FIXME: Consider printing a warning or exiting if + # exists($copiedFiles{$indexPath}) is true -- i.e. if + # $indexPath appears twice as a copy target. + $copiedFiles{$indexPath} = $sourcePath; + + push @copyDiffHashRefs, $diffHashRef; + } else { + # Then the diff is not a copy operation. + $sourcePath = $indexPath; + + push @nonCopyDiffHashRefs, $diffHashRef; + } + + if (defined($sourceRevision)) { + if (exists($sourceRevisionHash{$sourcePath}) && + ($sourceRevisionHash{$sourcePath} != $sourceRevision)) { + if (!$shouldForce) { + die "Two revisions of the same file required as a source:\n". + " $sourcePath:$sourceRevisionHash{$sourcePath}\n". + " $sourcePath:$sourceRevision"; + } + } + $sourceRevisionHash{$sourcePath} = $sourceRevision; + } + } + + my %preparedPatchHash; + + $preparedPatchHash{copyDiffHashRefs} = \@copyDiffHashRefs; + $preparedPatchHash{nonCopyDiffHashRefs} = \@nonCopyDiffHashRefs; + $preparedPatchHash{sourceRevisionHash} = \%sourceRevisionHash; + + return \%preparedPatchHash; +} + +# Return localtime() for the project's time zone, given an integer time as +# returned by Perl's time() function. +sub localTimeInProjectTimeZone($) +{ + my $epochTime = shift; + + # Change the time zone temporarily for the localtime() call. + my $savedTimeZone = $ENV{'TZ'}; + $ENV{'TZ'} = $changeLogTimeZone; + my @localTime = localtime($epochTime); + if (defined $savedTimeZone) { + $ENV{'TZ'} = $savedTimeZone; + } else { + delete $ENV{'TZ'}; + } + + return @localTime; +} + +# Set the reviewer and date in a ChangeLog patch, and return the new patch. +# +# Args: +# $patch: a ChangeLog patch as a string. +# $reviewer: the name of the reviewer, or undef if the reviewer should not be set. +# $epochTime: an integer time as returned by Perl's time() function. +sub setChangeLogDateAndReviewer($$$) +{ + my ($patch, $reviewer, $epochTime) = @_; + + my @localTime = localTimeInProjectTimeZone($epochTime); + my $newDate = strftime("%Y-%m-%d", @localTime); + + my $firstChangeLogLineRegEx = qr#(\n\+)\d{4}-[^-]{2}-[^-]{2}( )#; + $patch =~ s/$firstChangeLogLineRegEx/$1$newDate$2/; + + if (defined($reviewer)) { + # We include a leading plus ("+") in the regular expression to make + # the regular expression less likely to match text in the leading junk + # for the patch, if the patch has leading junk. + $patch =~ s/(\n\+.*)NOBODY \(OOPS!\)/$1$reviewer/; + } + + return $patch; +} + +# Removes a leading Subversion header without an associated diff if one exists. +# +# This subroutine dies if the specified patch does not begin with an "Index:" line. +# +# In SVN 1.9 or newer, "svn diff" of a moved/copied file without post changes always +# emits a leading header without an associated diff: +# Index: B.txt +# =================================================================== +# (end of file or next header) +# +# If the same file has a property change then the patch has the form: +# Index: B.txt +# =================================================================== +# Index: B.txt +# =================================================================== +# --- B.txt (revision 1) +# +++ B.txt (working copy) +# +# Property change on B.txt +# ___________________________________________________________________ +# Added: svn:executable +# ## -0,0 +1 ## +# +* +# \ No newline at end of property +# +# We need to apply this function to the ouput of "svn diff" for an addition with history +# to remove a duplicate header so that svn-apply can apply the resulting patch. +sub fixSVNPatchForAdditionWithHistory($) +{ + my ($patch) = @_; + + $patch =~ /(\r?\n)/; + my $lineEnding = $1; + my @lines = split(/$lineEnding/, $patch); + + if ($lines[0] !~ /$svnDiffStartRegEx/) { + die("First line of SVN diff does not begin with \"Index \": \"$lines[0]\""); + } + if (@lines <= 2) { + return ""; + } + splice(@lines, 0, 2) if $lines[2] =~ /$svnDiffStartRegEx/; + return join($lineEnding, @lines) . "\n"; # patch(1) expects an extra trailing newline. +} + +# If possible, returns a ChangeLog patch equivalent to the given one, +# but with the newest ChangeLog entry inserted at the top of the +# file -- i.e. no leading context and all lines starting with "+". +# +# If given a patch string not representable as a patch with the above +# properties, it returns the input back unchanged. +# +# WARNING: This subroutine can return an inequivalent patch string if +# both the beginning of the new ChangeLog file matches the beginning +# of the source ChangeLog, and the source beginning was modified. +# Otherwise, it is guaranteed to return an equivalent patch string, +# if it returns. +# +# Applying this subroutine to ChangeLog patches allows svn-apply to +# insert new ChangeLog entries at the top of the ChangeLog file. +# svn-apply uses patch with --fuzz=3 to do this. We need to apply +# this subroutine because the diff(1) command is greedy when matching +# lines. A new ChangeLog entry with the same date and author as the +# previous will match and cause the diff to have lines of starting +# context. +# +# This subroutine has unit tests in VCSUtils_unittest.pl. +# +# Returns $changeLogHashRef: +# $changeLogHashRef: a hash reference representing a change log patch. +# patch: a ChangeLog patch equivalent to the given one, but with the +# newest ChangeLog entry inserted at the top of the file, if possible. +sub fixChangeLogPatch($) +{ + my $patch = shift; # $patch will only contain patch fragments for ChangeLog. + + $patch =~ s|test_expectations.txt:|TestExpectations:|g; + + $patch =~ /(\r?\n)/; + my $lineEnding = $1; + my @lines = split(/$lineEnding/, $patch); + + my $i = 0; # We reuse the same index throughout. + + # Skip to beginning of first chunk. + for (; $i < @lines; ++$i) { + if (substr($lines[$i], 0, 1) eq "@") { + last; + } + } + my $chunkStartIndex = ++$i; + my %changeLogHashRef; + + # Optimization: do not process if new lines already begin the chunk. + if (substr($lines[$i], 0, 1) eq "+") { + $changeLogHashRef{patch} = $patch; + return \%changeLogHashRef; + } + + # Skip to first line of newly added ChangeLog entry. + # For example, +2009-06-03 Eric Seidel <eric@webkit.org> + my $dateStartRegEx = '^\+(\d{4}-\d{2}-\d{2})' # leading "+" and date + . '\s+(.+)\s+' # name + . '<([^<>]+)>$'; # e-mail address + + for (; $i < @lines; ++$i) { + my $line = $lines[$i]; + my $firstChar = substr($line, 0, 1); + if ($line =~ /$dateStartRegEx/) { + last; + } elsif ($firstChar eq " " or $firstChar eq "+") { + next; + } + $changeLogHashRef{patch} = $patch; # Do not change if, for example, "-" or "@" found. + return \%changeLogHashRef; + } + if ($i >= @lines) { + $changeLogHashRef{patch} = $patch; # Do not change if date not found. + return \%changeLogHashRef; + } + my $dateStartIndex = $i; + + # Rewrite overlapping lines to lead with " ". + my @overlappingLines = (); # These will include a leading "+". + for (; $i < @lines; ++$i) { + my $line = $lines[$i]; + if (substr($line, 0, 1) ne "+") { + last; + } + push(@overlappingLines, $line); + $lines[$i] = " " . substr($line, 1); + } + + # Remove excess ending context, if necessary. + my $shouldTrimContext = 1; + for (; $i < @lines; ++$i) { + my $firstChar = substr($lines[$i], 0, 1); + if ($firstChar eq " ") { + next; + } elsif ($firstChar eq "@") { + last; + } + $shouldTrimContext = 0; # For example, if "+" or "-" encountered. + last; + } + my $deletedLineCount = 0; + if ($shouldTrimContext) { # Also occurs if end of file reached. + splice(@lines, $i - @overlappingLines, @overlappingLines); + $deletedLineCount = @overlappingLines; + } + + # Work backwards, shifting overlapping lines towards front + # while checking that patch stays equivalent. + for ($i = $dateStartIndex - 1; @overlappingLines && $i >= $chunkStartIndex; --$i) { + my $line = $lines[$i]; + if (substr($line, 0, 1) ne " ") { + next; + } + my $text = substr($line, 1); + my $newLine = pop(@overlappingLines); + if ($text ne substr($newLine, 1)) { + $changeLogHashRef{patch} = $patch; # Unexpected difference. + return \%changeLogHashRef; + } + $lines[$i] = "+$text"; + } + + # If @overlappingLines > 0, this is where we make use of the + # assumption that the beginning of the source file was not modified. + splice(@lines, $chunkStartIndex, 0, @overlappingLines); + + # Update the date start index as it may have changed after shifting + # the overlapping lines towards the front. + for ($i = $chunkStartIndex; $i < $dateStartIndex; ++$i) { + $dateStartIndex = $i if $lines[$i] =~ /$dateStartRegEx/; + } + splice(@lines, $chunkStartIndex, $dateStartIndex - $chunkStartIndex); # Remove context of later entry. + $deletedLineCount += $dateStartIndex - $chunkStartIndex; + + # Update the initial chunk range. + my $chunkRangeHashRef = parseChunkRange($lines[$chunkStartIndex - 1]); + if (!$chunkRangeHashRef) { + # FIXME: Handle errors differently from ChangeLog files that + # are okay but should not be altered. That way we can find out + # if improvements to the script ever become necessary. + $changeLogHashRef{patch} = $patch; # Error: unexpected patch string format. + return \%changeLogHashRef; + } + my $oldSourceLineCount = $chunkRangeHashRef->{lineCount}; + my $oldTargetLineCount = $chunkRangeHashRef->{newLineCount}; + + my $sourceLineCount = $oldSourceLineCount + @overlappingLines - $deletedLineCount; + my $targetLineCount = $oldTargetLineCount + @overlappingLines - $deletedLineCount; + $lines[$chunkStartIndex - 1] = "@@ -1,$sourceLineCount +1,$targetLineCount @@"; + + $changeLogHashRef{patch} = join($lineEnding, @lines) . "\n"; # patch(1) expects an extra trailing newline. + return \%changeLogHashRef; +} + +# This is a supporting method for runPatchCommand. +# +# Arg: the optional $args parameter passed to runPatchCommand (can be undefined). +# +# Returns ($patchCommand, $isForcing). +# +# This subroutine has unit tests in VCSUtils_unittest.pl. +sub generatePatchCommand($) +{ + my ($passedArgsHashRef) = @_; + + my $argsHashRef = { # Defaults + ensureForce => 0, + shouldReverse => 0, + options => [] + }; + + # Merges hash references. It's okay here if passed hash reference is undefined. + @{$argsHashRef}{keys %{$passedArgsHashRef}} = values %{$passedArgsHashRef}; + + my $ensureForce = $argsHashRef->{ensureForce}; + my $shouldReverse = $argsHashRef->{shouldReverse}; + my $options = $argsHashRef->{options}; + + if (! $options) { + $options = []; + } else { + $options = [@{$options}]; # Copy to avoid side effects. + } + + my $isForcing = 0; + if (grep /^--force$/, @{$options}) { + $isForcing = 1; + } elsif ($ensureForce) { + push @{$options}, "--force"; + $isForcing = 1; + } + + if ($shouldReverse) { # No check: --reverse should never be passed explicitly. + push @{$options}, "--reverse"; + } + + @{$options} = sort(@{$options}); # For easier testing. + + my $patchCommand = join(" ", "patch -p0", @{$options}); + + return ($patchCommand, $isForcing); +} + +# Apply the given patch using the patch(1) command. +# +# On success, return the resulting exit status. Otherwise, exit with the +# exit status. If "--force" is passed as an option, however, then never +# exit and always return the exit status. +# +# Args: +# $patch: a patch string. +# $repositoryRootPath: an absolute path to the repository root. +# $pathRelativeToRoot: the path of the file to be patched, relative to the +# repository root. This should normally be the path +# found in the patch's "Index:" line. It is passed +# explicitly rather than reparsed from the patch +# string for optimization purposes. +# This is used only for error reporting. The +# patch command gleans the actual file to patch +# from the patch string. +# $args: a reference to a hash of optional arguments. The possible +# keys are -- +# ensureForce: whether to ensure --force is passed (defaults to 0). +# shouldReverse: whether to pass --reverse (defaults to 0). +# options: a reference to an array of options to pass to the +# patch command. The subroutine passes the -p0 option +# no matter what. This should not include --reverse. +# +# This subroutine has unit tests in VCSUtils_unittest.pl. +sub runPatchCommand($$$;$) +{ + my ($patch, $repositoryRootPath, $pathRelativeToRoot, $args) = @_; + + my ($patchCommand, $isForcing) = generatePatchCommand($args); + + # Temporarily change the working directory since the path found + # in the patch's "Index:" line is relative to the repository root + # (i.e. the same as $pathRelativeToRoot). + my $cwd = Cwd::getcwd(); + chdir $repositoryRootPath; + + open PATCH, "| $patchCommand" or die "Could not call \"$patchCommand\" for file \"$pathRelativeToRoot\": $!"; + print PATCH $patch; + close PATCH; + my $exitStatus = exitStatus($?); + + chdir $cwd; + + if ($exitStatus && !$isForcing) { + print "Calling \"$patchCommand\" for file \"$pathRelativeToRoot\" returned " . + "status $exitStatus. Pass --force to ignore patch failures.\n"; + exit $exitStatus; + } + + return $exitStatus; +} + +# Merge ChangeLog patches using a three-file approach. +# +# This is used by resolve-ChangeLogs when it's operated as a merge driver +# and when it's used to merge conflicts after a patch is applied or after +# an svn update. +# +# It's also used for traditional rejected patches. +# +# Args: +# $fileMine: The merged version of the file. Also known in git as the +# other branch's version (%B) or "ours". +# For traditional patch rejects, this is the *.rej file. +# $fileOlder: The base version of the file. Also known in git as the +# ancestor version (%O) or "base". +# For traditional patch rejects, this is the *.orig file. +# $fileNewer: The current version of the file. Also known in git as the +# current version (%A) or "theirs". +# For traditional patch rejects, this is the original-named +# file. +# +# Returns 1 if merge was successful, else 0. +sub mergeChangeLogs($$$) +{ + my ($fileMine, $fileOlder, $fileNewer) = @_; + + my $traditionalReject = $fileMine =~ /\.rej$/ ? 1 : 0; + + local $/ = undef; + + my $patch; + if ($traditionalReject) { + open(DIFF, "<", $fileMine) or die $!; + $patch = <DIFF>; + close(DIFF); + rename($fileMine, "$fileMine.save"); + rename($fileOlder, "$fileOlder.save"); + } else { + open(DIFF, "diff -u -a --binary \"$fileOlder\" \"$fileMine\" |") or die $!; + $patch = <DIFF>; + close(DIFF); + } + + unlink("${fileNewer}.orig"); + unlink("${fileNewer}.rej"); + + open(PATCH, "| patch --force --fuzz=3 --binary \"$fileNewer\" > " . File::Spec->devnull()) or die $!; + if ($traditionalReject) { + print PATCH $patch; + } else { + my $changeLogHash = fixChangeLogPatch($patch); + print PATCH $changeLogHash->{patch}; + } + close(PATCH); + + my $result = !exitStatus($?); + + # Refuse to merge the patch if it did not apply cleanly + if (-e "${fileNewer}.rej") { + unlink("${fileNewer}.rej"); + if (-f "${fileNewer}.orig") { + unlink($fileNewer); + rename("${fileNewer}.orig", $fileNewer); + } + } else { + unlink("${fileNewer}.orig"); + } + + if ($traditionalReject) { + rename("$fileMine.save", $fileMine); + rename("$fileOlder.save", $fileOlder); + } + + return $result; +} + +sub gitConfig($) +{ + return unless isGit(); + + my ($config) = @_; + + my $result = `git config $config`; + chomp $result; + return $result; +} + +sub changeLogNameError($) +{ + my ($message) = @_; + print STDERR "$message\nEither:\n"; + print STDERR " set CHANGE_LOG_NAME in your environment\n"; + print STDERR " OR pass --name= on the command line\n"; + print STDERR " OR set REAL_NAME in your environment"; + print STDERR " OR git users can set 'git config user.name'\n"; + exit(1); +} + +sub changeLogName() +{ + my $name = $ENV{CHANGE_LOG_NAME} || $ENV{REAL_NAME} || gitConfig("user.name"); + if (not $name and !isWindows()) { + $name = (split /\s*,\s*/, (getpwuid $<)[6])[0]; + } + + changeLogNameError("Failed to determine ChangeLog name.") unless $name; + # getpwuid seems to always succeed on windows, returning the username instead of the full name. This check will catch that case. + changeLogNameError("'$name' does not contain a space! ChangeLogs should contain your full name.") unless ($name =~ /\S\s\S/); + + return $name; +} + +sub changeLogEmailAddressError($) +{ + my ($message) = @_; + print STDERR "$message\nEither:\n"; + print STDERR " set CHANGE_LOG_EMAIL_ADDRESS in your environment\n"; + print STDERR " OR pass --email= on the command line\n"; + print STDERR " OR set EMAIL_ADDRESS in your environment\n"; + print STDERR " OR git users can set 'git config user.email'\n"; + exit(1); +} + +sub changeLogEmailAddress() +{ + my $emailAddress = $ENV{CHANGE_LOG_EMAIL_ADDRESS} || $ENV{EMAIL_ADDRESS} || gitConfig("user.email"); + + changeLogEmailAddressError("Failed to determine email address for ChangeLog.") unless $emailAddress; + changeLogEmailAddressError("Email address '$emailAddress' does not contain '\@' and is likely invalid.") unless ($emailAddress =~ /\@/); + + return $emailAddress; +} + +# http://tools.ietf.org/html/rfc1924 +sub decodeBase85($) +{ + my ($encoded) = @_; + my %table; + my @characters = ('0'..'9', 'A'..'Z', 'a'..'z', '!', '#', '$', '%', '&', '(', ')', '*', '+', '-', ';', '<', '=', '>', '?', '@', '^', '_', '`', '{', '|', '}', '~'); + for (my $i = 0; $i < 85; $i++) { + $table{$characters[$i]} = $i; + } + + my $decoded = ''; + my @encodedChars = $encoded =~ /./g; + + for (my $encodedIter = 0; defined($encodedChars[$encodedIter]);) { + my $digit = 0; + for (my $i = 0; $i < 5; $i++) { + $digit *= 85; + my $char = $encodedChars[$encodedIter]; + $digit += $table{$char}; + $encodedIter++; + } + + for (my $i = 0; $i < 4; $i++) { + $decoded .= chr(($digit >> (3 - $i) * 8) & 255); + } + } + + return $decoded; +} + +sub decodeGitBinaryChunk($$) +{ + my ($contents, $fullPath) = @_; + + # Load this module lazily in case the user don't have this module + # and won't handle git binary patches. + require Compress::Zlib; + + my $encoded = ""; + my $compressedSize = 0; + while ($contents =~ /^([A-Za-z])(.*)$/gm) { + my $line = $2; + next if $line eq ""; + die "$fullPath: unexpected size of a line: $&" if length($2) % 5 != 0; + my $actualSize = length($2) / 5 * 4; + my $encodedExpectedSize = ord($1); + my $expectedSize = $encodedExpectedSize <= ord("Z") ? $encodedExpectedSize - ord("A") + 1 : $encodedExpectedSize - ord("a") + 27; + + die "$fullPath: unexpected size of a line: $&" if int(($expectedSize + 3) / 4) * 4 != $actualSize; + $compressedSize += $expectedSize; + $encoded .= $line; + } + + my $compressed = decodeBase85($encoded); + $compressed = substr($compressed, 0, $compressedSize); + return Compress::Zlib::uncompress($compressed); +} + +sub decodeGitBinaryPatch($$) +{ + my ($contents, $fullPath) = @_; + + # Git binary patch has two chunks. One is for the normal patching + # and another is for the reverse patching. + # + # Each chunk a line which starts from either "literal" or "delta", + # followed by a number which specifies decoded size of the chunk. + # + # Then, content of the chunk comes. To decode the content, we + # need decode it with base85 first, and then zlib. + my $gitPatchRegExp = '(literal|delta) ([0-9]+)\n([A-Za-z0-9!#$%&()*+-;<=>?@^_`{|}~\\n]*?)\n\n'; + if ($contents !~ m"\nGIT binary patch\n$gitPatchRegExp$gitPatchRegExp(\Z|-- \n)") { + return (); + } + + my $binaryChunkType = $1; + my $binaryChunkExpectedSize = $2; + my $encodedChunk = $3; + my $reverseBinaryChunkType = $4; + my $reverseBinaryChunkExpectedSize = $5; + my $encodedReverseChunk = $6; + + my $binaryChunk = decodeGitBinaryChunk($encodedChunk, $fullPath); + my $binaryChunkActualSize = length($binaryChunk); + my $reverseBinaryChunk = decodeGitBinaryChunk($encodedReverseChunk, $fullPath); + my $reverseBinaryChunkActualSize = length($reverseBinaryChunk); + + die "$fullPath: unexpected size of the first chunk (expected $binaryChunkExpectedSize but was $binaryChunkActualSize" if ($binaryChunkType eq "literal" and $binaryChunkExpectedSize != $binaryChunkActualSize); + die "$fullPath: unexpected size of the second chunk (expected $reverseBinaryChunkExpectedSize but was $reverseBinaryChunkActualSize" if ($reverseBinaryChunkType eq "literal" and $reverseBinaryChunkExpectedSize != $reverseBinaryChunkActualSize); + + return ($binaryChunkType, $binaryChunk, $reverseBinaryChunkType, $reverseBinaryChunk); +} + +sub readByte($$) +{ + my ($data, $location) = @_; + + # Return the byte at $location in $data as a numeric value. + return ord(substr($data, $location, 1)); +} + +# The git binary delta format is undocumented, except in code: +# - https://github.com/git/git/blob/master/delta.h:get_delta_hdr_size is the source +# of the algorithm in decodeGitBinaryPatchDeltaSize. +# - https://github.com/git/git/blob/master/patch-delta.c:patch_delta is the source +# of the algorithm in applyGitBinaryPatchDelta. +sub decodeGitBinaryPatchDeltaSize($) +{ + my ($binaryChunk) = @_; + + # Source and destination buffer sizes are stored in 7-bit chunks at the + # start of the binary delta patch data. The highest bit in each byte + # except the last is set; the remaining 7 bits provide the next + # chunk of the size. The chunks are stored in ascending significance + # order. + my $cmd; + my $size = 0; + my $shift = 0; + for (my $i = 0; $i < length($binaryChunk);) { + $cmd = readByte($binaryChunk, $i++); + $size |= ($cmd & 0x7f) << $shift; + $shift += 7; + if (!($cmd & 0x80)) { + return ($size, $i); + } + } +} + +sub applyGitBinaryPatchDelta($$) +{ + my ($binaryChunk, $originalContents) = @_; + + # Git delta format consists of two headers indicating source buffer size + # and result size, then a series of commands. Each command is either + # a copy-from-old-version (the 0x80 bit is set) or a copy-from-delta + # command. Commands are applied sequentially to generate the result. + # + # A copy-from-old-version command encodes an offset and size to copy + # from in subsequent bits, while a copy-from-delta command consists only + # of the number of bytes to copy from the delta. + + # We don't use these values, but we need to know how big they are so that + # we can skip to the diff data. + my ($size, $bytesUsed) = decodeGitBinaryPatchDeltaSize($binaryChunk); + $binaryChunk = substr($binaryChunk, $bytesUsed); + ($size, $bytesUsed) = decodeGitBinaryPatchDeltaSize($binaryChunk); + $binaryChunk = substr($binaryChunk, $bytesUsed); + + my $out = ""; + for (my $i = 0; $i < length($binaryChunk); ) { + my $cmd = ord(substr($binaryChunk, $i++, 1)); + if ($cmd & 0x80) { + # Extract an offset and size from the delta data, then copy + # $size bytes from $offset in the original data into the output. + my $offset = 0; + my $size = 0; + if ($cmd & 0x01) { $offset = readByte($binaryChunk, $i++); } + if ($cmd & 0x02) { $offset |= readByte($binaryChunk, $i++) << 8; } + if ($cmd & 0x04) { $offset |= readByte($binaryChunk, $i++) << 16; } + if ($cmd & 0x08) { $offset |= readByte($binaryChunk, $i++) << 24; } + if ($cmd & 0x10) { $size = readByte($binaryChunk, $i++); } + if ($cmd & 0x20) { $size |= readByte($binaryChunk, $i++) << 8; } + if ($cmd & 0x40) { $size |= readByte($binaryChunk, $i++) << 16; } + if ($size == 0) { $size = 0x10000; } + $out .= substr($originalContents, $offset, $size); + } elsif ($cmd) { + # Copy $cmd bytes from the delta data into the output. + $out .= substr($binaryChunk, $i, $cmd); + $i += $cmd; + } else { + die "unexpected delta opcode 0"; + } + } + + return $out; +} + +sub escapeSubversionPath($) +{ + my ($path) = @_; + $path .= "@" if $path =~ /@/; + return $path; +} + +sub runCommand(@) +{ + my @args = @_; + my $pid = open(CHILD, "-|"); + if (!defined($pid)) { + die "Failed to fork(): $!"; + } + if ($pid) { + # Parent process + my $childStdout; + while (<CHILD>) { + $childStdout .= $_; + } + close(CHILD); + my %childOutput; + $childOutput{exitStatus} = exitStatus($?); + $childOutput{stdout} = $childStdout if $childStdout; + return \%childOutput; + } + # Child process + # FIXME: Consider further hardening of this function, including sanitizing the environment. + exec { $args[0] } @args or die "Failed to exec(): $!"; +} + +sub gitCommitForSVNRevision +{ + my ($svnRevision) = @_; + my $command = "git svn find-rev r" . $svnRevision; + $command = "LC_ALL=C $command" if !isWindows(); + my $gitHash = `$command`; + if (!defined($gitHash)) { + $gitHash = "unknown"; + warn "Unable to determine GIT commit from SVN revision"; + } else { + chop($gitHash); + } + return $gitHash; +} + +sub listOfChangedFilesBetweenRevisions +{ + my ($sourceDir, $firstRevision, $lastRevision) = @_; + my $command; + + if ($firstRevision eq "unknown" or $lastRevision eq "unknown") { + return (); + } + + # Some VCS functions don't work from within the build dir, so always + # go to the source dir first. + my $cwd = Cwd::getcwd(); + chdir $sourceDir; + + if (isGit()) { + my $firstCommit = gitCommitForSVNRevision($firstRevision); + my $lastCommit = gitCommitForSVNRevision($lastRevision); + $command = "git diff --name-status $firstCommit..$lastCommit"; + } elsif (isSVN()) { + $command = "svn diff --summarize -r $firstRevision:$lastRevision"; + } + + my @result = (); + + if ($command) { + my $diffOutput = `$command`; + $diffOutput =~ s/^[A-Z]\s+//gm; + @result = split(/[\r\n]+/, $diffOutput); + } + + chdir $cwd; + + return @result; +} + + +1; diff --git a/Tools/Scripts/run-gtk-tests b/Tools/Scripts/run-gtk-tests new file mode 100755 index 000000000..a3d3e6f28 --- /dev/null +++ b/Tools/Scripts/run-gtk-tests @@ -0,0 +1,494 @@ +#!/usr/bin/env python +# +# Copyright (C) 2011, 2012 Igalia S.L. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Library General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Library General Public License for more details. +# +# You should have received a copy of the GNU Library General Public License +# along with this library; see the file COPYING.LIB. If not, write to +# the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +# Boston, MA 02110-1301, USA. + +import logging +import subprocess +import os +import sys +import optparse +import re +from signal import alarm, signal, SIGALRM, SIGKILL, SIGSEGV +from gi.repository import Gio, GLib + +top_level_directory = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..")) +sys.path.append(os.path.join(top_level_directory, "Tools", "jhbuild")) +sys.path.append(os.path.join(top_level_directory, "Tools", "gtk")) +import common +import jhbuildutils +from webkitpy.common.host import Host + +class SkippedTest: + ENTIRE_SUITE = None + + def __init__(self, test, test_case, reason, bug, build_type=None): + self.test = test + self.test_case = test_case + self.reason = reason + self.bug = bug + self.build_type = build_type + + def __str__(self): + skipped_test_str = "%s" % self.test + + if not(self.skip_entire_suite()): + skipped_test_str += " [%s]" % self.test_case + + skipped_test_str += ": %s (https://bugs.webkit.org/show_bug.cgi?id=%d)" % (self.reason, self.bug) + return skipped_test_str + + def skip_entire_suite(self): + return self.test_case == SkippedTest.ENTIRE_SUITE + + def skip_for_build_type(self, build_type): + if self.build_type is None: + return True; + + return self.build_type == build_type + +class TestTimeout(Exception): + pass + +class TestRunner: + TEST_DIRS = [ "WebKit2Gtk", "WebKit2", "JavaScriptCore", "WTF", "WebCore" ] + + SKIPPED = [ + SkippedTest("WebKit2Gtk/TestUIClient", "/webkit2/WebKitWebView/mouse-target", "Test times out after r150890", 117689), + SkippedTest("WebKit2Gtk/TestUIClient", "/webkit2/WebKitWebView/usermedia-permission-requests", "Test times out", 158257), + SkippedTest("WebKit2Gtk/TestUIClient", "/webkit2/WebKitWebView/audio-usermedia-permission-request", "Test times out", 158257), + SkippedTest("WebKit2Gtk/TestCookieManager", "/webkit2/WebKitCookieManager/persistent-storage", "Test is flaky", 134580), + SkippedTest("WebKit2Gtk/TestPrinting", "/webkit2/WebKitPrintOperation/custom-widget", "Test is flaky", 168196), + SkippedTest("WebKit2Gtk/TestWebViewEditor", "/webkit2/WebKitWebView/editable/editable", "Test hits an assertion in Debug builds", 151654, "Debug"), + SkippedTest("WebKit2Gtk/TestWebExtensions", "/webkit2/WebKitWebExtension/form-controls-associated-signal", "Test is flaky", 168194), + SkippedTest("WebKit2Gtk/TestWebExtensions", "/webkit2/WebKitWebView/install-missing-plugins-permission-request", "Test times out", 147822), + SkippedTest("WebKit2/TestWebKit2", "WebKit2.MouseMoveAfterCrash", "Test is flaky", 85066), + SkippedTest("WebKit2/TestWebKit2", "WebKit2.NewFirstVisuallyNonEmptyLayoutForImages", "Test is flaky", 85066), + SkippedTest("WebKit2/TestWebKit2", "WebKit2.NewFirstVisuallyNonEmptyLayoutFrames", "Test fails", 85037), + SkippedTest("WebKit2/TestWebKit2", "WebKit2.SpacebarScrolling", "Test fails", 84961), + SkippedTest("WebKit2/TestWebKit2", "WebKit2.WKConnection", "Tests fail and time out out", 84959), + SkippedTest("WebKit2/TestWebKit2", "WebKit2.ForceRepaint", "Test times out", 105532), + SkippedTest("WebKit2/TestWebKit2", "WebKit2.ReloadPageAfterCrash", "Test flakily times out", 110129), + SkippedTest("WebKit2/TestWebKit2", "WebKit2.DidAssociateFormControls", "Test times out", 120302), + SkippedTest("WebKit2/TestWebKit2", "WebKit2.InjectedBundleFrameHitTest", "Test times out", 120303), + SkippedTest("WebKit2/TestWebKit2", "WebKit2.TerminateTwice", "Test causes crash on the next test", 121970), + SkippedTest("WebKit2/TestWebKit2", "WebKit2.GeolocationTransitionToHighAccuracy", "Test causes crash on the next test", 125068), + SkippedTest("WebKit2/TestWebKit2", "WebKit2.GeolocationTransitionToLowAccuracy", "Test causes crash on the next test", 125068), + ] + + SLOW = [ + "WTF_Lock.ContendedShortSection", + "WTF_Lock.ContendedLongSection", + "WTF_WordLock.ContendedShortSection", + "WTF_WordLock.ContendedLongSection", + "WebKit2Gtk/TestInspectorServer", + ] + + def __init__(self, options, tests=[]): + self._options = options + + self._build_type = "Debug" if self._options.debug else "Release" + common.set_build_types((self._build_type,)) + self._port = Host().port_factory.get("gtk") + self._driver = self._create_driver() + + self._programs_path = common.binary_build_path() + self._tests = self._get_tests(tests) + self._skipped_tests = [skipped for skipped in TestRunner.SKIPPED if skipped.skip_for_build_type(self._build_type)] + self._disabled_tests = [] + + # These SPI daemons need to be active for the accessibility tests to work. + self._spi_registryd = None + self._spi_bus_launcher = None + + def _test_programs_base_dir(self): + return os.path.join(self._programs_path, "TestWebKitAPI") + + def _get_tests_from_dir(self, test_dir): + if not os.path.isdir(test_dir): + return [] + + tests = [] + for test_file in os.listdir(test_dir): + if not test_file.lower().startswith("test"): + continue + test_path = os.path.join(test_dir, test_file) + if os.path.isfile(test_path) and os.access(test_path, os.X_OK): + tests.append(test_path) + return tests + + def _get_tests(self, initial_tests): + tests = [] + for test in initial_tests: + if os.path.isdir(test): + tests.extend(self._get_tests_from_dir(test)) + else: + tests.append(test) + if tests: + return tests + + tests = [] + for test_dir in self.TEST_DIRS: + absolute_test_dir = os.path.join(self._test_programs_base_dir(), test_dir) + tests.extend(self._get_tests_from_dir(absolute_test_dir)) + return tests + + def _lookup_atspi2_binary(self, filename): + exec_prefix = common.pkg_config_file_variable('atspi-2', 'exec_prefix') + if not exec_prefix: + return None + for path in ['libexec', 'lib/at-spi2-core', 'lib32/at-spi2-core', 'lib64/at-spi2-core']: + filepath = os.path.join(exec_prefix, path, filename) + if os.path.isfile(filepath): + return filepath + + return None + + def _wait_for_accessibility_bus(self): + def timeout_accessibility_bus(): + self._accessibility_bus_found = False + sys.stderr.write("Timeout waiting for the accesibility bus.\n") + sys.stderr.flush() + loop.quit() + # Backup current environment, and temporally set the test one. + oldenv = dict(os.environ) + os.environ.clear() + os.environ.update(self._test_env) + # We spin a main loop until the bus name appears on DBus. + self._accessibility_bus_found = True + loop = GLib.MainLoop() + Gio.bus_watch_name(Gio.BusType.SESSION, 'org.a11y.Bus', Gio.BusNameWatcherFlags.NONE, + lambda *args: loop.quit(), None) + GLib.timeout_add_seconds(5, timeout_accessibility_bus) + loop.run() + # Restore previous environment. + os.environ.clear() + os.environ.update(oldenv) + return self._accessibility_bus_found + + def _start_accessibility_daemons(self): + spi_bus_launcher_path = self._lookup_atspi2_binary('at-spi-bus-launcher') + spi_registryd_path = self._lookup_atspi2_binary('at-spi2-registryd') + if not spi_bus_launcher_path or not spi_registryd_path: + return False + + try: + self._spi_bus_launcher = subprocess.Popen([spi_bus_launcher_path], env=self._test_env) + except: + sys.stderr.write("Failed to launch the accessibility bus\n") + sys.stderr.flush() + return False + + # We need to wait until the SPI bus is launched before trying to start the SPI registry. + if not self._wait_for_accessibility_bus(): + sys.stderr.write("Failed checking the accessibility bus within D-Bus\n") + sys.stderr.flush() + return False + + try: + self._spi_registryd = subprocess.Popen([spi_registryd_path], env=self._test_env) + except: + sys.stderr.write("Failed to launch the accessibility registry\n") + sys.stderr.flush() + return False + + return True + + def _create_driver(self, port_options=[]): + self._port._display_server = self._options.display_server + driver = self._port.create_driver(worker_number=0, no_timeout=True)._make_driver(pixel_tests=False) + if not driver.check_driver(self._port): + raise RuntimeError("Failed to check driver %s" %driver.__class__.__name__) + return driver + + def _setup_testing_environment(self): + self._test_env = self._driver._setup_environ_for_test() + self._test_env["TEST_WEBKIT_API_WEBKIT2_RESOURCES_PATH"] = common.top_level_path("Tools", "TestWebKitAPI", "Tests", "WebKit2") + self._test_env["TEST_WEBKIT_API_WEBKIT2_INJECTED_BUNDLE_PATH"] = common.library_build_path() + self._test_env["WEBKIT_EXEC_PATH"] = self._programs_path + + # If we cannot start the accessibility daemons, we can just skip the accessibility tests. + if not self._start_accessibility_daemons(): + print "Could not start accessibility bus, so disabling TestWebKitAccessibility" + self._disabled_tests.append("WebKit2APITests/TestWebKitAccessibility") + return True + + def _tear_down_testing_environment(self): + if self._spi_registryd: + self._spi_registryd.terminate() + if self._spi_bus_launcher: + self._spi_bus_launcher.terminate() + if self._driver: + self._driver.stop() + + def _test_cases_to_skip(self, test_program): + if self._options.skipped_action != 'skip': + return [] + + test_cases = [] + for skipped in self._skipped_tests: + if test_program.endswith(skipped.test) and not skipped.skip_entire_suite(): + test_cases.append(skipped.test_case) + return test_cases + + def _should_run_test_program(self, test_program): + for disabled_test in self._disabled_tests: + if test_program.endswith(disabled_test): + return False + + if self._options.skipped_action != 'skip': + return True + + for skipped in self._skipped_tests: + if test_program.endswith(skipped.test) and skipped.skip_entire_suite(): + return False + return True + + def _get_child_pid_from_test_output(self, output): + if not output: + return -1 + match = re.search(r'\(pid=(?P<child_pid>[0-9]+)\)', output) + if not match: + return -1 + return int(match.group('child_pid')) + + def _kill_process(self, pid): + try: + os.kill(pid, SIGKILL) + except OSError: + # Process already died. + pass + + def _run_test_command(self, command, timeout=-1): + def alarm_handler(signum, frame): + raise TestTimeout + + child_pid = [-1] + def parse_line(line, child_pid = child_pid): + if child_pid[0] == -1: + child_pid[0] = self._get_child_pid_from_test_output(line) + + sys.stdout.write(line) + + def waitpid(pid): + while True: + try: + return os.waitpid(pid, 0) + except (OSError, IOError) as e: + if e.errno == errno.EINTR: + continue + raise + + def return_code_from_exit_status(status): + if os.WIFSIGNALED(status): + return -os.WTERMSIG(status) + elif os.WIFEXITED(status): + return os.WEXITSTATUS(status) + else: + # Should never happen + raise RuntimeError("Unknown child exit status!") + + pid, fd = os.forkpty() + if pid == 0: + os.execvpe(command[0], command, self._test_env) + sys.exit(0) + + if timeout > 0: + signal(SIGALRM, alarm_handler) + alarm(timeout) + + try: + common.parse_output_lines(fd, parse_line) + if timeout > 0: + alarm(0) + except TestTimeout: + self._kill_process(pid) + if child_pid[0] > 0: + self._kill_process(child_pid[0]) + raise + + try: + dummy, status = waitpid(pid) + except OSError as e: + if e.errno != errno.ECHILD: + raise + # This happens if SIGCLD is set to be ignored or waiting + # for child processes has otherwise been disabled for our + # process. This child is dead, we can't get the status. + status = 0 + + return return_code_from_exit_status(status) + + def _run_test_glib(self, test_program): + tester_command = ['gtester', '-k'] + if self._options.verbose: + tester_command.append('--verbose') + for test_case in self._test_cases_to_skip(test_program): + tester_command.extend(['-s', test_case]) + tester_command.append(test_program) + # This timeout is supposed to be per test case, but in the case of GLib tests it affects all the tests cases of + # the same test program. Some test programs like TestLoaderClient, that have a lot of test cases, often time out + # in the bots because the timeout is not enough to run all the tests cases. So, we use a longer timeout for GLib + # tests (timeout * 2). + timeout = self._options.timeout * 2 + test = os.path.join(os.path.basename(os.path.dirname(test_program)), os.path.basename(test_program)) + if test in TestRunner.SLOW: + timeout *= 5 + + return self._run_test_command(tester_command, timeout) + + def _get_tests_from_google_test_suite(self, test_program): + try: + output = subprocess.check_output([test_program, '--gtest_list_tests'], env=self._test_env) + except subprocess.CalledProcessError: + sys.stderr.write("ERROR: could not list available tests for binary %s.\n" % (test_program)) + sys.stderr.flush() + return 1 + + skipped_test_cases = self._test_cases_to_skip(test_program) + + tests = [] + prefix = None + for line in output.split('\n'): + if not line.startswith(' '): + prefix = line + continue + else: + test_name = prefix + line.strip() + if not test_name in skipped_test_cases: + tests.append(test_name) + return tests + + def _run_google_test(self, test_program, subtest): + test_command = [test_program, '--gtest_filter=%s' % (subtest)] + timeout = self._options.timeout + if subtest in TestRunner.SLOW: + timeout *= 5 + + status = self._run_test_command(test_command, timeout) + if status == -SIGSEGV: + sys.stdout.write("**CRASH** %s\n" % subtest) + sys.stdout.flush() + return status + + def _run_google_test_suite(self, test_program): + retcode = 0 + for subtest in self._get_tests_from_google_test_suite(test_program): + if self._run_google_test(test_program, subtest): + retcode = 1 + return retcode + + def _run_test(self, test_program): + basedir = os.path.basename(os.path.dirname(test_program)) + if basedir in ["WebKit2Gtk", "WebKitGtk"]: + return self._run_test_glib(test_program) + + if basedir in ["WebKit2", "JavaScriptCore", "WTF", "WebCore", "WebCoreGtk"]: + return self._run_google_test_suite(test_program) + + return 1 + + def run_tests(self): + if not self._tests: + sys.stderr.write("ERROR: tests not found in %s.\n" % (self._test_programs_base_dir())) + sys.stderr.flush() + return 1 + + if not self._setup_testing_environment(): + return 1 + + # Remove skipped tests now instead of when we find them, because + # some tests might be skipped while setting up the test environment. + self._tests = [test for test in self._tests if self._should_run_test_program(test)] + + crashed_tests = [] + failed_tests = [] + timed_out_tests = [] + try: + for test in self._tests: + exit_status_code = 0 + try: + exit_status_code = self._run_test(test) + except TestTimeout: + sys.stdout.write("TEST: %s: TIMEOUT\n" % test) + sys.stdout.flush() + timed_out_tests.append(test) + + if exit_status_code == -SIGSEGV: + sys.stdout.write("TEST: %s: CRASHED\n" % test) + sys.stdout.flush() + crashed_tests.append(test) + elif exit_status_code != 0: + failed_tests.append(test) + finally: + self._tear_down_testing_environment() + + if failed_tests: + names = [test.replace(self._test_programs_base_dir(), '', 1) for test in failed_tests] + sys.stdout.write("Tests failed (%d): %s\n" % (len(names), ", ".join(names))) + sys.stdout.flush() + + if crashed_tests: + names = [test.replace(self._test_programs_base_dir(), '', 1) for test in crashed_tests] + sys.stdout.write("Tests that crashed (%d): %s\n" % (len(names), ", ".join(names))) + sys.stdout.flush() + + if timed_out_tests: + names = [test.replace(self._test_programs_base_dir(), '', 1) for test in timed_out_tests] + sys.stdout.write("Tests that timed out (%d): %s\n" % (len(names), ", ".join(names))) + sys.stdout.flush() + + if self._skipped_tests and self._options.skipped_action == 'skip': + sys.stdout.write("Tests skipped (%d):\n%s\n" % + (len(self._skipped_tests), + "\n".join([str(skipped) for skipped in self._skipped_tests]))) + sys.stdout.flush() + + return len(failed_tests) + len(timed_out_tests) + +if __name__ == "__main__": + if not jhbuildutils.enter_jhbuild_environment_if_available("gtk"): + print "***" + print "*** Warning: jhbuild environment not present. Run update-webkitgtk-libs before build-webkit to ensure proper testing." + print "***" + + option_parser = optparse.OptionParser(usage='usage: %prog [options] [test...]') + option_parser.add_option('-r', '--release', + action='store_true', dest='release', + help='Run in Release') + option_parser.add_option('-d', '--debug', + action='store_true', dest='debug', + help='Run in Debug') + option_parser.add_option('-v', '--verbose', + action='store_true', dest='verbose', + help='Run gtester in verbose mode') + option_parser.add_option('--skipped', action='store', dest='skipped_action', + choices=['skip', 'ignore', 'only'], default='skip', + metavar='skip|ignore|only', + help='Specifies how to treat the skipped tests') + option_parser.add_option('-t', '--timeout', + action='store', type='int', dest='timeout', default=10, + help='Time in seconds until a test times out') + option_parser.add_option('--display-server', choices=['xvfb', 'xorg', 'weston', 'wayland'], default='xvfb', + help='"xvfb": Use a virtualized X11 server. "xorg": Use the current X11 session. ' + '"weston": Use a virtualized Weston server. "wayland": Use the current wayland session.'), + options, args = option_parser.parse_args() + + logging.basicConfig(level=logging.INFO, format="%(message)s") + + runner = TestRunner(options, args) + sys.exit(runner.run_tests()) diff --git a/Tools/Scripts/webkit-build-directory b/Tools/Scripts/webkit-build-directory new file mode 100755 index 000000000..5bac3708a --- /dev/null +++ b/Tools/Scripts/webkit-build-directory @@ -0,0 +1,84 @@ +#!/usr/bin/perl -w + +# Copyright (C) 2010 Google Inc. All rights reserved. +# Copyright (C) 2013, 2015 Apple Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of Apple Inc. ("Apple") nor the names of +# its contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# A script to expose WebKit's build directory detection logic to non-perl scripts. + +use FindBin; +use Getopt::Long; + +use lib $FindBin::Bin; +use webkitdirs; + +my $showConfigurationDirectory = 0; +my $showExecutablePath = 0; +my $showHelp = 0; +my $showTopLevelDirectory = 0; + + +my $programName = basename($0); +my $usage = <<EOF; +Usage: $programName [options] + --configuration Show the build directory for a specific configuration (e.g. Debug, Release. Defaults to the active configuration set by set-webkit-configuration) + --executablePath Show the path to the executables produced by a specific build configuration. This differs from --configuration on Windows. + -h|--help Show this help message + --top-level Show the top-level build directory + + --efl Find the build directory for the EFL port + --gtk Find the build directory for the GTK+ port + --wincairo Find the build directory for using Cairo (rather than CoreGraphics) on Windows + +Either --configuration or --top-level is required. +EOF + +setConfiguration(); # Figure out from the command line if we're --debug or --release or the default. + +# FIXME: Check if extra flags are valid or not. +Getopt::Long::Configure('pass_through'); # Let --blackberry, etc... be handled by webkitdirs +my $getOptionsResult = GetOptions( + 'configuration' => \$showConfigurationDirectory, + 'executablePath' => \$showExecutablePath, + 'top-level' => \$showTopLevelDirectory, + 'help|h' => \$showHelp, +); + +if (!$getOptionsResult || $showHelp) { + print STDERR $usage; + exit 1; +} + +if (!$showConfigurationDirectory && !$showTopLevelDirectory && !$showExecutablePath) { + print baseProductDir() . "\n"; + print productDir() . "\n"; +} elsif ($showTopLevelDirectory) { + print baseProductDir() . "\n"; +} elsif ($showExecutablePath) { + print executableProductDir() . "\n"; +} else { + print productDir() . "\n"; +} diff --git a/Tools/Scripts/webkitdirs.pm b/Tools/Scripts/webkitdirs.pm new file mode 100755 index 000000000..030cd9ff2 --- /dev/null +++ b/Tools/Scripts/webkitdirs.pm @@ -0,0 +1,2613 @@ +# Copyright (C) 2005-2007, 2010-2016 Apple Inc. All rights reserved. +# Copyright (C) 2009 Google Inc. All rights reserved. +# Copyright (C) 2011 Research In Motion Limited. All rights reserved. +# Copyright (C) 2013 Nokia Corporation and/or its subsidiary(-ies). +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of Apple Inc. ("Apple") nor the names of +# its contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# Module to share code to get to WebKit directories. + +use strict; +use version; +use warnings; +use Config; +use Cwd qw(realpath); +use Digest::MD5 qw(md5_hex); +use FindBin; +use File::Basename; +use File::Find; +use File::Path qw(make_path mkpath rmtree); +use File::Spec; +use File::stat; +use List::Util; +use POSIX; +use Time::HiRes qw(usleep); +use VCSUtils; + +BEGIN { + use Exporter (); + our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS); + $VERSION = 1.00; + @ISA = qw(Exporter); + @EXPORT = qw( + &XcodeCoverageSupportOptions + &XcodeOptionString + &XcodeOptionStringNoConfig + &XcodeOptions + &XcodeStaticAnalyzerOption + &appDisplayNameFromBundle + &appendToEnvironmentVariableList + &archCommandLineArgumentsForRestrictedEnvironmentVariables + &baseProductDir + &chdirWebKit + &checkFrameworks + &cmakeBasedPortArguments + ¤tSVNRevision + &debugSafari + &executableProductDir + &findOrCreateSimulatorForIOSDevice + &iosSimulatorDeviceByName + &nmPath + &passedConfiguration + &prependToEnvironmentVariableList + &printHelpAndExitForRunAndDebugWebKitAppIfNeeded + &productDir + &quitIOSSimulator + &relaunchIOSSimulator + &restartIOSSimulatorDevice + &runIOSWebKitApp + &runMacWebKitApp + &safariPath + &iosVersion + &setConfiguration + &setupMacWebKitEnvironment + &sharedCommandLineOptions + &sharedCommandLineOptionsUsage + &shutDownIOSSimulatorDevice + &willUseIOSDeviceSDK + &willUseIOSSimulatorSDK + SIMULATOR_DEVICE_SUFFIX_FOR_WEBKIT_DEVELOPMENT + USE_OPEN_COMMAND + ); + %EXPORT_TAGS = ( ); + @EXPORT_OK = (); +} + +# Ports +use constant { + AppleWin => "AppleWin", + GTK => "GTK", + Efl => "Efl", + iOS => "iOS", + Mac => "Mac", + JSCOnly => "JSCOnly", + WinCairo => "WinCairo", + Unknown => "Unknown" +}; + +use constant USE_OPEN_COMMAND => 1; # Used in runMacWebKitApp(). +use constant SIMULATOR_DEVICE_STATE_SHUTDOWN => "1"; +use constant SIMULATOR_DEVICE_STATE_BOOTED => "3"; +use constant SIMULATOR_DEVICE_SUFFIX_FOR_WEBKIT_DEVELOPMENT => "For WebKit Development"; + +# See table "Certificate types and names" on <https://developer.apple.com/library/ios/documentation/IDEs/Conceptual/AppDistributionGuide/MaintainingCertificates/MaintainingCertificates.html#//apple_ref/doc/uid/TP40012582-CH31-SW41>. +use constant IOS_DEVELOPMENT_CERTIFICATE_NAME_PREFIX => "iPhone Developer: "; + +our @EXPORT_OK; + +my $architecture; +my $asanIsEnabled; +my $numberOfCPUs; +my $maxCPULoad; +my $baseProductDir; +my @baseProductDirOption; +my $configuration; +my $xcodeSDK; +my $configurationForVisualStudio; +my $configurationProductDir; +my $sourceDir; +my $currentSVNRevision; +my $didLoadIPhoneSimulatorNotification; +my $nmPath; +my $osXVersion; +my $iosVersion; +my $generateDsym; +my $isCMakeBuild; +my $isWin64; +my $isInspectorFrontend; +my $portName; +my $shouldUseGuardMalloc; +my $shouldNotUseNinja; +my $xcodeVersion; + +my $unknownPortProhibited = 0; + +# Variables for Win32 support +my $programFilesPath; +my $vcBuildPath; +my $vsInstallDir; +my $msBuildInstallDir; +my $vsVersion; +my $windowsSourceDir; +my $winVersion; +my $willUseVCExpressWhenBuilding = 0; + +# Defined in VCSUtils. +sub exitStatus($); + +sub findMatchingArguments($$); +sub hasArgument($$); + +sub determineSourceDir +{ + return if $sourceDir; + $sourceDir = $FindBin::Bin; + $sourceDir =~ s|/+$||; # Remove trailing '/' as we would die later + + # walks up path checking each directory to see if it is the main WebKit project dir, + # defined by containing Sources, WebCore, and JavaScriptCore. + until ((-d File::Spec->catdir($sourceDir, "Source") && -d File::Spec->catdir($sourceDir, "Source", "WebCore") && -d File::Spec->catdir($sourceDir, "Source", "JavaScriptCore")) || (-d File::Spec->catdir($sourceDir, "Internal") && -d File::Spec->catdir($sourceDir, "OpenSource"))) + { + if ($sourceDir !~ s|/[^/]+$||) { + die "Could not find top level webkit directory above source directory using FindBin.\n"; + } + } + + $sourceDir = File::Spec->catdir($sourceDir, "OpenSource") if -d File::Spec->catdir($sourceDir, "OpenSource"); +} + +sub currentPerlPath() +{ + my $thisPerl = $^X; + if ($^O ne 'VMS') { + $thisPerl .= $Config{_exe} unless $thisPerl =~ m/$Config{_exe}$/i; + } + return $thisPerl; +} + +# used for scripts which are stored in a non-standard location +sub setSourceDir($) +{ + ($sourceDir) = @_; +} + +sub determineNinjaVersion +{ + chomp(my $ninjaVersion = `ninja --version`); + return $ninjaVersion; +} + +sub determineXcodeVersion +{ + return if defined $xcodeVersion; + my $xcodebuildVersionOutput = `xcodebuild -version`; + $xcodeVersion = ($xcodebuildVersionOutput =~ /Xcode ([0-9](\.[0-9]+)*)/) ? $1 : "3.0"; +} + +sub readXcodeUserDefault($) +{ + my ($key) = @_; + + my $devnull = File::Spec->devnull(); + + my $value = `defaults read com.apple.dt.Xcode ${key} 2> ${devnull}`; + return if $?; + + chomp $value; + return $value; +} + +sub determineBaseProductDir +{ + return if defined $baseProductDir; + determineSourceDir(); + + my $setSharedPrecompsDir; + $baseProductDir = $ENV{"WEBKIT_OUTPUTDIR"}; + + if (!defined($baseProductDir) and isAppleCocoaWebKit()) { + # Silently remove ~/Library/Preferences/xcodebuild.plist which can + # cause build failure. The presence of + # ~/Library/Preferences/xcodebuild.plist can prevent xcodebuild from + # respecting global settings such as a custom build products directory + # (<rdar://problem/5585899>). + my $personalPlistFile = $ENV{HOME} . "/Library/Preferences/xcodebuild.plist"; + if (-e $personalPlistFile) { + unlink($personalPlistFile) || die "Could not delete $personalPlistFile: $!"; + } + + my $buildLocationStyle = join '', readXcodeUserDefault("IDEBuildLocationStyle"); + if ($buildLocationStyle eq "Custom") { + my $buildLocationType = join '', readXcodeUserDefault("IDECustomBuildLocationType"); + # FIXME: Read CustomBuildIntermediatesPath and set OBJROOT accordingly. + $baseProductDir = readXcodeUserDefault("IDECustomBuildProductsPath") if $buildLocationType eq "Absolute"; + } + + # DeterminedByTargets corresponds to a setting of "Legacy" in Xcode. + # It is the only build location style for which SHARED_PRECOMPS_DIR is not + # overridden when building from within Xcode. + $setSharedPrecompsDir = 1 if $buildLocationStyle ne "DeterminedByTargets"; + + if (!defined($baseProductDir)) { + $baseProductDir = join '', readXcodeUserDefault("IDEApplicationwideBuildSettings"); + $baseProductDir = $1 if $baseProductDir =~ /SYMROOT\s*=\s*\"(.*?)\";/s; + } + + undef $baseProductDir unless $baseProductDir =~ /^\//; + } + + if (!defined($baseProductDir)) { # Port-specific checks failed, use default + $baseProductDir = File::Spec->catdir($sourceDir, "WebKitBuild"); + } + + if (isGit() && isGitBranchBuild()) { + my $branch = gitBranch(); + $baseProductDir = "$baseProductDir/$branch"; + } + + if (isAppleCocoaWebKit()) { + $baseProductDir =~ s|^\Q$(SRCROOT)/..\E$|$sourceDir|; + $baseProductDir =~ s|^\Q$(SRCROOT)/../|$sourceDir/|; + $baseProductDir =~ s|^~/|$ENV{HOME}/|; + die "Can't handle Xcode product directory with a ~ in it.\n" if $baseProductDir =~ /~/; + die "Can't handle Xcode product directory with a variable in it.\n" if $baseProductDir =~ /\$/; + @baseProductDirOption = ("SYMROOT=$baseProductDir", "OBJROOT=$baseProductDir"); + push(@baseProductDirOption, "SHARED_PRECOMPS_DIR=${baseProductDir}/PrecompiledHeaders") if $setSharedPrecompsDir; + } + + if (isCygwin()) { + my $dosBuildPath = `cygpath --windows \"$baseProductDir\"`; + chomp $dosBuildPath; + $ENV{"WEBKIT_OUTPUTDIR"} = $dosBuildPath; + my $unixBuildPath = `cygpath --unix \"$baseProductDir\"`; + chomp $unixBuildPath; + $baseProductDir = $dosBuildPath; + } +} + +sub systemVerbose { + print "+ @_\n"; + return system(@_); +} + +sub setBaseProductDir($) +{ + ($baseProductDir) = @_; +} + +sub determineConfiguration +{ + return if defined $configuration; + determineBaseProductDir(); + if (open CONFIGURATION, "$baseProductDir/Configuration") { + $configuration = <CONFIGURATION>; + close CONFIGURATION; + } + if ($configuration) { + chomp $configuration; + # compatibility for people who have old Configuration files + $configuration = "Release" if $configuration eq "Deployment"; + $configuration = "Debug" if $configuration eq "Development"; + } else { + $configuration = "Release"; + } +} + +sub determineArchitecture +{ + return if defined $architecture; + # make sure $architecture is defined in all cases + $architecture = ""; + + determineBaseProductDir(); + determineXcodeSDK(); + + if (isAppleCocoaWebKit()) { + if (open ARCHITECTURE, "$baseProductDir/Architecture") { + $architecture = <ARCHITECTURE>; + close ARCHITECTURE; + } + if ($architecture) { + chomp $architecture; + } else { + if (not defined $xcodeSDK or $xcodeSDK =~ /^(\/$|macosx)/) { + my $supports64Bit = `sysctl -n hw.optional.x86_64`; + chomp $supports64Bit; + $architecture = 'x86_64' if $supports64Bit; + } elsif ($xcodeSDK =~ /^iphonesimulator/) { + $architecture = 'x86_64'; + } elsif ($xcodeSDK =~ /^iphoneos/) { + $architecture = 'arm64'; + } + } + } elsif (isCMakeBuild()) { + if (open my $cmake_sysinfo, "cmake --system-information |") { + while (<$cmake_sysinfo>) { + next unless index($_, 'CMAKE_SYSTEM_PROCESSOR') == 0; + if (/^CMAKE_SYSTEM_PROCESSOR \"([^"]+)\"/) { + $architecture = $1; + $architecture = 'x86_64' if $architecture eq 'amd64'; + last; + } + } + close $cmake_sysinfo; + } + } + + if (!isAnyWindows()) { + if (!$architecture) { + # Fall back to output of `arch', if it is present. + $architecture = `arch`; + chomp $architecture; + } + + if (!$architecture) { + # Fall back to output of `uname -m', if it is present. + $architecture = `uname -m`; + chomp $architecture; + } + } + + $architecture = 'x86_64' if ($architecture =~ /amd64/ && isBSD()); +} + +sub determineASanIsEnabled +{ + return if defined $asanIsEnabled; + determineBaseProductDir(); + + $asanIsEnabled = 0; + my $asanConfigurationValue; + + if (open ASAN, "$baseProductDir/ASan") { + $asanConfigurationValue = <ASAN>; + close ASAN; + chomp $asanConfigurationValue; + $asanIsEnabled = 1 if $asanConfigurationValue eq "YES"; + } +} + +sub determineNumberOfCPUs +{ + return if defined $numberOfCPUs; + if (defined($ENV{NUMBER_OF_PROCESSORS})) { + $numberOfCPUs = $ENV{NUMBER_OF_PROCESSORS}; + } elsif (isLinux()) { + # First try the nproc utility, if it exists. If we get no + # results fall back to just interpretting /proc directly. + chomp($numberOfCPUs = `nproc --all 2> /dev/null`); + if ($numberOfCPUs eq "") { + $numberOfCPUs = (grep /processor/, `cat /proc/cpuinfo`); + } + } elsif (isAnyWindows()) { + # Assumes cygwin + $numberOfCPUs = `ls /proc/registry/HKEY_LOCAL_MACHINE/HARDWARE/DESCRIPTION/System/CentralProcessor | wc -w`; + } elsif (isDarwin() || isBSD()) { + chomp($numberOfCPUs = `sysctl -n hw.ncpu`); + } +} + +sub determineMaxCPULoad +{ + return if defined $maxCPULoad; + if (defined($ENV{MAX_CPU_LOAD})) { + $maxCPULoad = $ENV{MAX_CPU_LOAD}; + } +} + +sub jscPath($) +{ + my ($productDir) = @_; + my $jscName = "jsc"; + $jscName .= "_debug" if configuration() eq "Debug_All"; + $jscName .= ".exe" if (isAnyWindows()); + return "$productDir/$jscName" if -e "$productDir/$jscName"; + return "$productDir/JavaScriptCore.framework/Resources/$jscName"; +} + +sub argumentsForConfiguration() +{ + determineConfiguration(); + determineArchitecture(); + determineXcodeSDK(); + + my @args = (); + # FIXME: Is it necessary to pass --debug, --release, --32-bit or --64-bit? + # These are determined automatically from stored configuration. + push(@args, '--debug') if ($configuration =~ "^Debug"); + push(@args, '--release') if ($configuration =~ "^Release"); + push(@args, '--device') if (defined $xcodeSDK && $xcodeSDK =~ /^iphoneos/); + push(@args, '--ios-simulator') if (defined $xcodeSDK && $xcodeSDK =~ /^iphonesimulator/); + push(@args, '--32-bit') if ($architecture ne "x86_64" and !isWin64()); + push(@args, '--64-bit') if (isWin64()); + push(@args, '--gtk') if isGtk(); + push(@args, '--efl') if isEfl(); + push(@args, '--jsc-only') if isJSCOnly(); + push(@args, '--wincairo') if isWinCairo(); + push(@args, '--inspector-frontend') if isInspectorFrontend(); + return @args; +} + +sub determineXcodeSDK +{ + return if defined $xcodeSDK; + my $sdk; + if (checkForArgumentAndRemoveFromARGVGettingValue("--sdk", \$sdk)) { + $xcodeSDK = $sdk; + } + if (checkForArgumentAndRemoveFromARGV("--device")) { + my $hasInternalSDK = exitStatus(system("xcrun --sdk iphoneos.internal --show-sdk-version > /dev/null 2>&1")) == 0; + $xcodeSDK ||= $hasInternalSDK ? "iphoneos.internal" : "iphoneos"; + } + if (checkForArgumentAndRemoveFromARGV("--ios-simulator")) { + $xcodeSDK ||= 'iphonesimulator'; + } +} + +sub xcodeSDK +{ + determineXcodeSDK(); + return $xcodeSDK; +} + +sub setXcodeSDK($) +{ + ($xcodeSDK) = @_; +} + + +sub xcodeSDKPlatformName() +{ + determineXcodeSDK(); + return "" if !defined $xcodeSDK; + return "iphoneos" if $xcodeSDK =~ /iphoneos/i; + return "iphonesimulator" if $xcodeSDK =~ /iphonesimulator/i; + return "macosx" if $xcodeSDK =~ /macosx/i; + die "Couldn't determine platform name from Xcode SDK"; +} + +sub XcodeSDKPath +{ + determineXcodeSDK(); + + die "Can't find the SDK path because no Xcode SDK was specified" if not $xcodeSDK; + + my $sdkPath = `xcrun --sdk $xcodeSDK --show-sdk-path` if $xcodeSDK; + die 'Failed to get SDK path from xcrun' if $?; + chomp $sdkPath; + + return $sdkPath; +} + +sub xcodeSDKVersion +{ + determineXcodeSDK(); + + die "Can't find the SDK version because no Xcode SDK was specified" if !$xcodeSDK; + + chomp(my $sdkVersion = `xcrun --sdk $xcodeSDK --show-sdk-version`); + die "Failed to get SDK version from xcrun" if exitStatus($?); + + return $sdkVersion; +} + +sub programFilesPath +{ + return $programFilesPath if defined $programFilesPath; + + $programFilesPath = $ENV{'PROGRAMFILES(X86)'} || $ENV{'PROGRAMFILES'} || "C:\\Program Files"; + + return $programFilesPath; +} + +sub visualStudioInstallDir +{ + return $vsInstallDir if defined $vsInstallDir; + + if ($ENV{'VSINSTALLDIR'}) { + $vsInstallDir = $ENV{'VSINSTALLDIR'}; + $vsInstallDir =~ s|[\\/]$||; + } else { + $vsInstallDir = File::Spec->catdir(programFilesPath(), "Microsoft Visual Studio 14.0"); + } + chomp($vsInstallDir = `cygpath "$vsInstallDir"`) if isCygwin(); + + print "Using Visual Studio: $vsInstallDir\n"; + return $vsInstallDir; +} + +sub msBuildInstallDir +{ + return $msBuildInstallDir if defined $msBuildInstallDir; + + $msBuildInstallDir = File::Spec->catdir(programFilesPath(), "MSBuild", "14.0", "Bin"); + + chomp($msBuildInstallDir = `cygpath "$msBuildInstallDir"`) if isCygwin(); + + print "Using MSBuild: $msBuildInstallDir\n"; + return $msBuildInstallDir; +} + +sub visualStudioVersion +{ + return $vsVersion if defined $vsVersion; + + my $installDir = visualStudioInstallDir(); + + $vsVersion = ($installDir =~ /Microsoft Visual Studio ([0-9]+\.[0-9]*)/) ? $1 : "14"; + + print "Using Visual Studio $vsVersion\n"; + return $vsVersion; +} + +sub determineConfigurationForVisualStudio +{ + return if defined $configurationForVisualStudio; + determineConfiguration(); + # FIXME: We should detect when Debug_All or Production has been chosen. + $configurationForVisualStudio = "/p:Configuration=" . $configuration; +} + +sub usesPerConfigurationBuildDirectory +{ + # [Gtk] We don't have Release/Debug configurations in straight + # autotool builds (non build-webkit). In this case and if + # WEBKIT_OUTPUTDIR exist, use that as our configuration dir. This will + # allows us to run run-webkit-tests without using build-webkit. + return ($ENV{"WEBKIT_OUTPUTDIR"} && isGtk()) || isAppleWinWebKit(); +} + +sub determineConfigurationProductDir +{ + return if defined $configurationProductDir; + determineBaseProductDir(); + determineConfiguration(); + if (isAppleWinWebKit() || isWinCairo()) { + $configurationProductDir = File::Spec->catdir($baseProductDir, $configuration); + } else { + if (usesPerConfigurationBuildDirectory()) { + $configurationProductDir = "$baseProductDir"; + } else { + $configurationProductDir = "$baseProductDir/$configuration"; + $configurationProductDir .= "-" . xcodeSDKPlatformName() if isIOSWebKit(); + } + } +} + +sub setConfigurationProductDir($) +{ + ($configurationProductDir) = @_; +} + +sub determineCurrentSVNRevision +{ + # We always update the current SVN revision here, and leave the caching + # to currentSVNRevision(), so that changes to the SVN revision while the + # script is running can be picked up by calling this function again. + determineSourceDir(); + $currentSVNRevision = svnRevisionForDirectory($sourceDir); + return $currentSVNRevision; +} + + +sub chdirWebKit +{ + determineSourceDir(); + chdir $sourceDir or die; +} + +sub baseProductDir +{ + determineBaseProductDir(); + return $baseProductDir; +} + +sub sourceDir +{ + determineSourceDir(); + return $sourceDir; +} + +sub productDir +{ + determineConfigurationProductDir(); + return $configurationProductDir; +} + +sub executableProductDir +{ + my $productDirectory = productDir(); + + my $binaryDirectory; + if (isEfl() || isGtk() || isJSCOnly()) { + $binaryDirectory = "bin"; + } elsif (isAnyWindows()) { + $binaryDirectory = isWin64() ? "bin64" : "bin32"; + } else { + return $productDirectory; + } + + return File::Spec->catdir($productDirectory, $binaryDirectory); +} + +sub jscProductDir +{ + return executableProductDir(); +} + +sub configuration() +{ + determineConfiguration(); + return $configuration; +} + +sub asanIsEnabled() +{ + determineASanIsEnabled(); + return $asanIsEnabled; +} + +sub configurationForVisualStudio() +{ + determineConfigurationForVisualStudio(); + return $configurationForVisualStudio; +} + +sub currentSVNRevision +{ + determineCurrentSVNRevision() if not defined $currentSVNRevision; + return $currentSVNRevision; +} + +sub generateDsym() +{ + determineGenerateDsym(); + return $generateDsym; +} + +sub determineGenerateDsym() +{ + return if defined($generateDsym); + $generateDsym = checkForArgumentAndRemoveFromARGV("--dsym"); +} + +sub hasIOSDevelopmentCertificate() +{ + return !exitStatus(system("security find-identity -p codesigning | grep '" . IOS_DEVELOPMENT_CERTIFICATE_NAME_PREFIX . "' > /dev/null 2>&1")); +} + +sub argumentsForXcode() +{ + my @args = (); + push @args, "DEBUG_INFORMATION_FORMAT=dwarf-with-dsym" if generateDsym(); + return @args; +} + +sub XcodeOptions +{ + determineBaseProductDir(); + determineConfiguration(); + determineArchitecture(); + determineASanIsEnabled(); + determineXcodeSDK(); + + my @options; + push @options, "-UseSanitizedBuildSystemEnvironment=YES"; + push @options, ("-configuration", $configuration); + push @options, ("-xcconfig", sourceDir() . "/Tools/asan/asan.xcconfig", "ASAN_IGNORE=" . sourceDir() . "/Tools/asan/webkit-asan-ignore.txt") if $asanIsEnabled; + push @options, @baseProductDirOption; + push @options, "ARCHS=$architecture" if $architecture; + push @options, "SDKROOT=$xcodeSDK" if $xcodeSDK; + if (willUseIOSDeviceSDK()) { + push @options, "ENABLE_BITCODE=NO"; + if (hasIOSDevelopmentCertificate()) { + # FIXME: May match more than one installed development certificate. + push @options, "CODE_SIGN_IDENTITY=" . IOS_DEVELOPMENT_CERTIFICATE_NAME_PREFIX; + } else { + push @options, "CODE_SIGN_IDENTITY="; # No identity + push @options, "CODE_SIGNING_REQUIRED=NO"; + } + } + push @options, argumentsForXcode(); + return @options; +} + +sub XcodeOptionString +{ + return join " ", XcodeOptions(); +} + +sub XcodeOptionStringNoConfig +{ + return join " ", @baseProductDirOption; +} + +sub XcodeCoverageSupportOptions() +{ + my @coverageSupportOptions = (); + push @coverageSupportOptions, "GCC_GENERATE_TEST_COVERAGE_FILES=YES"; + push @coverageSupportOptions, "GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES"; + return @coverageSupportOptions; +} + +sub XcodeStaticAnalyzerOption() +{ + return "RUN_CLANG_STATIC_ANALYZER=YES"; +} + +my $passedConfiguration; +my $searchedForPassedConfiguration; +sub determinePassedConfiguration +{ + return if $searchedForPassedConfiguration; + $searchedForPassedConfiguration = 1; + $passedConfiguration = undef; + + if (checkForArgumentAndRemoveFromARGV("--debug")) { + $passedConfiguration = "Debug"; + } elsif(checkForArgumentAndRemoveFromARGV("--release")) { + $passedConfiguration = "Release"; + } elsif (checkForArgumentAndRemoveFromARGV("--profile") || checkForArgumentAndRemoveFromARGV("--profiling")) { + $passedConfiguration = "Profiling"; + } +} + +sub passedConfiguration +{ + determinePassedConfiguration(); + return $passedConfiguration; +} + +sub setConfiguration +{ + setArchitecture(); + + if (my $config = shift @_) { + $configuration = $config; + return; + } + + determinePassedConfiguration(); + $configuration = $passedConfiguration if $passedConfiguration; +} + + +my $passedArchitecture; +my $searchedForPassedArchitecture; +sub determinePassedArchitecture +{ + return if $searchedForPassedArchitecture; + $searchedForPassedArchitecture = 1; + + $passedArchitecture = undef; + if (checkForArgumentAndRemoveFromARGV("--32-bit")) { + if (isAppleCocoaWebKit()) { + # PLATFORM_IOS: Don't run `arch` command inside Simulator environment + local %ENV = %ENV; + delete $ENV{DYLD_ROOT_PATH}; + delete $ENV{DYLD_FRAMEWORK_PATH}; + + $passedArchitecture = `arch`; + chomp $passedArchitecture; + } + } +} + +sub passedArchitecture +{ + determinePassedArchitecture(); + return $passedArchitecture; +} + +sub architecture() +{ + determineArchitecture(); + return $architecture; +} + +sub numberOfCPUs() +{ + determineNumberOfCPUs(); + return $numberOfCPUs; +} + +sub maxCPULoad() +{ + determineMaxCPULoad(); + return $maxCPULoad; +} + +sub setArchitecture +{ + if (my $arch = shift @_) { + $architecture = $arch; + return; + } + + determinePassedArchitecture(); + $architecture = $passedArchitecture if $passedArchitecture; +} + +# Locate Safari. +sub safariPath +{ + die "Safari path is only relevant on Apple Mac platform\n" unless isAppleCocoaWebKit(); + + my $safariPath; + + # Use WEBKIT_SAFARI environment variable if present. + my $safariBundle = $ENV{WEBKIT_SAFARI}; + if (!$safariBundle) { + determineConfigurationProductDir(); + # Use Safari.app in product directory if present (good for Safari development team). + if (-d "$configurationProductDir/Safari.app") { + $safariBundle = "$configurationProductDir/Safari.app"; + } + } + + if ($safariBundle) { + $safariPath = "$safariBundle/Contents/MacOS/Safari"; + } else { + $safariPath = "/Applications/Safari.app/Contents/MacOS/SafariForWebKitDevelopment"; + } + + die "Can't find executable at $safariPath.\n" if !-x $safariPath; + return $safariPath; +} + +sub builtDylibPathForName +{ + my $libraryName = shift; + determineConfigurationProductDir(); + + if (isGtk()) { + my $extension = isDarwin() ? ".dylib" : ".so"; + return "$configurationProductDir/lib/libwebkit2gtk-4.0" . $extension; + } + if (isEfl()) { + return "$configurationProductDir/lib/libewebkit2.so"; + } + if (isIOSWebKit()) { + return "$configurationProductDir/$libraryName.framework/$libraryName"; + } + if (isAppleCocoaWebKit()) { + return "$configurationProductDir/$libraryName.framework/Versions/A/$libraryName"; + } + if (isAppleWinWebKit()) { + if ($libraryName eq "JavaScriptCore") { + return "$baseProductDir/lib/$libraryName.lib"; + } else { + return "$baseProductDir/$libraryName.intermediate/$configuration/$libraryName.intermediate/$libraryName.lib"; + } + } + + die "Unsupported platform, can't determine built library locations.\nTry `build-webkit --help` for more information.\n"; +} + +# Check to see that all the frameworks are built. +sub checkFrameworks # FIXME: This is a poor name since only the Mac calls built WebCore a Framework. +{ + return if isAnyWindows(); + my @frameworks = ("JavaScriptCore", "WebCore"); + push(@frameworks, "WebKit") if isAppleCocoaWebKit(); # FIXME: This seems wrong, all ports should have a WebKit these days. + for my $framework (@frameworks) { + my $path = builtDylibPathForName($framework); + die "Can't find built framework at \"$path\".\n" unless -e $path; + } +} + +sub isInspectorFrontend() +{ + determineIsInspectorFrontend(); + return $isInspectorFrontend; +} + +sub determineIsInspectorFrontend() +{ + return if defined($isInspectorFrontend); + $isInspectorFrontend = checkForArgumentAndRemoveFromARGV("--inspector-frontend"); +} + +sub commandExists($) +{ + my $command = shift; + my $devnull = File::Spec->devnull(); + + if (isAnyWindows()) { + return exitStatus(system("where /q $command >$devnull 2>&1")) == 0; + } + return exitStatus(system("which $command >$devnull 2>&1")) == 0; +} + +sub checkForArgumentAndRemoveFromARGV($) +{ + my $argToCheck = shift; + return checkForArgumentAndRemoveFromArrayRef($argToCheck, \@ARGV); +} + +sub checkForArgumentAndRemoveFromArrayRefGettingValue($$$) +{ + my ($argToCheck, $valueRef, $arrayRef) = @_; + my $argumentStartRegEx = qr#^$argToCheck(?:=\S|$)#; + my $i = 0; + for (; $i < @$arrayRef; ++$i) { + last if $arrayRef->[$i] =~ $argumentStartRegEx; + } + if ($i >= @$arrayRef) { + return $$valueRef = undef; + } + my ($key, $value) = split("=", $arrayRef->[$i]); + splice(@$arrayRef, $i, 1); + if (defined($value)) { + # e.g. --sdk=iphonesimulator + return $$valueRef = $value; + } + return $$valueRef = splice(@$arrayRef, $i, 1); # e.g. --sdk iphonesimulator +} + +sub checkForArgumentAndRemoveFromARGVGettingValue($$) +{ + my ($argToCheck, $valueRef) = @_; + return checkForArgumentAndRemoveFromArrayRefGettingValue($argToCheck, $valueRef, \@ARGV); +} + +sub findMatchingArguments($$) +{ + my ($argToCheck, $arrayRef) = @_; + my @matchingIndices; + foreach my $index (0 .. $#$arrayRef) { + my $opt = $$arrayRef[$index]; + if ($opt =~ /^$argToCheck$/i ) { + push(@matchingIndices, $index); + } + } + return @matchingIndices; +} + +sub hasArgument($$) +{ + my ($argToCheck, $arrayRef) = @_; + my @matchingIndices = findMatchingArguments($argToCheck, $arrayRef); + return scalar @matchingIndices > 0; +} + +sub checkForArgumentAndRemoveFromArrayRef +{ + my ($argToCheck, $arrayRef) = @_; + my @indicesToRemove = findMatchingArguments($argToCheck, $arrayRef); + my $removeOffset = 0; + foreach my $index (@indicesToRemove) { + splice(@$arrayRef, $index - $removeOffset++, 1); + } + return scalar @indicesToRemove > 0; +} + +sub prohibitUnknownPort() +{ + $unknownPortProhibited = 1; +} + +sub determinePortName() +{ + return if defined $portName; + + my %argToPortName = ( + efl => Efl, + gtk => GTK, + 'jsc-only' => JSCOnly, + wincairo => WinCairo + ); + + for my $arg (sort keys %argToPortName) { + if (checkForArgumentAndRemoveFromARGV("--$arg")) { + die "Argument '--$arg' conflicts with selected port '$portName'\n" + if defined $portName; + + $portName = $argToPortName{$arg}; + } + } + + return if defined $portName; + + # Port was not selected via command line, use appropriate default value + + if (isAnyWindows()) { + $portName = AppleWin; + } elsif (isDarwin()) { + determineXcodeSDK(); + if (willUseIOSDeviceSDK() || willUseIOSSimulatorSDK()) { + $portName = iOS; + } else { + $portName = Mac; + } + } else { + if ($unknownPortProhibited) { + my $portsChoice = join "\n\t", qw( + --efl + --gtk + --jsc-only + ); + die "Please specify which WebKit port to build using one of the following options:" + . "\n\t$portsChoice\n"; + } + + # If script is run without arguments we cannot determine port + # TODO: This state should be outlawed + $portName = Unknown; + } +} + +sub portName() +{ + determinePortName(); + return $portName; +} + +sub isEfl() +{ + return portName() eq Efl; +} + +sub isGtk() +{ + return portName() eq GTK; +} + +sub isJSCOnly() +{ + return portName() eq JSCOnly; +} + +# Determine if this is debian, ubuntu, linspire, or something similar. +sub isDebianBased() +{ + return -e "/etc/debian_version"; +} + +sub isFedoraBased() +{ + return -e "/etc/fedora-release"; +} + +sub isWinCairo() +{ + return portName() eq WinCairo; +} + +sub isWin64() +{ + determineIsWin64(); + return $isWin64; +} + +sub determineIsWin64() +{ + return if defined($isWin64); + $isWin64 = checkForArgumentAndRemoveFromARGV("--64-bit"); +} + +sub determineIsWin64FromArchitecture($) +{ + my $arch = shift; + $isWin64 = ($arch eq "x86_64"); + return $isWin64; +} + +sub isCygwin() +{ + return ($^O eq "cygwin") || 0; +} + +sub isAnyWindows() +{ + return isWindows() || isCygwin(); +} + +sub determineWinVersion() +{ + return if $winVersion; + + if (!isAnyWindows()) { + $winVersion = -1; + return; + } + + my $versionString = `cmd /c ver`; + $versionString =~ /(\d)\.(\d)\.(\d+)/; + + $winVersion = { + major => $1, + minor => $2, + build => $3, + }; +} + +sub winVersion() +{ + determineWinVersion(); + return $winVersion; +} + +sub isWindows7SP0() +{ + return isAnyWindows() && winVersion()->{major} == 6 && winVersion()->{minor} == 1 && winVersion()->{build} == 7600; +} + +sub isWindowsVista() +{ + return isAnyWindows() && winVersion()->{major} == 6 && winVersion()->{minor} == 0; +} + +sub isWindowsXP() +{ + return isAnyWindows() && winVersion()->{major} == 5 && winVersion()->{minor} == 1; +} + +sub isDarwin() +{ + return ($^O eq "darwin") || 0; +} + +sub isWindows() +{ + return ($^O eq "MSWin32") || 0; +} + +sub isLinux() +{ + return ($^O eq "linux") || 0; +} + +sub isBSD() +{ + return ($^O eq "freebsd") || ($^O eq "openbsd") || ($^O eq "netbsd") || 0; +} + +sub isARM() +{ + return ($Config{archname} =~ /^arm[v\-]/) || ($Config{archname} =~ /^aarch64[v\-]/); +} + +sub isX86_64() +{ + return (architecture() eq "x86_64") || 0; +} + +sub isCrossCompilation() +{ + my $compiler = ""; + $compiler = $ENV{'CC'} if (defined($ENV{'CC'})); + if ($compiler =~ /gcc/) { + my $compiler_options = `$compiler -v 2>&1`; + my @host = $compiler_options =~ m/--host=(.*?)\s/; + my @target = $compiler_options =~ m/--target=(.*?)\s/; + + return ($host[0] ne "" && $target[0] ne "" && $host[0] ne $target[0]); + } + return 0; +} + +sub isAppleWebKit() +{ + return isAppleCocoaWebKit() || isAppleWinWebKit(); +} + +sub isAppleCocoaWebKit() +{ + return (portName() eq Mac) || isIOSWebKit(); +} + +sub isAppleWinWebKit() +{ + return portName() eq AppleWin; +} + +sub iOSSimulatorDevicesPath +{ + return "$ENV{HOME}/Library/Developer/CoreSimulator/Devices"; +} + +sub iOSSimulatorDevices +{ + eval "require Foundation"; + my $devicesPath = iOSSimulatorDevicesPath(); + opendir(DEVICES, $devicesPath); + my @udids = grep { + $_ =~ m/^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$/; + } readdir(DEVICES); + close(DEVICES); + + # FIXME: We should parse the device.plist file ourself and map the dictionary keys in it to known + # dictionary keys so as to decouple our representation of the plist from the actual structure + # of the plist, which may change. + my @devices = map { + Foundation::perlRefFromObjectRef(NSDictionary->dictionaryWithContentsOfFile_("$devicesPath/$_/device.plist")); + } @udids; + + return @devices; +} + +sub createiOSSimulatorDevice +{ + my $name = shift; + my $deviceTypeId = shift; + my $runtimeId = shift; + + my $created = system("xcrun", "--sdk", "iphonesimulator", "simctl", "create", $name, $deviceTypeId, $runtimeId) == 0; + die "Couldn't create simulator device: $name $deviceTypeId $runtimeId" if not $created; + + system("xcrun", "--sdk", "iphonesimulator", "simctl", "list"); + + print "Waiting for device to be created ...\n"; + sleep 5; + for (my $tries = 0; $tries < 5; $tries++){ + my @devices = iOSSimulatorDevices(); + foreach my $device (@devices) { + return $device if $device->{name} eq $name and $device->{deviceType} eq $deviceTypeId and $device->{runtime} eq $runtimeId; + } + sleep 5; + } + die "Device $name $deviceTypeId $runtimeId wasn't found in " . iOSSimulatorDevicesPath(); +} + +sub willUseIOSDeviceSDK() +{ + return xcodeSDKPlatformName() eq "iphoneos"; +} + +sub willUseIOSSimulatorSDK() +{ + return xcodeSDKPlatformName() eq "iphonesimulator"; +} + +sub isIOSWebKit() +{ + return portName() eq iOS; +} + +sub determineNmPath() +{ + return if $nmPath; + + if (isAppleCocoaWebKit()) { + $nmPath = `xcrun -find nm`; + chomp $nmPath; + } + $nmPath = "nm" if !$nmPath; +} + +sub nmPath() +{ + determineNmPath(); + return $nmPath; +} + +sub splitVersionString +{ + my $versionString = shift; + my @splitVersion = split(/\./, $versionString); + @splitVersion >= 2 or die "Invalid version $versionString"; + $osXVersion = { + "major" => $splitVersion[0], + "minor" => $splitVersion[1], + "subminor" => (defined($splitVersion[2]) ? $splitVersion[2] : 0), + }; +} + +sub determineOSXVersion() +{ + return if $osXVersion; + + if (!isDarwin()) { + $osXVersion = -1; + return; + } + + my $versionString = `sw_vers -productVersion`; + $osXVersion = splitVersionString($versionString); +} + +sub osXVersion() +{ + determineOSXVersion(); + return $osXVersion; +} + +sub determineIOSVersion() +{ + return if $iosVersion; + + if (!isIOSWebKit()) { + $iosVersion = -1; + return; + } + + my $versionString = xcodeSDKVersion(); + $iosVersion = splitVersionString($versionString); +} + +sub iosVersion() +{ + determineIOSVersion(); + return $iosVersion; +} + +sub isWindowsNT() +{ + return $ENV{'OS'} eq 'Windows_NT'; +} + +sub appendToEnvironmentVariableList($$) +{ + my ($name, $value) = @_; + + if (defined($ENV{$name})) { + $ENV{$name} .= $Config{path_sep} . $value; + } else { + $ENV{$name} = $value; + } +} + +sub prependToEnvironmentVariableList($$) +{ + my ($name, $value) = @_; + + if (defined($ENV{$name})) { + $ENV{$name} = $value . $Config{path_sep} . $ENV{$name}; + } else { + $ENV{$name} = $value; + } +} + +sub sharedCommandLineOptions() +{ + return ( + "g|guard-malloc" => \$shouldUseGuardMalloc, + ); +} + +sub sharedCommandLineOptionsUsage +{ + my %opts = @_; + + my %switches = ( + '-g|--guard-malloc' => 'Use guardmalloc when running executable', + ); + + my $indent = " " x ($opts{indent} || 2); + my $switchWidth = List::Util::max(int($opts{switchWidth}), List::Util::max(map { length($_) } keys %switches) + ($opts{brackets} ? 2 : 0)); + + my $result = "Common switches:\n"; + + for my $switch (keys %switches) { + my $switchName = $opts{brackets} ? "[" . $switch . "]" : $switch; + $result .= sprintf("%s%-" . $switchWidth . "s %s\n", $indent, $switchName, $switches{$switch}); + } + + return $result; +} + +sub setUpGuardMallocIfNeeded +{ + if (!isDarwin()) { + return; + } + + if (!defined($shouldUseGuardMalloc)) { + $shouldUseGuardMalloc = checkForArgumentAndRemoveFromARGV("-g") || checkForArgumentAndRemoveFromARGV("--guard-malloc"); + } + + if ($shouldUseGuardMalloc) { + appendToEnvironmentVariableList("DYLD_INSERT_LIBRARIES", "/usr/lib/libgmalloc.dylib"); + appendToEnvironmentVariableList("__XPC_DYLD_INSERT_LIBRARIES", "/usr/lib/libgmalloc.dylib"); + } +} + +sub relativeScriptsDir() +{ + my $scriptDir = File::Spec->catpath("", File::Spec->abs2rel($FindBin::Bin, getcwd()), ""); + if ($scriptDir eq "") { + $scriptDir = "."; + } + return $scriptDir; +} + +sub launcherPath() +{ + my $relativeScriptsPath = relativeScriptsDir(); + if (isGtk() || isEfl()) { + return "$relativeScriptsPath/run-minibrowser"; + } elsif (isAppleWebKit()) { + return "$relativeScriptsPath/run-safari"; + } +} + +sub launcherName() +{ + if (isGtk() || isEfl()) { + return "MiniBrowser"; + } elsif (isAppleCocoaWebKit()) { + return "Safari"; + } elsif (isAppleWinWebKit()) { + return "MiniBrowser"; + } +} + +sub checkRequiredSystemConfig +{ + if (isDarwin()) { + chomp(my $productVersion = `sw_vers -productVersion`); + if (eval "v$productVersion" lt v10.10.5) { + print "*************************************************************\n"; + print "OS X Yosemite v10.10.5 or later is required to build WebKit.\n"; + print "You have " . $productVersion . ", thus the build will most likely fail.\n"; + print "*************************************************************\n"; + } + determineXcodeVersion(); + if (eval "v$xcodeVersion" lt v7.0) { + print "*************************************************************\n"; + print "Xcode 7.0 or later is required to build WebKit.\n"; + print "You have an earlier version of Xcode, thus the build will\n"; + print "most likely fail. The latest Xcode is available from the App Store.\n"; + print "*************************************************************\n"; + } + } +} + +sub determineWindowsSourceDir() +{ + return if $windowsSourceDir; + $windowsSourceDir = sourceDir(); + chomp($windowsSourceDir = `cygpath -w '$windowsSourceDir'`) if isCygwin(); +} + +sub windowsSourceDir() +{ + determineWindowsSourceDir(); + return $windowsSourceDir; +} + +sub windowsSourceSourceDir() +{ + return File::Spec->catdir(windowsSourceDir(), "Source"); +} + +sub windowsLibrariesDir() +{ + return File::Spec->catdir(windowsSourceDir(), "WebKitLibraries", "win"); +} + +sub windowsOutputDir() +{ + return File::Spec->catdir(windowsSourceDir(), "WebKitBuild"); +} + +sub fontExists($) +{ + my $font = shift; + my $cmd = "reg query \"HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts\" /v \"$font\" 2>&1"; + my $val = `$cmd`; + return $? == 0; +} + +sub checkInstalledTools() +{ + # environment variables. Avoid until this is corrected. + my $pythonVer = `python --version 2>&1`; + die "You must have Python installed to build WebKit.\n" if ($?); + + # cURL 7.34.0 has a bug that prevents authentication with opensource.apple.com (and other things using SSL3). + my $curlVer = `curl --version 2> NUL`; + if (!$? and $curlVer =~ "(.*curl.*)") { + $curlVer = $1; + if ($curlVer =~ /libcurl\/7\.34\.0/) { + print "cURL version 7.34.0 has a bug that prevents authentication with SSL v2 or v3.\n"; + print "cURL 7.33.0 is known to work. The cURL projects is preparing an update to\n"; + print "correct this problem.\n\n"; + die "Please install a working cURL and try again.\n"; + } + } + + # MathML requires fonts that may not ship with Windows. + # Warn the user if they are missing. + my @fonts = ('Cambria & Cambria Math (TrueType)', 'LatinModernMath-Regular (TrueType)', 'STIXMath-Regular (TrueType)'); + my @missing = (); + foreach my $font (@fonts) { + push @missing, $font if not fontExists($font); + } + + if (scalar @missing > 0) { + print "*************************************************************\n"; + print "Mathematical fonts, such as Latin Modern Math are needed to\n"; + print "use the MathML feature. You do not appear to have these fonts\n"; + print "on your system.\n\n"; + print "You can download a suitable set of fonts from the following URL:\n"; + print "https://trac.webkit.org/wiki/MathML/Fonts\n"; + print "*************************************************************\n"; + } + + print "Installed tools are correct for the WebKit build.\n"; +} + +sub setupAppleWinEnv() +{ + return unless isAppleWinWebKit(); + + checkInstalledTools(); + + if (isWindowsNT()) { + my $restartNeeded = 0; + my %variablesToSet = (); + + # FIXME: We should remove this explicit version check for cygwin once we stop supporting Cygwin 1.7.9 or older versions. + # https://bugs.webkit.org/show_bug.cgi?id=85791 + my $uname_version = (POSIX::uname())[2]; + $uname_version =~ s/\(.*\)//; # Remove the trailing cygwin version, if any. + $uname_version =~ s/\-.*$//; # Remove trailing dash-version content, if any + if (version->parse($uname_version) < version->parse("1.7.10")) { + # Setting the environment variable 'CYGWIN' to 'tty' makes cygwin enable extra support (i.e., termios) + # for UNIX-like ttys in the Windows console + $variablesToSet{CYGWIN} = "tty" unless $ENV{CYGWIN}; + } + + # Those environment variables must be set to be able to build inside Visual Studio. + $variablesToSet{WEBKIT_LIBRARIES} = windowsLibrariesDir() unless $ENV{WEBKIT_LIBRARIES}; + $variablesToSet{WEBKIT_OUTPUTDIR} = windowsOutputDir() unless $ENV{WEBKIT_OUTPUTDIR}; + $variablesToSet{MSBUILDDISABLENODEREUSE} = "1" unless $ENV{MSBUILDDISABLENODEREUSE}; + $variablesToSet{_IsNativeEnvironment} = "true" unless $ENV{_IsNativeEnvironment}; + $variablesToSet{PreferredToolArchitecture} = "x64" unless $ENV{PreferredToolArchitecture}; + + foreach my $variable (keys %variablesToSet) { + print "Setting the Environment Variable '" . $variable . "' to '" . $variablesToSet{$variable} . "'\n\n"; + my $ret = system "setx", $variable, $variablesToSet{$variable}; + if ($ret != 0) { + system qw(regtool -s set), '\\HKEY_CURRENT_USER\\Environment\\' . $variable, $variablesToSet{$variable}; + } + $restartNeeded ||= $variable eq "WEBKIT_LIBRARIES" || $variable eq "WEBKIT_OUTPUTDIR"; + } + + if ($restartNeeded) { + print "Please restart your computer before attempting to build inside Visual Studio.\n\n"; + } + } else { + if (!defined $ENV{'WEBKIT_LIBRARIES'} || !$ENV{'WEBKIT_LIBRARIES'}) { + print "Warning: You must set the 'WebKit_Libraries' environment variable\n"; + print " to be able build WebKit from within Visual Studio 2013 and newer.\n"; + print " Make sure that 'WebKit_Libraries' points to the\n"; + print " 'WebKitLibraries/win' directory, not the 'WebKitLibraries/' directory.\n\n"; + } + if (!defined $ENV{'WEBKIT_OUTPUTDIR'} || !$ENV{'WEBKIT_OUTPUTDIR'}) { + print "Warning: You must set the 'WebKit_OutputDir' environment variable\n"; + print " to be able build WebKit from within Visual Studio 2013 and newer.\n\n"; + } + if (!defined $ENV{'MSBUILDDISABLENODEREUSE'} || !$ENV{'MSBUILDDISABLENODEREUSE'}) { + print "Warning: You should set the 'MSBUILDDISABLENODEREUSE' environment variable to '1'\n"; + print " to avoid periodic locked log files when building.\n\n"; + } + } + # FIXME (125180): Remove the following temporary 64-bit support once official support is available. + if (isWin64() and !$ENV{'WEBKIT_64_SUPPORT'}) { + print "Warning: You must set the 'WEBKIT_64_SUPPORT' environment variable\n"; + print " to be able run WebKit or JavaScriptCore tests.\n\n"; + } +} + +sub setupCygwinEnv() +{ + return if !isAnyWindows(); + return if $vcBuildPath; + + my $programFilesPath = programFilesPath(); + my $visualStudioPath = File::Spec->catfile(visualStudioInstallDir(), qw(Common7 IDE devenv.com)); + if (-e $visualStudioPath) { + # Visual Studio is installed; + if (visualStudioVersion() eq "12") { + $visualStudioPath = File::Spec->catfile(visualStudioInstallDir(), qw(Common7 IDE devenv.exe)); + } + } else { + # Visual Studio not found, try VC++ Express + $visualStudioPath = File::Spec->catfile(visualStudioInstallDir(), qw(Common7 IDE WDExpress.exe)); + if (! -e $visualStudioPath) { + print "*************************************************************\n"; + print "Cannot find '$visualStudioPath'\n"; + print "Please execute the file 'vcvars32.bat' from\n"; + print "'$programFilesPath\\Microsoft Visual Studio 14.0\\VC\\bin\\'\n"; + print "to setup the necessary environment variables.\n"; + print "*************************************************************\n"; + die; + } + $willUseVCExpressWhenBuilding = 1; + } + + print "Building results into: ", baseProductDir(), "\n"; + print "WEBKIT_OUTPUTDIR is set to: ", $ENV{"WEBKIT_OUTPUTDIR"}, "\n"; + print "WEBKIT_LIBRARIES is set to: ", $ENV{"WEBKIT_LIBRARIES"}, "\n"; + # FIXME (125180): Remove the following temporary 64-bit support once official support is available. + print "WEBKIT_64_SUPPORT is set to: ", $ENV{"WEBKIT_64_SUPPORT"}, "\n" if isWin64(); + + # We will actually use MSBuild to build WebKit, but we need to find the Visual Studio install (above) to make + # sure we use the right options. + $vcBuildPath = File::Spec->catfile(msBuildInstallDir(), qw(MSBuild.exe)); + if (! -e $vcBuildPath) { + print "*************************************************************\n"; + print "Cannot find '$vcBuildPath'\n"; + print "Please make sure execute that the Microsoft .NET Framework SDK\n"; + print "is installed on this machine.\n"; + print "*************************************************************\n"; + die; + } +} + +sub dieIfWindowsPlatformSDKNotInstalled +{ + my $registry32Path = "/proc/registry/"; + my $registry64Path = "/proc/registry64/"; + my @windowsPlatformSDKRegistryEntries = ( + "HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Microsoft SDKs/Windows/v8.0A", + "HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Microsoft SDKs/Windows/v8.0", + "HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Microsoft SDKs/Windows/v7.1A", + "HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Microsoft SDKs/Windows/v7.0A", + "HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/MicrosoftSDK/InstalledSDKs/D2FF9F89-8AA2-4373-8A31-C838BF4DBBE1", + ); + + # FIXME: It would be better to detect whether we are using 32- or 64-bit Windows + # and only check the appropriate entry. But for now we just blindly check both. + my $recommendedPlatformSDK = $windowsPlatformSDKRegistryEntries[0]; + + while (@windowsPlatformSDKRegistryEntries) { + my $windowsPlatformSDKRegistryEntry = shift @windowsPlatformSDKRegistryEntries; + return if (-e $registry32Path . $windowsPlatformSDKRegistryEntry) || (-e $registry64Path . $windowsPlatformSDKRegistryEntry); + } + + print "*************************************************************\n"; + print "Cannot find registry entry '$recommendedPlatformSDK'.\n"; + print "Please download and install the Microsoft Windows SDK\n"; + print "from <http://www.microsoft.com/en-us/download/details.aspx?id=8279>.\n\n"; + print "Then follow step 2 in the Windows section of the \"Installing Developer\n"; + print "Tools\" instructions at <http://www.webkit.org/building/tools.html>.\n"; + print "*************************************************************\n"; + die; +} + +sub buildXCodeProject($$@) +{ + my ($project, $clean, @extraOptions) = @_; + + if ($clean) { + push(@extraOptions, "-alltargets"); + push(@extraOptions, "clean"); + } + + chomp($ENV{DSYMUTIL_NUM_THREADS} = `sysctl -n hw.activecpu`); + return system "xcodebuild", "-project", "$project.xcodeproj", @extraOptions; +} + +sub usingVisualStudioExpress() +{ + setupCygwinEnv(); + return $willUseVCExpressWhenBuilding; +} + +sub buildVisualStudioProject +{ + my ($project, $clean) = @_; + setupCygwinEnv(); + + my $config = configurationForVisualStudio(); + + dieIfWindowsPlatformSDKNotInstalled() if $willUseVCExpressWhenBuilding; + + chomp($project = `cygpath -w "$project"`) if isCygwin(); + + my $action = "/t:build"; + if ($clean) { + $action = "/t:clean"; + } + + my $platform = "/p:Platform=" . (isWin64() ? "x64" : "Win32"); + my $logPath = File::Spec->catdir($baseProductDir, $configuration); + make_path($logPath) unless -d $logPath or $logPath eq "."; + + my $errorLogFile = File::Spec->catfile($logPath, "webkit_errors.log"); + chomp($errorLogFile = `cygpath -w "$errorLogFile"`) if isCygwin(); + my $errorLogging = "/flp:LogFile=" . $errorLogFile . ";ErrorsOnly"; + + my $warningLogFile = File::Spec->catfile($logPath, "webkit_warnings.log"); + chomp($warningLogFile = `cygpath -w "$warningLogFile"`) if isCygwin(); + my $warningLogging = "/flp1:LogFile=" . $warningLogFile . ";WarningsOnly"; + + my @command = ($vcBuildPath, "/verbosity:minimal", $project, $action, $config, $platform, "/fl", $errorLogging, "/fl1", $warningLogging); + print join(" ", @command), "\n"; + return system @command; +} + +sub getJhbuildPath() +{ + my @jhbuildPath = File::Spec->splitdir(baseProductDir()); + if (isGit() && isGitBranchBuild() && gitBranch()) { + pop(@jhbuildPath); + } + if (isEfl()) { + push(@jhbuildPath, "DependenciesEFL"); + } elsif (isGtk()) { + push(@jhbuildPath, "DependenciesGTK"); + } else { + die "Cannot get JHBuild path for platform that isn't GTK+ or EFL.\n"; + } + return File::Spec->catdir(@jhbuildPath); +} + +sub isCachedArgumentfileOutOfDate($@) +{ + my ($filename, $currentContents) = @_; + + if (! -e $filename) { + return 1; + } + + open(CONTENTS_FILE, $filename); + chomp(my $previousContents = <CONTENTS_FILE>); + close(CONTENTS_FILE); + + if ($previousContents ne $currentContents) { + print "Contents for file $filename have changed.\n"; + print "Previous contents were: $previousContents\n\n"; + print "New contents are: $currentContents\n"; + return 1; + } + + return 0; +} + +sub wrapperPrefixIfNeeded() +{ + if (isAnyWindows() || isJSCOnly()) { + return (); + } + if (isAppleCocoaWebKit()) { + return ("xcrun"); + } + if (-e getJhbuildPath()) { + my @prefix = (File::Spec->catfile(sourceDir(), "Tools", "jhbuild", "jhbuild-wrapper")); + if (isEfl()) { + push(@prefix, "--efl"); + } elsif (isGtk()) { + push(@prefix, "--gtk"); + } + push(@prefix, "run"); + + return @prefix; + } + + return (); +} + +sub cmakeCachePath() +{ + return File::Spec->catdir(baseProductDir(), configuration(), "CMakeCache.txt"); +} + +sub cmakeFilesPath() +{ + return File::Spec->catdir(baseProductDir(), configuration(), "CMakeFiles"); +} + +sub shouldRemoveCMakeCache(@) +{ + my ($cacheFilePath, @buildArgs) = @_; + + # We check this first, because we always want to create this file for a fresh build. + my $productDir = File::Spec->catdir(baseProductDir(), configuration()); + my $optionsCache = File::Spec->catdir($productDir, "build-webkit-options.txt"); + my $joinedBuildArgs = join(" ", @buildArgs); + if (isCachedArgumentfileOutOfDate($optionsCache, $joinedBuildArgs)) { + File::Path::mkpath($productDir) unless -d $productDir; + open(CACHED_ARGUMENTS, ">", $optionsCache); + print CACHED_ARGUMENTS $joinedBuildArgs; + close(CACHED_ARGUMENTS); + + return 1; + } + + my $cmakeCache = cmakeCachePath(); + unless (-e $cmakeCache) { + return 0; + } + + my $cacheFileModifiedTime = stat($cmakeCache)->mtime; + my $platformConfiguration = File::Spec->catdir(sourceDir(), "Source", "cmake", "Options" . cmakeBasedPortName() . ".cmake"); + if ($cacheFileModifiedTime < stat($platformConfiguration)->mtime) { + return 1; + } + + my $globalConfiguration = File::Spec->catdir(sourceDir(), "Source", "cmake", "OptionsCommon.cmake"); + if ($cacheFileModifiedTime < stat($globalConfiguration)->mtime) { + return 1; + } + + my $inspectorUserInterfaceDircetory = File::Spec->catdir(sourceDir(), "Source", "WebInspectorUI", "UserInterface"); + if ($cacheFileModifiedTime < stat($inspectorUserInterfaceDircetory)->mtime) { + return 1; + } + + if(isAnyWindows()) { + my $winConfiguration = File::Spec->catdir(sourceDir(), "Source", "cmake", "OptionsWin.cmake"); + if ($cacheFileModifiedTime < stat($winConfiguration)->mtime) { + return 1; + } + } + + return 0; +} + +sub removeCMakeCache(@) +{ + my (@buildArgs) = @_; + if (shouldRemoveCMakeCache(@buildArgs)) { + my $cmakeCache = cmakeCachePath(); + my $cmakeFiles = cmakeFilesPath(); + unlink($cmakeCache) if -e $cmakeCache; + rmtree($cmakeFiles) if -d $cmakeFiles; + } +} + +sub canUseNinja(@) +{ + if (!defined($shouldNotUseNinja)) { + $shouldNotUseNinja = checkForArgumentAndRemoveFromARGV("--no-ninja"); + } + + if ($shouldNotUseNinja) { + return 0; + } + + # Test both ninja and ninja-build. Fedora uses ninja-build and has patched CMake to also call ninja-build. + return commandExists("ninja") || commandExists("ninja-build"); +} + +sub canUseNinjaGenerator(@) +{ + # Check that a Ninja generator is installed + my $devnull = File::Spec->devnull(); + return exitStatus(system("cmake -N -G Ninja >$devnull 2>&1")) == 0; +} + +sub canUseEclipseNinjaGenerator(@) +{ + # Check that eclipse and eclipse Ninja generator is installed + my $devnull = File::Spec->devnull(); + return commandExists("eclipse") && exitStatus(system("cmake -N -G 'Eclipse CDT4 - Ninja' >$devnull 2>&1")) == 0; +} + +sub cmakeGeneratedBuildfile(@) +{ + my ($willUseNinja) = @_; + if ($willUseNinja) { + return File::Spec->catfile(baseProductDir(), configuration(), "build.ninja") + } elsif (isAnyWindows()) { + return File::Spec->catfile(baseProductDir(), configuration(), "WebKit.sln") + } else { + return File::Spec->catfile(baseProductDir(), configuration(), "Makefile") + } +} + +sub generateBuildSystemFromCMakeProject +{ + my ($prefixPath, @cmakeArgs) = @_; + my $config = configuration(); + my $port = cmakeBasedPortName(); + my $buildPath = File::Spec->catdir(baseProductDir(), $config); + File::Path::mkpath($buildPath) unless -d $buildPath; + my $originalWorkingDirectory = getcwd(); + chdir($buildPath) or die; + + # We try to be smart about when to rerun cmake, so that we can have faster incremental builds. + my $willUseNinja = canUseNinja() && canUseNinjaGenerator(); + if (-e cmakeCachePath() && -e cmakeGeneratedBuildfile($willUseNinja)) { + return 0; + } + + my @args; + push @args, "-DPORT=\"$port\""; + push @args, "-DCMAKE_INSTALL_PREFIX=\"$prefixPath\"" if $prefixPath; + push @args, "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON"; + if ($config =~ /release/i) { + push @args, "-DCMAKE_BUILD_TYPE=Release"; + } elsif ($config =~ /debug/i) { + push @args, "-DCMAKE_BUILD_TYPE=Debug"; + } + + if ($willUseNinja) { + push @args, "-G"; + if (canUseEclipseNinjaGenerator()) { + push @args, "'Eclipse CDT4 - Ninja'"; + } else { + push @args, "Ninja"; + } + } elsif (isAnyWindows() && isWin64()) { + push @args, '-G "Visual Studio 14 2015 Win64"'; + } + # Do not show progress of generating bindings in interactive Ninja build not to leave noisy lines on tty + push @args, '-DSHOW_BINDINGS_GENERATION_PROGRESS=1' unless ($willUseNinja && -t STDOUT); + + # Some ports have production mode, but build-webkit should always use developer mode. + push @args, "-DDEVELOPER_MODE=ON" if isEfl() || isGtk() || isJSCOnly(); + + # Don't warn variables which aren't used by cmake ports. + push @args, "--no-warn-unused-cli"; + push @args, @cmakeArgs if @cmakeArgs; + + my $cmakeSourceDir = isCygwin() ? windowsSourceDir() : sourceDir(); + push @args, '"' . $cmakeSourceDir . '"'; + + # Compiler options to keep floating point values consistent + # between 32-bit and 64-bit architectures. + determineArchitecture(); + if ($architecture eq "i686" && !isCrossCompilation() && !isAnyWindows()) { + $ENV{'CXXFLAGS'} = "-march=pentium4 -msse2 -mfpmath=sse " . ($ENV{'CXXFLAGS'} || ""); + } + + # We call system("cmake @args") instead of system("cmake", @args) so that @args is + # parsed for shell metacharacters. + my $wrapper = join(" ", wrapperPrefixIfNeeded()) . " "; + my $returnCode = systemVerbose($wrapper . "cmake @args"); + + chdir($originalWorkingDirectory); + return $returnCode; +} + +sub buildCMakeGeneratedProject($) +{ + my ($makeArgs) = @_; + my $config = configuration(); + my $buildPath = File::Spec->catdir(baseProductDir(), $config); + if (! -d $buildPath) { + die "Must call generateBuildSystemFromCMakeProject() before building CMake project."; + } + + my $command = "cmake"; + my @args = ("--build", $buildPath, "--config", $config); + push @args, ("--", $makeArgs) if $makeArgs; + + # GTK and JSCOnly can use a build script to preserve colors and pretty-printing. + if ((isGtk() || isJSCOnly()) && -e "$buildPath/build.sh") { + chdir "$buildPath" or die; + $command = "$buildPath/build.sh"; + @args = ($makeArgs); + } + + if ($ENV{VERBOSE} && canUseNinja()) { + push @args, "-v"; + push @args, "-d keeprsp" if (version->parse(determineNinjaVersion()) >= version->parse("1.4.0")); + } + + # We call system("cmake @args") instead of system("cmake", @args) so that @args is + # parsed for shell metacharacters. In particular, $makeArgs may contain such metacharacters. + my $wrapper = join(" ", wrapperPrefixIfNeeded()) . " "; + return systemVerbose($wrapper . "$command @args"); +} + +sub cleanCMakeGeneratedProject() +{ + my $config = configuration(); + my $buildPath = File::Spec->catdir(baseProductDir(), $config); + if (-d $buildPath) { + return systemVerbose("cmake", "--build", $buildPath, "--config", $config, "--target", "clean"); + } + return 0; +} + +sub buildCMakeProjectOrExit($$$@) +{ + my ($clean, $prefixPath, $makeArgs, @cmakeArgs) = @_; + my $returnCode; + + exit(exitStatus(cleanCMakeGeneratedProject())) if $clean; + + if (isEfl() && checkForArgumentAndRemoveFromARGV("--update-efl")) { + system("perl", "$sourceDir/Tools/Scripts/update-webkitefl-libs") == 0 or die $!; + } + + if (isGtk() && checkForArgumentAndRemoveFromARGV("--update-gtk")) { + system("perl", "$sourceDir/Tools/Scripts/update-webkitgtk-libs") == 0 or die $!; + } + + $returnCode = exitStatus(generateBuildSystemFromCMakeProject($prefixPath, @cmakeArgs)); + exit($returnCode) if $returnCode; + + $returnCode = exitStatus(buildCMakeGeneratedProject($makeArgs)); + exit($returnCode) if $returnCode; + return 0; +} + +sub cmakeBasedPortArguments() +{ + return (); +} + +sub cmakeBasedPortName() +{ + return ucfirst portName(); +} + +sub determineIsCMakeBuild() +{ + return if defined($isCMakeBuild); + $isCMakeBuild = checkForArgumentAndRemoveFromARGV("--cmake"); +} + +sub isCMakeBuild() +{ + return 1 unless isAppleCocoaWebKit(); + determineIsCMakeBuild(); + return $isCMakeBuild; +} + +sub promptUser +{ + my ($prompt, $default) = @_; + my $defaultValue = $default ? "[$default]" : ""; + print "$prompt $defaultValue: "; + chomp(my $input = <STDIN>); + return $input ? $input : $default; +} + +sub appleApplicationSupportPath +{ + open INSTALL_DIR, "</proc/registry/HKEY_LOCAL_MACHINE/SOFTWARE/Apple\ Inc./Apple\ Application\ Support/InstallDir"; + my $path = <INSTALL_DIR>; + $path =~ s/[\r\n\x00].*//; + close INSTALL_DIR; + + my $unixPath = `cygpath -u '$path'`; + chomp $unixPath; + return $unixPath; +} + +sub setPathForRunningWebKitApp +{ + my ($env) = @_; + + if (isAnyWindows()) { + my $productBinaryDir = executableProductDir(); + if (isAppleWinWebKit()) { + $env->{PATH} = join(':', $productBinaryDir, appleApplicationSupportPath(), $env->{PATH} || ""); + } elsif (isWinCairo()) { + my $winCairoBin = sourceDir() . "/WebKitLibraries/win/" . (isWin64() ? "bin64/" : "bin32/"); + my $gstreamerBin = isWin64() ? $ENV{"GSTREAMER_1_0_ROOT_X86_64"} . "bin" : $ENV{"GSTREAMER_1_0_ROOT_X86"} . "bin"; + $env->{PATH} = join(':', $productBinaryDir, $winCairoBin, $gstreamerBin, $env->{PATH} || ""); + } + } +} + +sub printHelpAndExitForRunAndDebugWebKitAppIfNeeded +{ + return unless checkForArgumentAndRemoveFromARGV("--help"); + + print STDERR <<EOF; +Usage: @{[basename($0)]} [options] [args ...] + --help Show this help message + --no-saved-state Launch the application without state restoration + -g|--guard-malloc Enable Guard Malloc (OS X only) +EOF + + exit(1); +} + +sub argumentsForRunAndDebugMacWebKitApp() +{ + my @args = (); + if (checkForArgumentAndRemoveFromARGV("--no-saved-state")) { + push @args, ("-ApplePersistenceIgnoreStateQuietly", "YES"); + # FIXME: Don't set ApplePersistenceIgnoreState once all supported OS versions respect ApplePersistenceIgnoreStateQuietly (rdar://15032886). + push @args, ("-ApplePersistenceIgnoreState", "YES"); + } + unshift @args, @ARGV; + + return @args; +} + +sub setupMacWebKitEnvironment($) +{ + my ($dyldFrameworkPath) = @_; + + $dyldFrameworkPath = File::Spec->rel2abs($dyldFrameworkPath); + + prependToEnvironmentVariableList("DYLD_FRAMEWORK_PATH", $dyldFrameworkPath); + prependToEnvironmentVariableList("__XPC_DYLD_FRAMEWORK_PATH", $dyldFrameworkPath); + $ENV{WEBKIT_UNSET_DYLD_FRAMEWORK_PATH} = "YES"; + + setUpGuardMallocIfNeeded(); +} + +sub setupIOSWebKitEnvironment($) +{ + my ($dyldFrameworkPath) = @_; + $dyldFrameworkPath = File::Spec->rel2abs($dyldFrameworkPath); + + prependToEnvironmentVariableList("DYLD_FRAMEWORK_PATH", $dyldFrameworkPath); + prependToEnvironmentVariableList("DYLD_LIBRARY_PATH", $dyldFrameworkPath); + + setUpGuardMallocIfNeeded(); +} + +sub iosSimulatorApplicationsPath() +{ + return File::Spec->catdir(XcodeSDKPath(), "Applications"); +} + +sub installedMobileSafariBundle() +{ + return File::Spec->catfile(iosSimulatorApplicationsPath(), "MobileSafari.app"); +} + +sub mobileSafariBundle() +{ + determineConfigurationProductDir(); + + # Use MobileSafari.app in product directory if present. + if (isAppleCocoaWebKit() && -d "$configurationProductDir/MobileSafari.app") { + return "$configurationProductDir/MobileSafari.app"; + } + return installedMobileSafariBundle(); +} + +sub plistPathFromBundle($) +{ + my ($appBundle) = @_; + return "$appBundle/Info.plist" if -f "$appBundle/Info.plist"; # iOS app bundle + return "$appBundle/Contents/Info.plist" if -f "$appBundle/Contents/Info.plist"; # Mac app bundle + return ""; +} + +sub appIdentifierFromBundle($) +{ + my ($appBundle) = @_; + my $plistPath = File::Spec->rel2abs(plistPathFromBundle($appBundle)); # defaults(1) will complain if the specified path is not absolute. + chomp(my $bundleIdentifier = `defaults read '$plistPath' CFBundleIdentifier 2> /dev/null`); + return $bundleIdentifier; +} + +sub appDisplayNameFromBundle($) +{ + my ($appBundle) = @_; + my $plistPath = File::Spec->rel2abs(plistPathFromBundle($appBundle)); # defaults(1) will complain if the specified path is not absolute. + chomp(my $bundleDisplayName = `defaults read '$plistPath' CFBundleDisplayName 2> /dev/null`); + return $bundleDisplayName; +} + +sub waitUntilIOSSimulatorDeviceIsInState($$) +{ + my ($deviceUDID, $waitUntilState) = @_; + my $device = iosSimulatorDeviceByUDID($deviceUDID); + # FIXME: We should add a maximum time limit to wait here. + while ($device->{state} ne $waitUntilState) { + usleep(500 * 1000); # Waiting 500ms between file system polls does not make script run-safari feel sluggish. + $device = iosSimulatorDeviceByUDID($deviceUDID); + } +} + +sub shutDownIOSSimulatorDevice($) +{ + my ($simulatorDevice) = @_; + system("xcrun --sdk iphonesimulator simctl shutdown $simulatorDevice->{UDID} > /dev/null 2>&1"); +} + +sub restartIOSSimulatorDevice($) +{ + my ($simulatorDevice) = @_; + shutDownIOSSimulatorDevice($simulatorDevice); + + exitStatus(system("xcrun", "--sdk", "iphonesimulator", "simctl", "boot", $simulatorDevice->{UDID})) == 0 or die "Failed to boot simulator device $simulatorDevice->{UDID}"; +} + +sub relaunchIOSSimulator($) +{ + my ($simulatedDevice) = @_; + quitIOSSimulator($simulatedDevice->{UDID}); + + # FIXME: <rdar://problem/20916140> Switch to using CoreSimulator.framework for launching and quitting iOS Simulator + chomp(my $developerDirectory = $ENV{DEVELOPER_DIR} || `xcode-select --print-path`); + my $iosSimulatorPath = File::Spec->catfile($developerDirectory, "Applications", "Simulator.app"); + system("open", "-a", $iosSimulatorPath, "--args", "-CurrentDeviceUDID", $simulatedDevice->{UDID}) == 0 or die "Failed to open $iosSimulatorPath: $!"; + + waitUntilIOSSimulatorDeviceIsInState($simulatedDevice->{UDID}, SIMULATOR_DEVICE_STATE_BOOTED); +} + +sub quitIOSSimulator(;$) +{ + my ($waitForShutdownOfSimulatedDeviceUDID) = @_; + # FIXME: <rdar://problem/20916140> Switch to using CoreSimulator.framework for launching and quitting iOS Simulator + if (exitStatus(system {"osascript"} "osascript", "-e", 'tell application id "com.apple.iphonesimulator" to quit')) { + # osascript returns a non-zero exit status if Simulator.app is not registered in LaunchServices. + return; + } + + if (!defined($waitForShutdownOfSimulatedDeviceUDID)) { + return; + } + # FIXME: We assume that $waitForShutdownOfSimulatedDeviceUDID was not booted using the simctl command line tool. + # Otherwise we will spin indefinitely since quiting the iOS Simulator will not shutdown this device. We + # should add a maximum time limit to wait for a device to shutdown and either return an error or die() + # on expiration of the time limit. + waitUntilIOSSimulatorDeviceIsInState($waitForShutdownOfSimulatedDeviceUDID, SIMULATOR_DEVICE_STATE_SHUTDOWN); +} + +sub iosSimulatorDeviceByName($) +{ + my ($simulatorName) = @_; + my $simulatorRuntime = iosSimulatorRuntime(); + my @devices = iOSSimulatorDevices(); + for my $device (@devices) { + if ($device->{name} eq $simulatorName && $device->{runtime} eq $simulatorRuntime) { + return $device; + } + } + return undef; +} + +sub iosSimulatorDeviceByUDID($) +{ + my ($simulatedDeviceUDID) = @_; + my $devicePlistPath = File::Spec->catfile(iOSSimulatorDevicesPath(), $simulatedDeviceUDID, "device.plist"); + if (!-f $devicePlistPath) { + return; + } + # FIXME: We should parse the device.plist file ourself and map the dictionary keys in it to known + # dictionary keys so as to decouple our representation of the plist from the actual structure + # of the plist, which may change. + eval "require Foundation"; + return Foundation::perlRefFromObjectRef(NSDictionary->dictionaryWithContentsOfFile_($devicePlistPath)); +} + +sub iosSimulatorRuntime() +{ + my $xcodeSDKVersion = xcodeSDKVersion(); + $xcodeSDKVersion =~ s/\./-/; + return "com.apple.CoreSimulator.SimRuntime.iOS-$xcodeSDKVersion"; +} + +sub findOrCreateSimulatorForIOSDevice($) +{ + my ($simulatorNameSuffix) = @_; + my $simulatorName; + my $simulatorDeviceType; + if (architecture() eq "x86_64") { + $simulatorName = "iPhone 5s " . $simulatorNameSuffix; + $simulatorDeviceType = "com.apple.CoreSimulator.SimDeviceType.iPhone-5s"; + } else { + $simulatorName = "iPhone 5 " . $simulatorNameSuffix; + $simulatorDeviceType = "com.apple.CoreSimulator.SimDeviceType.iPhone-5"; + } + my $simulatedDevice = iosSimulatorDeviceByName($simulatorName); + return $simulatedDevice if $simulatedDevice; + return createiOSSimulatorDevice($simulatorName, $simulatorDeviceType, iosSimulatorRuntime()); +} + +sub isIOSSimulatorSystemInstalledApp($) +{ + my ($appBundle) = @_; + my $simulatorApplicationsPath = realpath(iosSimulatorApplicationsPath()); + return substr(realpath($appBundle), 0, length($simulatorApplicationsPath)) eq $simulatorApplicationsPath; +} + +sub hasUserInstalledAppInSimulatorDevice($$) +{ + my ($appIdentifier, $simulatedDeviceUDID) = @_; + my $userInstalledAppPath = File::Spec->catfile($ENV{HOME}, "Library", "Developer", "CoreSimulator", "Devices", $simulatedDeviceUDID, "data", "Containers", "Bundle", "Application"); + if (!-d $userInstalledAppPath) { + return 0; # No user installed apps. + } + local @::userInstalledAppBundles; + my $wantedFunction = sub { + my $file = $_; + + # Ignore hidden files and directories. + if ($file =~ /^\../) { + $File::Find::prune = 1; + return; + } + + return if !-d $file || $file !~ /\.app$/; + push @::userInstalledAppBundles, $File::Find::name; + $File::Find::prune = 1; # Do not traverse contents of app bundle. + }; + find($wantedFunction, $userInstalledAppPath); + for my $userInstalledAppBundle (@::userInstalledAppBundles) { + if (appIdentifierFromBundle($userInstalledAppBundle) eq $appIdentifier) { + return 1; # Has user installed app. + } + } + return 0; # Does not have user installed app. +} + +sub isSimulatorDeviceBooted($) +{ + my ($simulatedDeviceUDID) = @_; + my $device = iosSimulatorDeviceByUDID($simulatedDeviceUDID); + return $device && $device->{state} eq SIMULATOR_DEVICE_STATE_BOOTED; +} + +sub runIOSWebKitAppInSimulator($;$) +{ + my ($appBundle, $simulatorOptions) = @_; + my $productDir = productDir(); + my $appDisplayName = appDisplayNameFromBundle($appBundle); + my $appIdentifier = appIdentifierFromBundle($appBundle); + my $simulatedDevice = findOrCreateSimulatorForIOSDevice(SIMULATOR_DEVICE_SUFFIX_FOR_WEBKIT_DEVELOPMENT); + my $simulatedDeviceUDID = $simulatedDevice->{UDID}; + + my $willUseSystemInstalledApp = isIOSSimulatorSystemInstalledApp($appBundle); + if ($willUseSystemInstalledApp) { + if (hasUserInstalledAppInSimulatorDevice($appIdentifier, $simulatedDeviceUDID)) { + # Restore the system-installed app in the simulator device corresponding to $appBundle as it + # was previously overwritten with a custom built version of the app. + # FIXME: Only restore the system-installed version of the app instead of erasing all contents and settings. + print "Quitting iOS Simulator...\n"; + quitIOSSimulator($simulatedDeviceUDID); + print "Erasing contents and settings for simulator device \"$simulatedDevice->{name}\".\n"; + exitStatus(system("xcrun", "--sdk", "iphonesimulator", "simctl", "erase", $simulatedDeviceUDID)) == 0 or die; + } + # FIXME: We assume that if $simulatedDeviceUDID is not booted then iOS Simulator is not open. However + # $simulatedDeviceUDID may have been booted using the simctl command line tool. If $simulatedDeviceUDID + # was booted using simctl then we should shutdown the device and launch iOS Simulator to boot it again. + if (!isSimulatorDeviceBooted($simulatedDeviceUDID)) { + print "Launching iOS Simulator...\n"; + relaunchIOSSimulator($simulatedDevice); + } + } else { + # FIXME: We should killall(1) any running instances of $appBundle before installing it to ensure + # that simctl launch opens the latest installed version of the app. For now we quit and + # launch the iOS Simulator again to ensure there are no running instances of $appBundle. + print "Quitting and launching iOS Simulator...\n"; + relaunchIOSSimulator($simulatedDevice); + + print "Installing $appBundle.\n"; + # Install custom built app, overwriting an app with the same app identifier if one exists. + exitStatus(system("xcrun", "--sdk", "iphonesimulator", "simctl", "install", $simulatedDeviceUDID, $appBundle)) == 0 or die; + + } + + $simulatorOptions = {} unless $simulatorOptions; + + my %simulatorENV; + %simulatorENV = %{$simulatorOptions->{applicationEnvironment}} if $simulatorOptions->{applicationEnvironment}; + { + local %ENV; # Shadow global-scope %ENV so that changes to it will not be seen outside of this scope. + setupIOSWebKitEnvironment($productDir); + %simulatorENV = %ENV; + } + my $applicationArguments = \@ARGV; + $applicationArguments = $simulatorOptions->{applicationArguments} if $simulatorOptions && $simulatorOptions->{applicationArguments}; + + # Prefix the environment variables with SIMCTL_CHILD_ per `xcrun simctl help launch`. + foreach my $key (keys %simulatorENV) { + $ENV{"SIMCTL_CHILD_$key"} = $simulatorENV{$key}; + } + + print "Starting $appDisplayName with DYLD_FRAMEWORK_PATH set to point to built WebKit in $productDir.\n"; + return exitStatus(system("xcrun", "--sdk", "iphonesimulator", "simctl", "launch", $simulatedDeviceUDID, $appIdentifier, @$applicationArguments)); +} + +sub runIOSWebKitApp($) +{ + my ($appBundle) = @_; + if (willUseIOSDeviceSDK()) { + die "Only running Safari in iOS Simulator is supported now."; + } + if (willUseIOSSimulatorSDK()) { + return runIOSWebKitAppInSimulator($appBundle); + } + die "Not using an iOS SDK." +} + +sub archCommandLineArgumentsForRestrictedEnvironmentVariables() +{ + my @arguments = (); + foreach my $key (keys(%ENV)) { + if ($key =~ /^DYLD_/) { + push @arguments, "-e", "$key=$ENV{$key}"; + } + } + return @arguments; +} + +sub runMacWebKitApp($;$) +{ + my ($appPath, $useOpenCommand) = @_; + my $productDir = productDir(); + print "Starting @{[basename($appPath)]} with DYLD_FRAMEWORK_PATH set to point to built WebKit in $productDir.\n"; + + local %ENV = %ENV; + setupMacWebKitEnvironment($productDir); + + if (defined($useOpenCommand) && $useOpenCommand == USE_OPEN_COMMAND) { + return system("open", "-W", "-a", $appPath, "--args", argumentsForRunAndDebugMacWebKitApp()); + } + if (architecture()) { + return system "arch", "-" . architecture(), archCommandLineArgumentsForRestrictedEnvironmentVariables(), $appPath, argumentsForRunAndDebugMacWebKitApp(); + } + return system { $appPath } $appPath, argumentsForRunAndDebugMacWebKitApp(); +} + +sub execMacWebKitAppForDebugging($) +{ + my ($appPath) = @_; + my $architectureSwitch = "--arch"; + my $argumentsSeparator = "--"; + + my $debuggerPath = `xcrun -find lldb`; + chomp $debuggerPath; + die "Can't find the lldb executable.\n" unless -x $debuggerPath; + + my $productDir = productDir(); + setupMacWebKitEnvironment($productDir); + + my @architectureFlags = ($architectureSwitch, architecture()); + print "Starting @{[basename($appPath)]} under lldb with DYLD_FRAMEWORK_PATH set to point to built WebKit in $productDir.\n"; + exec { $debuggerPath } $debuggerPath, @architectureFlags, $argumentsSeparator, $appPath, argumentsForRunAndDebugMacWebKitApp() or die; +} + +sub debugSafari +{ + if (isAppleCocoaWebKit()) { + checkFrameworks(); + execMacWebKitAppForDebugging(safariPath()); + } + + return 1; # Unsupported platform; can't debug Safari on this platform. +} + +sub runSafari +{ + if (isIOSWebKit()) { + return runIOSWebKitApp(mobileSafariBundle()); + } + + if (isAppleCocoaWebKit()) { + return runMacWebKitApp(safariPath()); + } + + if (isAppleWinWebKit()) { + my $result; + my $webKitLauncherPath = File::Spec->catfile(executableProductDir(), "MiniBrowser.exe"); + return system { $webKitLauncherPath } $webKitLauncherPath, @ARGV; + } + + return 1; # Unsupported platform; can't run Safari on this platform. +} + +sub runMiniBrowser +{ + if (isAppleCocoaWebKit()) { + return runMacWebKitApp(File::Spec->catfile(productDir(), "MiniBrowser.app", "Contents", "MacOS", "MiniBrowser")); + } elsif (isAppleWinWebKit()) { + my $result; + my $webKitLauncherPath = File::Spec->catfile(executableProductDir(), "MiniBrowser.exe"); + return system { $webKitLauncherPath } $webKitLauncherPath, @ARGV; + } + + return 1; +} + +sub debugMiniBrowser +{ + if (isAppleCocoaWebKit()) { + execMacWebKitAppForDebugging(File::Spec->catfile(productDir(), "MiniBrowser.app", "Contents", "MacOS", "MiniBrowser")); + } + + return 1; +} + +sub runWebKitTestRunner +{ + if (isAppleCocoaWebKit()) { + return runMacWebKitApp(File::Spec->catfile(productDir(), "WebKitTestRunner")); + } + + return 1; +} + +sub debugWebKitTestRunner +{ + if (isAppleCocoaWebKit()) { + execMacWebKitAppForDebugging(File::Spec->catfile(productDir(), "WebKitTestRunner")); + } + + return 1; +} + +sub readRegistryString +{ + my ($valueName) = @_; + chomp(my $string = `regtool --wow32 get "$valueName"`); + return $string; +} + +sub writeRegistryString +{ + my ($valueName, $string) = @_; + + my $error = system "regtool", "--wow32", "set", "-s", $valueName, $string; + + # On Windows Vista/7 with UAC enabled, regtool will fail to modify the registry, but will still + # return a successful exit code. So we double-check here that the value we tried to write to the + # registry was really written. + return !$error && readRegistryString($valueName) eq $string; +} + +sub formatBuildTime($) +{ + my ($buildTime) = @_; + + my $buildHours = int($buildTime / 3600); + my $buildMins = int(($buildTime - $buildHours * 3600) / 60); + my $buildSecs = $buildTime - $buildHours * 3600 - $buildMins * 60; + + if ($buildHours) { + return sprintf("%dh:%02dm:%02ds", $buildHours, $buildMins, $buildSecs); + } + return sprintf("%02dm:%02ds", $buildMins, $buildSecs); +} + +sub runSvnUpdateAndResolveChangeLogs(@) +{ + my @svnOptions = @_; + my $openCommand = "svn update " . join(" ", @svnOptions); + open my $update, "$openCommand |" or die "cannot execute command $openCommand"; + my @conflictedChangeLogs; + while (my $line = <$update>) { + print $line; + $line =~ m/^C\s+(.+?)[\r\n]*$/; + if ($1) { + my $filename = normalizePath($1); + push @conflictedChangeLogs, $filename if basename($filename) eq "ChangeLog"; + } + } + close $update or die; + + if (@conflictedChangeLogs) { + print "Attempting to merge conflicted ChangeLogs.\n"; + my $resolveChangeLogsPath = File::Spec->catfile(sourceDir(), "Tools", "Scripts", "resolve-ChangeLogs"); + (system($resolveChangeLogsPath, "--no-warnings", @conflictedChangeLogs) == 0) + or die "Could not open resolve-ChangeLogs script: $!.\n"; + } +} + +sub runGitUpdate() +{ + # Doing a git fetch first allows setups with svn-remote.svn.fetch = trunk:refs/remotes/origin/master + # to perform the rebase much much faster. + system("git", "fetch"); + if (isGitSVNDirectory(".")) { + system("git", "svn", "rebase") == 0 or die; + } else { + # This will die if branch.$BRANCHNAME.merge isn't set, which is + # almost certainly what we want. + system("git", "pull") == 0 or die; + } +} + +1; |