Initial work to enable ASM diff generation in CI (#20366)
[platform/upstream/coreclr.git] / tests / scripts / run-pmi-diffs.py
1 #!/usr/bin/env python
2 #
3 # Licensed to the .NET Foundation under one or more agreements.
4 # The .NET Foundation licenses this file to you under the MIT license.
5 # See the LICENSE file in the project root for more information.
6 #
7 ##########################################################################
8 ##########################################################################
9 #
10 # Module: run-pmi-diffs.py
11 #
12 # Notes:
13 #
14 # Script to automate running PMI diffs on a pull request
15 #
16 ##########################################################################
17 ##########################################################################
18
19 import argparse
20 import distutils.dir_util
21 import os
22 import re
23 import shutil
24 import subprocess
25 import urllib
26 import urllib2
27 import sys
28 import tarfile
29 import zipfile
30
31 ##########################################################################
32 # Globals
33 ##########################################################################
34
35 testing = False
36
37 Coreclr_url = 'https://github.com/dotnet/coreclr.git'
38 Jitutils_url = 'https://github.com/dotnet/jitutils.git'
39
40 # This should be factored out of build.sh
41 Unix_name_map = {
42     'Linux': 'Linux',
43     'Darwin': 'OSX',
44     'FreeBSD': 'FreeBSD',
45     'OpenBSD': 'OpenBSD',
46     'NetBSD': 'NetBSD',
47     'SunOS': 'SunOS'
48 }
49
50 Is_windows = (os.name == 'nt')
51 clr_os = 'Windows_NT' if Is_windows else Unix_name_map[os.uname()[0]]
52
53 ##########################################################################
54 # Delete protocol
55 ##########################################################################
56
57 def del_rw(action, name, exc):
58     os.chmod(name, 0651)
59     os.remove(name)
60
61 ##########################################################################
62 # Argument Parser
63 ##########################################################################
64
65 description = 'Tool to generate JIT assembly diffs from the CoreCLR repo'
66
67 parser = argparse.ArgumentParser(description=description)
68
69 # base_root is normally expected to be None, in which case we'll clone the
70 # coreclr tree and build it. If base_root is passed, we'll use it, and not
71 # clone or build the base.
72
73 # TODO: need to fix parser so -skip_baseline_build / -skip_diffs don't take an argument
74
75 parser.add_argument('-arch', dest='arch', default='x64')
76 parser.add_argument('-ci_arch', dest='ci_arch', default=None)
77 parser.add_argument('-build_type', dest='build_type', default='Checked')
78 parser.add_argument('-base_root', dest='base_root', default=None)
79 parser.add_argument('-diff_root', dest='diff_root', default=None)
80 parser.add_argument('-scratch_root', dest='scratch_root', default=None)
81 parser.add_argument('-skip_baseline_build', dest='skip_baseline_build', default=False)
82 parser.add_argument('-skip_diffs', dest='skip_diffs', default=False)
83 parser.add_argument('-target_branch', dest='target_branch', default='master')
84 parser.add_argument('-commit_hash', dest='commit_hash', default=None)
85
86 ##########################################################################
87 # Helper Functions
88 ##########################################################################
89
90 def validate_args(args):
91     """ Validate all of the arguments parsed.
92     Args:
93         args (argparser.ArgumentParser): Args parsed by the argument parser.
94     Returns:
95         (arch, ci_arch, build_type, base_root, diff_root, scratch_root, skip_baseline_build, skip_diffs, target_branch, commit_hash)
96             (str, str, str, str, str, str, bool, bool, str, str)
97     Notes:
98     If the arguments are valid then return them all in a tuple. If not, raise
99     an exception stating x argument is incorrect.
100     """
101
102     arch = args.arch
103     ci_arch = args.ci_arch
104     build_type = args.build_type
105     base_root = args.base_root
106     diff_root = args.diff_root
107     scratch_root = args.scratch_root
108     skip_baseline_build = args.skip_baseline_build
109     skip_diffs = args.skip_diffs
110     target_branch = args.target_branch
111     commit_hash = args.commit_hash
112
113     def validate_arg(arg, check):
114         """ Validate an individual arg
115         Args:
116            arg (str|bool): argument to be validated
117            check (lambda: x-> bool): test that returns either True or False
118                                    : based on whether the check passes.
119
120         Returns:
121            is_valid (bool): Is the argument valid?
122         """
123
124         helper = lambda item: item is not None and check(item)
125
126         if not helper(arg):
127             raise Exception('Argument: %s is not valid.' % (arg))
128
129     valid_archs = ['x86', 'x64', 'arm', 'arm64']
130     valid_ci_archs = valid_archs + ['x86_arm_altjit', 'x64_arm64_altjit']
131     valid_build_types = ['Debug', 'Checked', 'Release']
132
133     arch = next((a for a in valid_archs if a.lower() == arch.lower()), arch)
134     build_type = next((b for b in valid_build_types if b.lower() == build_type.lower()), build_type)
135
136     validate_arg(arch, lambda item: item in valid_archs)
137     validate_arg(build_type, lambda item: item in valid_build_types)
138
139     if diff_root is None:
140         diff_root = nth_dirname(os.path.abspath(sys.argv[0]), 3)
141     else:
142         diff_root = os.path.abspath(diff_root)
143         validate_arg(diff_root, lambda item: os.path.isdir(diff_root))
144
145     if scratch_root is None:
146         scratch_root = os.path.join(diff_root, '_')
147     else:
148         scratch_root = os.path.abspath(scratch_root)
149
150     if ci_arch is not None:
151         validate_arg(ci_arch, lambda item: item in valid_ci_archs)
152
153     args = (arch, ci_arch, build_type, base_root, diff_root, scratch_root, skip_baseline_build, skip_diffs, target_branch, commit_hash)
154
155     log('Configuration:')
156     log(' arch: %s' % arch)
157     log(' ci_arch: %s' % ci_arch)
158     log(' build_type: %s' % build_type)
159     log(' base_root: %s' % base_root)
160     log(' diff_root: %s' % diff_root)
161     log(' scratch_root: %s' % scratch_root)
162     log(' skip_baseline_build: %s' % skip_baseline_build)
163     log(' skip_diffs: %s' % skip_diffs)
164     log(' target_branch: %s' % target_branch)
165     log(' commit_hash: %s' % commit_hash)
166
167     return args
168
169 def nth_dirname(path, n):
170     """ Find the Nth parent directory of the given path
171     Args:
172         path (str): path name containing at least N components
173         n (int): num of basenames to remove
174     Returns:
175         outpath (str): path with the last n components removed
176     Notes:
177         If n is 0, path is returned unmodified
178     """
179
180     assert n >= 0
181
182     for i in range(0, n):
183         path = os.path.dirname(path)
184
185     return path
186
187 def log(message):
188     """ Print logging information
189     Args:
190         message (str): message to be printed
191     """
192
193     print '[%s]: %s' % (sys.argv[0], message)
194
195 def copy_files(source_dir, target_dir):
196     """ Copy any files in the source_dir to the target_dir.
197         The copy is not recursive.
198         The directories must already exist.
199     Args:
200         source_dir (str): source directory path
201         target_dir (str): target directory path
202     Returns:
203         Nothing
204     """
205
206     global testing
207     assert os.path.isdir(source_dir)
208     assert os.path.isdir(target_dir)
209
210     for source_filename in os.listdir(source_dir):
211         source_pathname = os.path.join(source_dir, source_filename)
212         if os.path.isfile(source_pathname):
213             target_pathname = os.path.join(target_dir, source_filename)
214             log('Copy: %s => %s' % (source_pathname, target_pathname))
215             if not testing:
216                 shutil.copy2(source_pathname, target_pathname)
217
218 ##########################################################################
219 # Do baseline build:
220 # 1. determine appropriate commit,
221 # 2. clone coreclr,
222 # 3. do build
223 ##########################################################################
224
225 def baseline_build():
226
227     if not testing:
228         if os.path.isdir(baseCoreClrPath):
229             log('Removing existing tree: %s' % baseCoreClrPath)
230             shutil.rmtree(baseCoreClrPath, onerror=del_rw)
231
232     # Find the baseline commit
233
234     # Clone at that commit
235
236     command = 'git clone -b %s --single-branch %s %s' % (
237         target_branch, Coreclr_url, baseCoreClrPath)
238     log(command)
239     returncode = 0 if testing else os.system(command)
240     if returncode != 0:
241         log('ERROR: git clone failed')
242         return 1
243
244     # Change directory to the baseline root
245
246     cwd = os.getcwd()
247     log('[cd] %s' % baseCoreClrPath)
248     if not testing:
249         os.chdir(baseCoreClrPath)
250
251     # Set up for possible docker usage
252
253     scriptPath = '.'
254     buildOpts = ''
255     dockerCmd = ''
256     if not Is_windows and (arch == 'arm' or arch == 'arm64'):
257         # Linux arm and arm64 builds are cross-compilation builds using Docker.
258         if arch == 'arm':
259             dockerFile = 'microsoft/dotnet-buildtools-prereqs:ubuntu-14.04-cross-e435274-20180426002420'
260             dockerOpts = '-e ROOTFS_DIR=/crossrootfs/arm -e CAC_ROOTFS_DIR=/crossrootfs/x86'
261         else:
262             # arch == 'arm64'
263             dockerFile = 'microsoft/dotnet-buildtools-prereqs:ubuntu-16.04-cross-arm64-a3ae44b-20180315221921'
264             dockerOpts = '-e ROOTFS_DIR=/crossrootfs/arm64'
265
266         dockerCmd = 'docker run -i --rm -v %s:%s -w %s %s %s ' % (baseCoreClrPath, baseCoreClrPath, baseCoreClrPath, dockerOpts, dockerFile)
267         buildOpts = 'cross crosscomponent'
268         scriptPath = baseCoreClrPath
269
270     # Build a checked baseline jit 
271
272     if Is_windows:
273         command = 'set __TestIntermediateDir=int&&build.cmd %s checked skiptests skipbuildpackages' % arch
274     else:
275         command = '%s%s/build.sh %s checked skiptests skipbuildpackages %s' % (dockerCmd, scriptPath, arch, buildOpts)
276     log(command)
277     returncode = 0 if testing else os.system(command)
278     if returncode != 0:
279         log('ERROR: build failed')
280         return 1
281
282     # Build the layout (Core_Root) directory
283
284     # For Windows, you need to first do a restore. It's unfortunately complicated. Run:
285     #   run.cmd build -Project="tests\build.proj" -BuildOS=Windows_NT -BuildType=Checked -BuildArch=x64 -BatchRestorePackages
286
287     if Is_windows:
288         command = 'run.cmd build -Project="tests\\build.proj" -BuildOS=Windows_NT -BuildType=%s -BuildArch=%s -BatchRestorePackages' % (build_type, arch)
289         log(command)
290         returncode = 0 if testing else os.system(command)
291         if returncode != 0:
292             log('ERROR: restoring packages failed')
293             return 1
294
295     if Is_windows:
296         command = 'tests\\runtest.cmd %s checked GenerateLayoutOnly' % arch
297     else:
298         command = '%s%s/build-test.sh %s checked generatelayoutonly' % (dockerCmd, scriptPath, arch)
299     log(command)
300     returncode = 0 if testing else os.system(command)
301     if returncode != 0:
302         log('ERROR: generating layout failed')
303         return 1
304
305     # After baseline build, change directory back to where we started
306
307     log('[cd] %s' % cwd)
308     if not testing:
309         os.chdir(cwd)
310
311     return 0
312
313 ##########################################################################
314 # Do PMI diff run:
315 # 1. download dotnet CLI (needed by jitutils)
316 # 2. clone jitutils repo
317 # 3. build jitutils
318 # 4. run PMI asm generation on baseline
319 # 5. run PMI asm generation on diff
320 # 6. run jit-analyze to compare baseline and diff
321 ##########################################################################
322
323 def do_pmi_diffs():
324     global baseCoreClrPath
325
326     # Setup scratch directories. Names are short to avoid path length problems on Windows.
327     dotnetcliPath = os.path.abspath(os.path.join(scratch_root, '_d'))
328     jitutilsPath = os.path.abspath(os.path.join(scratch_root, '_j'))
329     asmRootPath = os.path.abspath(os.path.join(scratch_root, '_asm'))
330
331     dotnet_tool = 'dotnet.exe' if Is_windows else 'dotnet'
332
333     # Make sure the temporary directories do not exist. If they do already, delete them.
334
335     if not testing:
336         # If we can't delete the dotnet tree, it might be because a previous run failed or was
337         # cancelled, and the build servers are still running. Try to stop it if that happens.
338         if os.path.isdir(dotnetcliPath):
339             try:
340                 log('Removing existing tree: %s' % dotnetcliPath)
341                 shutil.rmtree(dotnetcliPath, onerror=del_rw)
342             except OSError:
343                 if os.path.isfile(os.path.join(dotnetcliPath, dotnet_tool)):
344                     log('Failed to remove existing tree; trying to shutdown the dotnet build servers before trying again.')
345
346                     # Looks like the dotnet too is still there; try to run it to shut down the build servers.
347                     temp_env = my_env
348                     temp_env["PATH"] = dotnetcliPath + os.pathsep + my_env["PATH"]
349                     log('Shutting down build servers')
350                     command = ["dotnet", "build-server", "shutdown"]
351                     log('Invoking: %s' % (' '.join(command)))
352                     proc = subprocess.Popen(command, env=temp_env)
353                     output,error = proc.communicate()
354                     returncode = proc.returncode
355                     if returncode != 0:
356                         log('Return code = %s' % returncode)
357
358                     # Try again
359                     log('Trying again to remove existing tree: %s' % dotnetcliPath)
360                     shutil.rmtree(dotnetcliPath, onerror=del_rw)
361                 else:
362                     log('Failed to remove existing tree')
363                     return 1
364
365         if os.path.isdir(jitutilsPath):
366             log('Removing existing tree: %s' % jitutilsPath)
367             shutil.rmtree(jitutilsPath, onerror=del_rw)
368         if os.path.isdir(asmRootPath):
369             log('Removing existing tree: %s' % asmRootPath)
370             shutil.rmtree(asmRootPath, onerror=del_rw)
371
372         try:
373             os.makedirs(dotnetcliPath)
374             os.makedirs(jitutilsPath)
375             os.makedirs(asmRootPath)
376         except OSError:
377             if not os.path.isdir(dotnetcliPath):
378                 log('ERROR: cannot create CLI install directory %s' % dotnetcliPath)
379                 return 1
380             if not os.path.isdir(jitutilsPath):
381                 log('ERROR: cannot create jitutils install directory %s' % jitutilsPath)
382                 return 1
383             if not os.path.isdir(asmRootPath):
384                 log('ERROR: cannot create diff directory %s' % asmRootPath)
385                 return 1
386
387     log('dotnet CLI install directory: %s' % dotnetcliPath)
388     log('jitutils install directory: %s' % jitutilsPath)
389     log('asm directory: %s' % asmRootPath)
390
391     # Download .NET CLI
392
393     log('Downloading .Net CLI')
394
395     dotnetcliUrl = ""
396     dotnetcliFilename = ""
397
398     if clr_os == 'Linux':
399         dotnetcliUrl = "https://dotnetcli.azureedge.net/dotnet/Sdk/2.1.402/dotnet-sdk-2.1.402-linux-x64.tar.gz"
400         dotnetcliFilename = os.path.join(dotnetcliPath, 'dotnetcli-jitutils.tar.gz')
401     elif clr_os == 'OSX':
402         dotnetcliUrl = "https://dotnetcli.azureedge.net/dotnet/Sdk/2.1.402/dotnet-sdk-2.1.402-osx-x64.tar.gz"
403         dotnetcliFilename = os.path.join(dotnetcliPath, 'dotnetcli-jitutils.tar.gz')
404     elif clr_os == 'Windows_NT':
405         dotnetcliUrl = "https://dotnetcli.azureedge.net/dotnet/Sdk/2.1.402/dotnet-sdk-2.1.402-win-x64.zip"
406         dotnetcliFilename = os.path.join(dotnetcliPath, 'dotnetcli-jitutils.zip')
407     else:
408         log('ERROR: unknown or unsupported OS %s' % os)
409         return 1
410
411     log('Downloading: %s => %s' % (dotnetcliUrl, dotnetcliFilename))
412
413     if not testing:
414         response = urllib2.urlopen(dotnetcliUrl)
415         request_url = response.geturl()
416         testfile = urllib.URLopener()
417         testfile.retrieve(request_url, dotnetcliFilename)
418
419         if not os.path.isfile(dotnetcliFilename):
420             log('ERROR: Did not download .Net CLI')
421             return 1
422
423     # Install .Net CLI
424
425     log('Unpacking .Net CLI')
426
427     if not testing:
428         if Is_windows:
429             with zipfile.ZipFile(dotnetcliFilename, "r") as z:
430                 z.extractall(dotnetcliPath)
431         else:
432             tar = tarfile.open(dotnetcliFilename)
433             tar.extractall(dotnetcliPath)
434             tar.close()
435
436         if not os.path.isfile(os.path.join(dotnetcliPath, dotnet_tool)):
437             log('ERROR: did not extract .Net CLI from download')
438             return 1
439
440     # Add dotnet CLI to PATH we'll use to spawn processes.
441
442     log('Add %s to my PATH' % dotnetcliPath)
443     my_env["PATH"] = dotnetcliPath + os.pathsep + my_env["PATH"]
444
445     # Clone jitutils
446
447     command = 'git clone -b master --single-branch %s %s' % (Jitutils_url, jitutilsPath)
448     log(command)
449     returncode = 0 if testing else os.system(command)
450     if returncode != 0:
451         log('ERROR: cannot clone jitutils');
452         return 1
453
454     #
455     # Build jitutils, including "dotnet restore"
456     #
457
458     # Change directory to the jitutils root
459
460     cwd = os.getcwd()
461     log('[cd] %s' % jitutilsPath)
462     if not testing:
463         os.chdir(jitutilsPath)
464
465     # Do "dotnet restore"
466
467     command = ["dotnet", "restore"]
468     log('Invoking: %s' % (' '.join(command)))
469     if not testing:
470         proc = subprocess.Popen(command, env=my_env)
471         output,error = proc.communicate()
472         returncode = proc.returncode
473         if returncode != 0:
474             log('Return code = %s' % returncode)
475
476     # Do build
477
478     command = ['build.cmd' if Is_windows else 'build.sh', '-p']
479     log('Invoking: %s' % (' '.join(command)))
480     if not testing:
481         proc = subprocess.Popen(command, env=my_env)
482         output,error = proc.communicate()
483         returncode = proc.returncode
484         if returncode != 0:
485             log('Return code = %s' % returncode)
486             log('ERROR: jitutils build failed')
487             return 1
488
489     jitutilsBin = os.path.join(jitutilsPath, "bin")
490
491     if not testing and not os.path.isdir(jitutilsBin):
492         log("ERROR: jitutils not correctly built")
493         return 1
494
495     jitDiffPath = os.path.join(jitutilsBin, "jit-diff.dll")
496     if not testing and not os.path.isfile(jitDiffPath):
497         log("ERROR: jit-diff.dll not built")
498         return 1
499
500     jitAnalyzePath = os.path.join(jitutilsBin, "jit-analyze.dll")
501     if not testing and not os.path.isfile(jitAnalyzePath):
502         log("ERROR: jit-analyze.dll not built")
503         return 1
504
505     # Add jitutils bin to path for spawned processes
506
507     log('Add %s to my PATH' % jitutilsBin)
508     my_env["PATH"] = jitutilsBin + os.pathsep + my_env["PATH"]
509
510     # After baseline build, change directory back to where we started
511
512     log('[cd] %s' % cwd)
513     if not testing:
514         os.chdir(cwd)
515
516     #
517     # Run PMI asm diffs
518     #
519
520     # We continue through many failures, to get as much asm generated as possible. But make sure we return
521     # a failure code if there are any failures.
522
523     result = 0
524
525     # First, generate the diffs
526
527     # Invoke command like:
528     #   dotnet c:\gh\jitutils\bin\jit-diff.dll diff --pmi --corelib --diff --diff_root f:\gh\coreclr10 --arch x64 --build Checked --tag diff --output f:\output\diffs
529     #
530     # TODO: Fix issues when invoking this from a script:
531     # 1. There is no way to turn off the progress output
532     # 2. Make it easier to specify the exact directory you want output to go to?
533     # 3. run base and diff with a single command?
534     # 4. put base and diff in saner directory names.
535
536     altjit_args = []
537     if ci_arch is not None and (ci_arch == 'x86_arm_altjit' or ci_arch == 'x64_arm64_altjit'):
538         altjit_args = ["--altjit", "protononjit.dll"]
539
540     # Over which set of assemblies should we generate asm?
541     # TODO: parameterize this
542     asm_source_args = ["--corelib"]
543     # asm_source_args = ["--frameworks"]
544
545     command = ["dotnet", jitDiffPath, "diff", "--pmi", "--diff", "--diff_root", diff_root, "--arch", arch, "--build", build_type, "--tag", "diff", "--output", asmRootPath] + asm_source_args + altjit_args
546     log('Invoking: %s' % (' '.join(command)))
547     if not testing:
548         proc = subprocess.Popen(command, env=my_env)
549         output,error = proc.communicate()
550         returncode = proc.returncode
551         if returncode != 0:
552             log('Return code = %s' % returncode)
553             result = 1
554
555     # Did we get any diffs?
556
557     diffOutputDir = os.path.join(asmRootPath, "diff", "diff")
558     if not testing and not os.path.isdir(diffOutputDir):
559         log("ERROR: diff asm not generated")
560         return 1
561
562     # Next, generate the baseline asm
563
564     command = ["dotnet", jitDiffPath, "diff", "--pmi", "--base", "--base_root", baseCoreClrPath, "--arch", arch, "--build", build_type, "--tag", "base", "--output", asmRootPath] + asm_source_args + altjit_args
565     log('Invoking: %s' % (' '.join(command)))
566     if not testing:
567         proc = subprocess.Popen(command, env=my_env)
568         output,error = proc.communicate()
569         returncode = proc.returncode
570         if returncode != 0:
571             log('Return code = %s' % returncode)
572             result = 1
573
574     # Did we get any diffs?
575
576     baseOutputDir = os.path.join(asmRootPath, "base", "base")
577     if not testing and not os.path.isdir(baseOutputDir):
578         log("ERROR: base asm not generated")
579         return 1
580
581     # Do the jit-analyze comparison:
582     #   dotnet c:\gh\jitutils\bin\jit-analyze.dll --base f:\output\diffs\base\diff --recursive --diff f:\output\diffs\diff\diff
583
584     command = ["dotnet", jitAnalyzePath, "--base", baseOutputDir, "--diff", diffOutputDir]
585     log('Invoking: %s' % (' '.join(command)))
586     if not testing:
587         proc = subprocess.Popen(command, env=my_env)
588         output,error = proc.communicate()
589         returncode = proc.returncode
590         if returncode != 0:
591             log('Return code = %s' % returncode)
592             log('Compare: %s %s' % (baseOutputDir, diffOutputDir))
593
594     # Shutdown the dotnet build servers before cleaning things up
595     # TODO: make this shutdown happen anytime after we've run any 'dotnet' commands. I.e., try/finally style.
596
597     log('Shutting down build servers')
598     command = ["dotnet", "build-server", "shutdown"]
599     log('Invoking: %s' % (' '.join(command)))
600     if not testing:
601         proc = subprocess.Popen(command, env=my_env)
602         output,error = proc.communicate()
603         returncode = proc.returncode
604         if returncode != 0:
605             log('Return code = %s' % returncode)
606
607     return result
608
609 ##########################################################################
610 # Main
611 ##########################################################################
612
613 def main(args):
614
615     global arch, ci_arch, build_type, base_root, diff_root, scratch_root, skip_baseline_build, skip_diffs, target_branch, commit_hash
616     global my_env
617     global base_layout_root
618     global diff_layout_root
619     global baseCoreClrPath
620     global testing
621
622     arch, ci_arch, build_type, base_root, diff_root, scratch_root, skip_baseline_build, skip_diffs, target_branch, commit_hash = validate_args(args)
623
624     my_env = os.environ
625
626     if not testing and not os.path.isdir(diff_root):
627        log('ERROR: root directory for coreclr diff tree not found: %s' % diff_root)
628        return 1
629
630     # Check the diff layout directory before going too far.
631
632     diff_layout_root = os.path.join(diff_root,
633                                     'bin',
634                                     'tests',
635                                     '%s.%s.%s' % (clr_os, arch, build_type),
636                                     'Tests',
637                                     'Core_Root')
638
639     if not testing and not os.path.isdir(diff_layout_root):
640        log('ERROR: diff test overlay not found or is not a directory: %s' % diff_layout_root)
641        return 1
642
643     # Create the scratch root directory
644
645     if not testing:
646         try:
647             os.makedirs(scratch_root)
648         except OSError:
649             if not os.path.isdir(scratch_root):
650                 log('ERROR: cannot create scratch directory %s' % scratch_root)
651                 return 1
652
653     # Set up baseline root directory. If one is passed to us, we use it. Otherwise, we create
654     # a temporary directory.
655
656     if base_root is None:
657         # Setup scratch directories. Names are short to avoid path length problems on Windows.
658         # No need to create this directory now, as the "git clone" will do it later.
659         baseCoreClrPath = os.path.abspath(os.path.join(scratch_root, '_c'))
660     else:
661         baseCoreClrPath = os.path.abspath(base_root)
662         if not testing and not os.path.isdir(baseCoreClrPath):
663            log('ERROR: base root directory not found or is not a directory: %s' % baseCoreClrPath)
664            return 1
665
666     # Do the baseline build, if needed
667
668     if not skip_baseline_build and base_root is None:
669         returncode = baseline_build()
670         if returncode != 0:
671             return 1
672
673     # Check that the baseline root directory was created.
674
675     base_layout_root = os.path.join(baseCoreClrPath,
676                                     'bin',
677                                     'tests',
678                                     '%s.%s.%s' % (clr_os, arch, build_type),
679                                     'Tests',
680                                     'Core_Root')
681
682     if not testing and not os.path.isdir(base_layout_root):
683        log('ERROR: baseline test overlay not found or is not a directory: %s' % base_layout_root)
684        return 1
685
686     # Do the diff run, if needed
687
688     if not skip_diffs:
689         returncode = do_pmi_diffs()
690         if returncode != 0:
691             return 1
692
693     return 0
694
695
696 ##########################################################################
697 # setup for Main
698 ##########################################################################
699
700 if __name__ == '__main__':
701     Args = parser.parse_args(sys.argv[1:])
702     return_code = main(Args)
703     log('Exit code: %s' % return_code)
704     sys.exit(return_code)