blob: a6d227948abda93bab0e3ea5ee51f808bef0c290 [file] [log] [blame]
Jeff Gastoncc0993d2019-04-02 18:02:44 -04001#!/bin/bash
2set -e
Jeff Gastoncc0993d2019-04-02 18:02:44 -04003
4scriptName="$(basename $0)"
5
6function usage() {
Jeff Gastona6c665042020-07-22 12:57:33 -04007 echo "NAME"
8 echo " diagnose-build-failure.sh"
Jeff Gastoncc0993d2019-04-02 18:02:44 -04009 echo
Jeff Gastona6c665042020-07-22 12:57:33 -040010 echo "SYNOPSIS"
11 echo " ./development/diagnose-build-failure/diagnose-build-failure.sh [--message <message>] '<tasks>'"
Jeff Gastoncc0993d2019-04-02 18:02:44 -040012 echo
Jeff Gastona6c665042020-07-22 12:57:33 -040013 echo "DESCRIPTION"
14 echo " Attempts to identify why "'`'"./gradlew <tasks>"'`'" fails"
15 echo
16 echo "OPTIONS"
17 echo "--message <message>"
18 echo " Replaces the requirement for "'`'"./gradlew <tasks>"'`'" to fail with the requirement that it produces the given message"
19 echo
20 echo "SAMPLE USAGE"
Jeff Gaston6c1bd592021-04-29 12:53:47 -040021 echo " $0 assembleRelease # or any other arguments you would normally give to ./gradlew"
Jeff Gastoncc0993d2019-04-02 18:02:44 -040022 echo
Jeff Gastona6c665042020-07-22 12:57:33 -040023 echo "OUTPUT"
24 echo " diagnose-build-failure will conclude one of the following:"
Jeff Gastoncc0993d2019-04-02 18:02:44 -040025 echo
26 echo " A) Some state saved in memory by the Gradle daemon is triggering an error"
27 echo " B) Your source files have been changed"
Jeff Gaston61cef332020-12-22 11:23:09 -050028 echo " To (slowly) generate a simpler reproduction case, you can run simplify-build-failure.sh"
Jeff Gastoncc0993d2019-04-02 18:02:44 -040029 echo " C) Some file in the out/ dir is triggering an error"
30 echo " If this happens, $scriptName will identify which file(s) specifically"
31 echo " D) The build is nondeterministic and/or affected by timestamps"
32 echo " E) The build via gradlew actually passes"
33 exit 1
34}
35
Jeff Gastona6c665042020-07-22 12:57:33 -040036expectedMessage=""
37while true; do
38 if [ "$#" -lt 1 ]; then
39 usage
40 fi
41 arg="$1"
42 shift
43 if [ "$arg" == "--message" ]; then
44 expectedMessage="$1"
45 shift
46 continue
47 fi
48 gradleArgs="$arg"
49 break
50done
Jeff Gastoncc0993d2019-04-02 18:02:44 -040051if [ "$gradleArgs" == "" ]; then
52 usage
53fi
Jeff Gastonf1817f72021-06-07 15:28:42 -040054# split Gradle arguments into options and tasks
55gradleOptions=""
56gradleTasks=""
57for arg in $gradleArgs; do
58 if [[ "$arg" == "-*" ]]; then
59 gradleOptions="$gradleOptions $arg"
60 else
61 gradleTasks="$gradleTasks $arg"
62 fi
63done
Jeff Gastoncc0993d2019-04-02 18:02:44 -040064
Jeff Gastona6c665042020-07-22 12:57:33 -040065if [ "$#" -gt 0 ]; then
66 echo "Unrecognized argument: $1"
67 exit 1
68fi
69
Jeff Gaston63234502019-07-09 13:47:31 -040070workingDir="$(pwd)"
71if [ ! -e "$workingDir/gradlew" ]; then
72 echo "Error; ./gradlew does not exist. Must cd to a dir containing a ./gradlew first"
73 # so that this script knows which gradlew to use (in frameworks/support or frameworks/support/ui)
74 exit 1
75fi
76
Jeff Gaston65c35b92021-05-11 12:20:45 -040077# resolve some paths
Jeff Gastoncc0993d2019-04-02 18:02:44 -040078scriptPath="$(cd $(dirname $0) && pwd)"
Jeff Gaston599b9e32020-08-05 18:36:56 -040079vgrep="$scriptPath/impl/vgrep.sh"
Jeff Gastoncc0993d2019-04-02 18:02:44 -040080supportRoot="$(cd $scriptPath/../.. && pwd)"
81checkoutRoot="$(cd $supportRoot/../.. && pwd)"
Jeff Gastona58e3082019-08-05 19:44:26 -040082tempDir="$checkoutRoot/diagnose-build-failure/"
Jeff Gaston65c35b92021-05-11 12:20:45 -040083if [ "$OUT_DIR" != "" ]; then
84 mkdir -p "$OUT_DIR"
85 OUT_DIR="$(cd $OUT_DIR && pwd)"
86fi
87if [ "$DIST_DIR" != "" ]; then
88 mkdir -p "$DIST_DIR"
89 DIST_DIR="$(cd $DIST_DIR && pwd)"
Jeff Gastoncc0993d2019-04-02 18:02:44 -040090fi
91COLOR_WHITE="\e[97m"
92COLOR_GREEN="\e[32m"
93
94function checkStatusRepo() {
95 repo status
96}
97
98function checkStatusGit() {
99 git status
100 git log -1
101}
102
103function checkStatus() {
104 cd "$checkoutRoot"
105 if [ "-e" .repo ]; then
106 checkStatusRepo
107 else
108 checkStatusGit
109 fi
110}
111
Jeff Gastonf1817f72021-06-07 15:28:42 -0400112# echos a shell command for running the build in the current directory
Jeff Gastonec553a32020-09-03 10:55:44 -0400113function getBuildCommand() {
Jeff Gastona6c665042020-07-22 12:57:33 -0400114 if [ "$expectedMessage" == "" ]; then
Jeff Gastona6c665042020-07-22 12:57:33 -0400115 testCommand="$*"
Jeff Gastona6c665042020-07-22 12:57:33 -0400116 else
Jeff Gaston599b9e32020-08-05 18:36:56 -0400117 testCommand="$* 2>&1 | $vgrep '$expectedMessage'"
Jeff Gastona6c665042020-07-22 12:57:33 -0400118 fi
Jeff Gaston599b9e32020-08-05 18:36:56 -0400119 echo "$testCommand"
120}
121
Jeff Gastonf1817f72021-06-07 15:28:42 -0400122# Echos a shell command for testing the state in the current directory
123# Status can be inverted by the '--invert' flag
124# The dir of the state being tested is $testDir
125# The dir of the source code is $workingDir
126function getTestStateCommand() {
127 successStatus=0
128 failureStatus=1
129 if [[ "$1" == "--invert" ]]; then
130 successStatus=1
131 failureStatus=0
132 shift
133 fi
134
135 setupCommand="testDir=\$(pwd)
136$scriptPath/impl/restore-state.sh . $workingDir --move && cd $workingDir
137"
138 buildCommand="$*"
139 cleanupCommand="$scriptPath/impl/backup-state.sh \$testDir $workingDir --move >/dev/null"
140
141 fullFiltererCommand="$setupCommand
Jeff Gaston735b1252021-06-24 14:26:40 -0400142if $buildCommand >/dev/null 2>/dev/null; then
Jeff Gastonf1817f72021-06-07 15:28:42 -0400143 $cleanupCommand
144 exit $successStatus
145else
146 $cleanupCommand
147 exit $failureStatus
148fi"
149
150 echo "$fullFiltererCommand"
151}
152
Jeff Gaston599b9e32020-08-05 18:36:56 -0400153function runBuild() {
154 testCommand="$(getBuildCommand $*)"
Jeff Gaston40660e72020-01-21 16:46:14 -0500155 cd "$workingDir"
Jeff Gaston65c35b92021-05-11 12:20:45 -0400156 echo Running $testCommand
157 if bash -c "$testCommand"; then
Jeff Gastoncc0993d2019-04-02 18:02:44 -0400158 echo -e "$COLOR_WHITE"
159 echo
Jeff Gastona6c665042020-07-22 12:57:33 -0400160 echo '`'$testCommand'`' succeeded
Jeff Gaston599b9e32020-08-05 18:36:56 -0400161 return 0
Jeff Gastoncc0993d2019-04-02 18:02:44 -0400162 else
163 echo -e "$COLOR_WHITE"
164 echo
Jeff Gastona6c665042020-07-22 12:57:33 -0400165 echo '`'$testCommand'`' failed
Jeff Gaston599b9e32020-08-05 18:36:56 -0400166 return 1
Jeff Gastoncc0993d2019-04-02 18:02:44 -0400167 fi
168}
169
170function backupState() {
171 cd "$scriptPath"
172 backupDir="$1"
Jeff Gaston65c35b92021-05-11 12:20:45 -0400173 shift
174 ./impl/backup-state.sh "$backupDir" "$workingDir" "$@"
Jeff Gastoncc0993d2019-04-02 18:02:44 -0400175}
176
177function restoreState() {
178 cd "$scriptPath"
179 backupDir="$1"
Jeff Gaston63234502019-07-09 13:47:31 -0400180 ./impl/restore-state.sh "$backupDir" "$workingDir"
Jeff Gastoncc0993d2019-04-02 18:02:44 -0400181}
182
183function clearState() {
184 restoreState /dev/null
185}
186
187echo
188echo "Making sure that we can reproduce the build failure"
189if runBuild ./gradlew $gradleArgs; then
190 echo
191 echo "This script failed to reproduce the build failure."
192 echo "If the build failure you were observing was in Android Studio, then:"
193 echo ' Were you launching Android Studio by running `./studiow`?'
194 echo " Try asking a team member why Android Studio is failing but gradlew is succeeding"
195 echo "If you previously observed a build failure, then this means one of:"
196 echo " The state of your build is different than when you started your previous build"
197 echo " You could ask a team member if they've seen this error."
198 echo " The build is nondeterministic"
199 echo " If this seems likely to you, then please open a bug."
200 exit 1
201else
202 echo
203 echo "Reproduced build failure"
204fi
205
206echo
207echo "Stopping the Gradle Daemon and rebuilding"
208cd "$supportRoot"
209./gradlew --stop || true
210if runBuild ./gradlew --no-daemon $gradleArgs; then
211 echo
212 echo "The build passed when disabling the Gradle Daemon"
213 echo "This suggests that there is some state saved in the Gradle Daemon that is causing a failure."
214 echo "Unfortunately, this script does not know how to diagnose this further."
215 echo "You could ask a team member if they've seen this error."
216 exit 1
217else
218 echo
219 echo "The build failed even with the Gradle Daemon disabled."
220 echo "This may mean that there is state stored in a file somewhere, triggering the build to fail."
221 echo "We will investigate the possibility of saved state next."
222 echo
Jeff Gastona1280e02021-04-16 16:43:02 -0400223 # We're going to immediately overwrite the user's current state,
224 # so we can simply move the current state into $tempDir/prev rather than copying it
225 backupState "$tempDir/prev" --move
Jeff Gastoncc0993d2019-04-02 18:02:44 -0400226fi
227
228echo
229echo "Checking whether a clean build passes"
230clearState
231backupState "$tempDir/empty"
232successState="$tempDir/empty"
233if runBuild ./gradlew --no-daemon $gradleArgs; then
234 echo
235 echo "The clean build passed, so we can now investigate what cached state is triggering this build to fail."
236 backupState "$tempDir/clean"
237else
238 echo
Jeff Gastona6c665042020-07-22 12:57:33 -0400239 echo "The clean build also reproduced the issue."
240 echo "This may mean that everyone is observing this issue"
Jeff Gastoncc0993d2019-04-02 18:02:44 -0400241 echo "This may mean that something about your checkout is different from others'"
Jeff Gaston216c9702019-05-14 17:44:16 -0400242 echo "You may be interested in running development/simplify-build-failure/simplify-build-failure.sh to identify the minimal set of source files required to reproduce this error"
Jeff Gastoncc0993d2019-04-02 18:02:44 -0400243 echo "Checking the status of your checkout:"
244 checkStatus
245 exit 1
246fi
247
248echo
249echo "Checking whether a second build passes when starting from the output of the first clean build"
250if runBuild ./gradlew --no-daemon $gradleArgs; then
251 echo
252 echo "The next build after the clean build passed, so we can use the output of the first clean build as the successful state to compare against"
253 successState="$tempDir/clean"
254else
255 echo
256 echo "The next build after the clean build failed."
257 echo "Although this is unexpected, we should still be able to diagnose it."
258 echo "This might be slower than normal, though, because it may require us to rebuild more things more often"
259fi
260
261echo
262echo "Next we'll double-check that after restoring the failing state, the build fails"
263restoreState "$tempDir/prev"
264if runBuild ./gradlew --no-daemon $gradleArgs; then
265 echo
266 echo "After restoring the saved state, the build passed."
267 echo "This might mean that there is additional state being saved somewhere else that this script does not know about"
268 echo "This might mean that the success or failure status of the build is dependent on timestamps."
269 echo "This might mean that the build is nondeterministic."
270 echo "Unfortunately, this script does not know how to diagnose this further."
271 echo "You could:"
272 echo " Ask a team member if they know where the state may be stored"
273 echo " Ask a team member if they recognize the build error"
274 exit 1
275else
276 echo
277 echo "After restoring the saved state, the build failed. This confirms that this script is successfully saving and restoring the relevant state"
278fi
279
Jeff Gastonf1817f72021-06-07 15:28:42 -0400280# Ask diff-filterer.py to run a binary search to determine the minimum set of tasks that must be passed to reproduce this error
281# (it's possible that the caller passed more tasks than needed, particularly if the caller is a script)
282requiredTasksDir="$tempDir/requiredTasks"
283function determineMinimalSetOfRequiredTasks() {
284 echo Calculating the list of tasks to run
285 allTasksLog="$tempDir/tasks.log"
286 restoreState "$successState"
287 rm -f "$allTasksLog"
288 bash -c "cd $workingDir && ./gradlew --no-daemon --dry-run $gradleArgs > $allTasksLog 2>&1" || true
289
290 # process output and split into files
291 taskListFile="$tempDir/tasks.list"
292 cat "$allTasksLog" | grep '^:' | sed 's/ .*//' > "$taskListFile"
293 requiredTasksWork="$tempDir/requiredTasksWork"
294 rm -rf "$requiredTasksWork"
295 cp -r "$tempDir/prev" "$requiredTasksWork"
296 mkdir -p "$requiredTasksWork/tasks"
297 bash -c "cd $requiredTasksWork/tasks && split -l 1 '$taskListFile'"
298
299 rm -rf "$requiredTasksDir"
300 # Build the command for passing to diff-filterer.
301 # We call xargs because the full set of tasks might be too long for the shell, and xargs will
302 # split into multiple gradlew invocations if needed.
303 # We also cd into the tasks/ dir before calling 'cat' to avoid reaching its argument length limit.
304 # note that the variable "$testDir" gets set by $getTestStateCommand
305 buildCommand="$(getBuildCommand "rm -f log && (cd \$testDir/tasks && cat *) | xargs --no-run-if-empty ./gradlew $gradleOptions")"
306
307 # command for moving state, running build, and moving state back
308 fullFiltererCommand="$(getTestStateCommand --invert $buildCommand)"
309
310 if $supportRoot/development/file-utils/diff-filterer.py --work-path "$tempDir" "$requiredTasksWork" "$tempDir/prev" "$fullFiltererCommand"; then
311 echo diff-filterer successfully identified a minimal set of required tasks. Saving into $requiredTasksDir
312 cp -r "$tempDir/bestResults/tasks" "$requiredTasksDir"
313 else
314 echo diff-filterer was unable to identify a minimal set of tasks required to reproduce the error
315 exit 1
316 fi
317}
318determineMinimalSetOfRequiredTasks
319# update variables
320gradleTasks="$(cat $requiredTasksDir/*)"
321gradleArgs="$gradleOptions $gradleTasks"
322
Jeff Gastoncc0993d2019-04-02 18:02:44 -0400323# Now ask diff-filterer.py to run a binary search to determine what the relevant differences are between "$tempDir/prev" and "$tempDir/clean"
324echo
325echo "Binary-searching the contents of the two output directories until the relevant differences are identified."
326echo "This may take a while."
327echo
Jeff Gastonf1817f72021-06-07 15:28:42 -0400328
329# command for running a build
Jeff Gaston5725bdc2021-05-13 17:53:20 -0400330buildCommand="$(getBuildCommand "./gradlew --no-daemon $gradleArgs")"
Jeff Gastonf1817f72021-06-07 15:28:42 -0400331# command for moving state, running build, and moving state back
332fullFiltererCommand="$(getTestStateCommand $buildCommand)"
333
Jeff Gaston5725bdc2021-05-13 17:53:20 -0400334if $supportRoot/development/file-utils/diff-filterer.py --assume-input-states-are-correct --work-path $tempDir $successState $tempDir/prev "$fullFiltererCommand"; then
Jeff Gastoncc0993d2019-04-02 18:02:44 -0400335 echo
336 echo "There should be something wrong with the above file state"
337 echo "Hopefully the output from diff-filterer.py above is enough information for you to figure out what is wrong"
338 echo "If not, you could ask a team member about your original error message and see if they have any ideas"
339else
340 echo
341 echo "Something went wrong running diff-filterer.py"
342 echo "Maybe that means the build is nondeterministic"
343 echo "Maybe that means that there's something wrong with this script ($0)"
344fi