diff --git a/jenkins/compare.sh b/jenkins/compare.sh new file mode 100755 index 000000000000..bd54d5835e0c --- /dev/null +++ b/jenkins/compare.sh @@ -0,0 +1,10 @@ +#!/bin/bash -e + +cd $WORKSPACE +./tools/compare-commits.sh --base=origin/pr/$ghprbPullId/merge^1 + +mkdir -p jenkins-results/apicomparison + +cp -R tools/comparison/apidiff/diff jenkins-results/apicomparison/ +cp tools/comparison/apidiff/*.html jenkins-results/apicomparison/ +cp -R tools/comparison/generator-diff jenkins-results/generator-diff diff --git a/tools/.gitignore b/tools/.gitignore new file mode 100644 index 000000000000..fb17c93452d6 --- /dev/null +++ b/tools/.gitignore @@ -0,0 +1,2 @@ +comparison + diff --git a/tools/compare-commits.sh b/tools/compare-commits.sh new file mode 100755 index 000000000000..409913c28fcc --- /dev/null +++ b/tools/compare-commits.sh @@ -0,0 +1,218 @@ +#!/bin/bash -e + +BASE_HASH= +COMP_HASH=HEAD +WORKING_DIR= + +WHITE=$(tput setaf 7 2>/dev/null || true) +BLUE=$(tput setaf 6 2>/dev/null || true) +RED=$(tput setaf 9 2>/dev/null || true) +CLEAR=$(tput sgr0 2>/dev/null || true) + +# Clone files on High Sierra, instead of copying them. Much faster. + +CP="cp" +OSVERSION=${OSTYPE:6} +if [[ $OSVERSION -ge 17 ]]; then + CP="cp -c" +fi + +function show_help () +{ + echo "$(basename "$0"): Compare the managed API and generate a diff for the generated code between the currently built assemblies and a specific hash." + echo "Usage is: $(basename "$0") --base=[TREEISH] [options]" + echo " -h, -?, --help Displays the help" + echo " -b, --base=[TREEISH] The treeish to compare the currently built assemblies against." + #not quite implemented yet# echo " -c, --compare=[TREEISH] Optional, if specified use this hash to build the 'after' assemblies for the comparison." + echo "" + printf "${BLUE} WARNING: This tool will temporarily change the current HEAD of your git checkout.${CLEAR}\\n" + printf "${BLUE} WARNING: The tool will try to restore the current HEAD when done (or if cancelled), but this is not guaranteed.${CLEAR}\\n" + echo "" +} + +ORIGINAL_ARGS=("$@") +while ! test -z "$1"; do + case "$1" in + --help|-\?|-h) + show_help + exit 0 + ;; + --base=*|-b=*) + BASE_HASH="${1#*=}" + shift + ;; + --base|-b) + BASE_HASH="$2" + shift 2 + ;; + --compare=*|-c=*) + COMP_HASH="${1#*=}" + shift + ;; + --compare|-c) + COMP_HASH="$2" + shift 2 + ;; + --impl-working-dir=*) + WORKING_DIR="${1#*=}" + shift + ;; + *) + echo "Unknown argument: $1" + exit 1 + ;; + esac +done + +if test -z "$BASE_HASH"; then + echo "${RED}It's required to specify the hash to compare against (--base=HASH).${CLEAR}" + exit 1 +fi + +ROOT_DIR=$(git rev-parse --show-toplevel) + +# We'll checkout another hash, which may not have this script, and executing a script that is deleted +# sounds like a bad idea. So copy the scripts to /tmp and execute it from there +if test -z "$WORKING_DIR"; then + $CP "$ROOT_DIR/tools/compare-commits.sh" "$ROOT_DIR/tools/diff-to-html" "$TMPDIR/" + exec "$TMPDIR/$(basename "$0")" "${ORIGINAL_ARGS[@]}" "--impl-working-dir=$(pwd)" + exit $? +fi +cd "$WORKING_DIR" + +# Go to the root directory of the git repo, so that we don't run into any surprises with paths. +# Also make ROOT_DIR an absolute path. +cd "$ROOT_DIR" +ROOT_DIR=$(pwd) + +# Only show colors locally. The normal "has-controlling-terminal" doesn't work, because +# we always capture the output to indent it (thus the git processes never have a controlling +# terminal) +if test -z "$BUILD_REVISION"; then + GIT_COLOR=--color=always + GIT_COLOR_P="-c color.status=always" +fi + +if [ -n "$(git status --porcelain --ignore-submodule)" ]; then + echo "${RED}Working directory isn't clean:${CLEAR}" + git $GIT_COLOR_P status --ignore-submodule | sed 's/^/ /' + exit 1 +fi + +echo "Comparing the changes between $WHITE$BASE_HASH$CLEAR and $WHITE$COMP_HASH$CLEAR:" +git log "$BASE_HASH..$COMP_HASH" --oneline $GIT_COLOR | sed 's/^/ /' + +# Resolve any treeish hash value (for instance HEAD^4) to the unique (MD5) hash +COMP_HASH=$(git log -1 --pretty=%H "$COMP_HASH") +BASE_HASH=$(git log -1 --pretty=%H "$BASE_HASH") + +# Save the current branch/hash +CURRENT_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || true) +CURRENT_HASH=$(git log -1 --pretty=%H) + +GENERATOR_DIFF_FILE= +APIDIFF_FILE= + +function upon_exit () +{ + if test -z "$CURRENT_BRANCH"; then + echo "Restoring the previous hash ${BLUE}${CURRENT_HASH}${CLEAR} (there was no previous branch; probably because HEAD was detached)" + git checkout --force "$CURRENT_HASH" + echo "Previous hash restored successfully." + else + echo "Restoring the previous branch ${BLUE}$CURRENT_BRANCH${CLEAR}..." + git checkout --quiet --force "$CURRENT_BRANCH" + git reset --hard "$CURRENT_HASH" + echo "Previous branch restored successfully." + fi + + if ! test -z "$GENERATOR_DIFF_FILE"; then + echo "Generator diff: $GENERATOR_DIFF_FILE" + fi + if ! test -z "$APIDIFF_FILE"; then + echo "API diff: $APIDIFF_FILE" + fi +} + +trap upon_exit EXIT + +OUTPUT_SUBDIR=tools/comparison +OUTPUT_DIR=$ROOT_DIR/$OUTPUT_SUBDIR + +rm -Rf "$OUTPUT_DIR" + +# Create fake destination directories in $OUTPUT_DIR +# We will build in src/ setting DESTDIR to these destination directories, but the +# build in src/ depends on a few files installed from builds/, so copy those files +# from the normal destination directories. +echo "${BLUE}Preparing temporary output directory...${CLEAR}" +mkdir -p "$OUTPUT_DIR/_ios-build/Library/Frameworks/Xamarin.iOS.framework/Versions/git/lib/mono" +mkdir -p "$OUTPUT_DIR/_mac-build/Library/Frameworks/Xamarin.Mac.framework/Versions/git/lib/mono" + +ln -s git "$OUTPUT_DIR/_ios-build/Library/Frameworks/Xamarin.iOS.framework/Versions/Current" +ln -s git "$OUTPUT_DIR/_mac-build/Library/Frameworks/Xamarin.Mac.framework/Versions/Current" + +for dir in 2.1 Xamarin.iOS Xamarin.TVOS Xamarin.WatchOS; do + $CP -R "$ROOT_DIR/_ios-build/Library/Frameworks/Xamarin.iOS.framework/Versions/git/lib/mono/$dir" "$OUTPUT_DIR/_ios-build/Library/Frameworks/Xamarin.iOS.framework/Versions/git/lib/mono" +done + +for dir in Xamarin.Mac 4.5; do + $CP -R "$ROOT_DIR/_mac-build/Library/Frameworks/Xamarin.Mac.framework/Versions/git/lib/mono/$dir" "$OUTPUT_DIR/_mac-build/Library/Frameworks/Xamarin.Mac.framework/Versions/git/lib/mono" +done + +touch "$OUTPUT_DIR/stamp" + +if test -z "$CURRENT_BRANCH"; then + echo "${BLUE}Current hash: ${WHITE}$(git log -1 --pretty="%h: %s")${BLUE} (detached)${CLEAR}" +else + echo "${BLUE}Current branch: ${WHITE}$CURRENT_BRANCH${BLUE} (${WHITE}$(git log -1 --pretty="%h: %s")${BLUE})${CLEAR}" +fi +echo "${BLUE}Checking out ${WHITE}$(git log -1 --pretty="%h: %s" "$BASE_HASH")${CLEAR}...${CLEAR}" +git checkout --quiet --force --detach "$BASE_HASH" + +echo "${BLUE}Building src/...${CLEAR}" +make -C "$ROOT_DIR/src" BUILD_DIR=../tools/comparison/build "IOS_DESTDIR=$OUTPUT_DIR/_ios-build" "MAC_DESTDIR=$OUTPUT_DIR/_mac-build" -j8 + +# +# API diff +# + +# Calculate apidiff references according to the temporary build +echo "${BLUE}Updating apidiff references...${CLEAR}" +make update-refs -C "$ROOT_DIR/tools/apidiff" -j8 APIDIFF_DIR="$OUTPUT_DIR/apidiff" IOS_DESTDIR="$OUTPUT_DIR/_ios-build" MAC_DESTDIR="$OUTPUT_DIR/_mac-build" + +# Now compare the current build against those references +echo "${BLUE}Running apidiff...${CLEAR}" +APIDIFF_FILE=$OUTPUT_DIR/apidiff/api-diff.html +make all-local -C "$ROOT_DIR/tools/apidiff" -j8 APIDIFF_DIR="$OUTPUT_DIR/apidiff" + +# +# Generator diff +# + +# We make a copy of the generated source code to compare against, +# so that we can remove files we don't want to compare without +# affecting that build. +$CP -R "$ROOT_DIR/src/build" "$OUTPUT_DIR/build-new" +cd "$OUTPUT_DIR" +find build build-new '(' -name '*.dll' -or -name '*.mdb' -or -name '*.pdb' -or -name 'generated-sources' -or -name 'generated_sources' -or -name '*.exe' ')' -delete +mkdir -p "$OUTPUT_DIR/generator-diff" +GENERATOR_DIFF_FILE="$OUTPUT_DIR/generator-diff/index.html" +git diff --no-index build build-new > "$OUTPUT_DIR/generator-diff/generator.diff" || true +"$TMPDIR/diff-to-html" "$OUTPUT_DIR/generator-diff/generator.diff" "$GENERATOR_DIFF_FILE" + +# Check if any files in the normal output paths were modified (there should be none) +MODIFIED_FILES=$(find \ + "$ROOT_DIR/_ios-build" \ + "$ROOT_DIR/_mac-build" \ + "$ROOT_DIR/src/build" \ + "$ROOT_DIR/tools/apidiff" \ + -newer "$OUTPUT_DIR/stamp") + +if test -n "$MODIFIED_FILES"; then + # If this list files, it means something's wrong with the build process + # (the logic to build/work in a different directory is incomplete/broken) + echo "${RED}The following files were modified, and they shouldn't have been:${CLEAR}" + echo "$MODIFIED_FILES" | sed 's/^/ /' + exit 1 +fi diff --git a/tools/diff-to-html b/tools/diff-to-html new file mode 100755 index 000000000000..5020d58138a3 --- /dev/null +++ b/tools/diff-to-html @@ -0,0 +1,199 @@ +#!/usr/bin/env /Library/Frameworks/Mono.framework/Commands/csharp + +using System.IO; +using System.Reflection; +using System.Text; +using System.Web; + +LoadAssembly ("System.Web.dll"); + +Func ParseDiff = (string diff) => +{ + StringBuilder result = new StringBuilder (diff.Length); + string line; + int old_ln = 0, new_ln = 0; + int old_d = 0, new_d = 0; + bool started = false; + result.Append ("
"); + using (StringReader reader = new StringReader (diff)) { + while ((line = reader.ReadLine ()) != null) { + if (line.StartsWith ("diff --git")) { + // New file + if (started) + result.AppendLine (""); + started = true; + result.AppendLine (""); + var fn = line.Substring (10).Trim ().Split (' ') [0]; + var fn_split = fn.Split ('/').ToList (); + fn_split.RemoveAt (0); + fn_split.RemoveAt (0); + fn = string.Join ("/", fn_split); + + result.AppendFormat ("\n", fn); + } else if (line.StartsWith ("index")) { + // Not sure what this is + result.AppendFormat ("\n", line); + } else if (line.StartsWith ("---") || line.StartsWith ("+++")) { + // Ignore this for now + // style = "background-color: white"; + // result.AppendFormat ("", line, style); + } else if (line.StartsWith ("@@")) { + // line numbers + string [] nl = line.Replace ("@@", "").Trim ().Split (' '); + var oldc = nl [0].IndexOf (','); + var newc = nl [1].IndexOf (','); + old_ln = int.Parse (nl [0].Substring (1, oldc > 0 ? oldc - 1 : nl [0].Length - 1)); + new_ln = int.Parse (nl [1].Substring (1, newc > 0 ? newc - 1 : nl [1].Length - 1)); + result.AppendFormat ("\n", line); + } else { + string cl; + if (line.StartsWith ("-")) { + cl = "diff_view_removed_line"; + old_d = 1; + new_d = 0; + } else if (line.StartsWith ("+")) { + cl = "diff_view_added_line"; + old_d = 0; + new_d = 1; + } else { + cl = "diff_view_normal_line"; + old_d = 1; + new_d = 1; + } + result.AppendFormat ("\n", + HttpUtility.HtmlEncode (line), cl, old_d == 0 ? string.Empty : old_ln.ToString (), new_d == 0 ? string.Empty : new_ln.ToString ()); + old_ln += old_d; + new_ln += new_d; + } + } + } + result.AppendLine ("
{0}
 
 
{0}
{0}
 
 
{0}
{2}
{3}
{0}
"); + result.AppendLine ("
"); + return result.ToString (); +}; + +var args = Environment.GetCommandLineArgs (); +if (args.Length != 4 /* 2 default + 2 real */) { + // first arg is "/Library/Frameworks/Mono.framework/Versions/4.8.0/lib/mono/4.5/csharp.exe" + // second arg the script itself + // then comes the ones we care about + Console.WriteLine ("Need 2 arguments, got {0}", args.Length - 2); + Environment.Exit (1); + return; +} + +var input = args [2]; +var output = args [3]; + +var diff = ParseDiff (File.ReadAllText (input)); +diff = @" + + + + +Raw diff
+" + diff + @" + +"; +File.WriteAllText (output, diff); + +Environment.Exit (0); \ No newline at end of file