summaryrefslogtreecommitdiff
path: root/Tools/Scripts
diff options
context:
space:
mode:
Diffstat (limited to 'Tools/Scripts')
-rw-r--r--Tools/Scripts/VCSUtils.pm2433
-rwxr-xr-xTools/Scripts/run-gtk-tests494
-rwxr-xr-xTools/Scripts/webkit-build-directory84
-rwxr-xr-xTools/Scripts/webkitdirs.pm2613
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
+ &currentSVNRevision
+ &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;