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.
7 ##########################################################################
8 ##########################################################################
10 # Module: run-pmi-diffs.py
14 # Script to automate running PMI diffs on a pull request
16 ##########################################################################
17 ##########################################################################
20 import distutils.dir_util
31 ##########################################################################
33 ##########################################################################
37 Coreclr_url = 'https://github.com/dotnet/coreclr.git'
38 Jitutils_url = 'https://github.com/dotnet/jitutils.git'
40 # This should be factored out of build.sh
50 Is_windows = (os.name == 'nt')
51 clr_os = 'Windows_NT' if Is_windows else Unix_name_map[os.uname()[0]]
53 ##########################################################################
55 ##########################################################################
57 def del_rw(action, name, exc):
61 ##########################################################################
63 ##########################################################################
65 description = 'Tool to generate JIT assembly diffs from the CoreCLR repo'
67 parser = argparse.ArgumentParser(description=description)
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.
73 # TODO: need to fix parser so -skip_baseline_build / -skip_diffs don't take an argument
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)
86 ##########################################################################
88 ##########################################################################
90 def validate_args(args):
91 """ Validate all of the arguments parsed.
93 args (argparser.ArgumentParser): Args parsed by the argument parser.
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)
98 If the arguments are valid then return them all in a tuple. If not, raise
99 an exception stating x argument is incorrect.
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
113 def validate_arg(arg, check):
114 """ Validate an individual arg
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.
121 is_valid (bool): Is the argument valid?
124 helper = lambda item: item is not None and check(item)
127 raise Exception('Argument: %s is not valid.' % (arg))
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']
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)
136 validate_arg(arch, lambda item: item in valid_archs)
137 validate_arg(build_type, lambda item: item in valid_build_types)
139 if diff_root is None:
140 diff_root = nth_dirname(os.path.abspath(sys.argv[0]), 3)
142 diff_root = os.path.abspath(diff_root)
143 validate_arg(diff_root, lambda item: os.path.isdir(diff_root))
145 if scratch_root is None:
146 scratch_root = os.path.join(diff_root, '_')
148 scratch_root = os.path.abspath(scratch_root)
150 if ci_arch is not None:
151 validate_arg(ci_arch, lambda item: item in valid_ci_archs)
153 args = (arch, ci_arch, build_type, base_root, diff_root, scratch_root, skip_baseline_build, skip_diffs, target_branch, commit_hash)
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)
169 def nth_dirname(path, n):
170 """ Find the Nth parent directory of the given path
172 path (str): path name containing at least N components
173 n (int): num of basenames to remove
175 outpath (str): path with the last n components removed
177 If n is 0, path is returned unmodified
182 for i in range(0, n):
183 path = os.path.dirname(path)
188 """ Print logging information
190 message (str): message to be printed
193 print '[%s]: %s' % (sys.argv[0], message)
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.
200 source_dir (str): source directory path
201 target_dir (str): target directory path
207 assert os.path.isdir(source_dir)
208 assert os.path.isdir(target_dir)
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))
216 shutil.copy2(source_pathname, target_pathname)
218 ##########################################################################
220 # 1. determine appropriate commit,
223 ##########################################################################
225 def baseline_build():
228 if os.path.isdir(baseCoreClrPath):
229 log('Removing existing tree: %s' % baseCoreClrPath)
230 shutil.rmtree(baseCoreClrPath, onerror=del_rw)
232 # Find the baseline commit
234 # Clone at that commit
236 command = 'git clone -b %s --single-branch %s %s' % (
237 target_branch, Coreclr_url, baseCoreClrPath)
239 returncode = 0 if testing else os.system(command)
241 log('ERROR: git clone failed')
244 # Change directory to the baseline root
247 log('[cd] %s' % baseCoreClrPath)
249 os.chdir(baseCoreClrPath)
251 # Set up for possible docker usage
256 if not Is_windows and (arch == 'arm' or arch == 'arm64'):
257 # Linux arm and arm64 builds are cross-compilation builds using Docker.
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'
263 dockerFile = 'microsoft/dotnet-buildtools-prereqs:ubuntu-16.04-cross-arm64-a3ae44b-20180315221921'
264 dockerOpts = '-e ROOTFS_DIR=/crossrootfs/arm64'
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
270 # Build a checked baseline jit
273 command = 'set __TestIntermediateDir=int&&build.cmd %s checked skiptests skipbuildpackages' % arch
275 command = '%s%s/build.sh %s checked skiptests skipbuildpackages %s' % (dockerCmd, scriptPath, arch, buildOpts)
277 returncode = 0 if testing else os.system(command)
279 log('ERROR: build failed')
282 # Build the layout (Core_Root) directory
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
288 command = 'run.cmd build -Project="tests\\build.proj" -BuildOS=Windows_NT -BuildType=%s -BuildArch=%s -BatchRestorePackages' % (build_type, arch)
290 returncode = 0 if testing else os.system(command)
292 log('ERROR: restoring packages failed')
296 command = 'tests\\runtest.cmd %s checked GenerateLayoutOnly' % arch
298 command = '%s%s/build-test.sh %s checked generatelayoutonly' % (dockerCmd, scriptPath, arch)
300 returncode = 0 if testing else os.system(command)
302 log('ERROR: generating layout failed')
305 # After baseline build, change directory back to where we started
313 ##########################################################################
315 # 1. download dotnet CLI (needed by jitutils)
316 # 2. clone jitutils repo
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 ##########################################################################
324 global baseCoreClrPath
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'))
331 dotnet_tool = 'dotnet.exe' if Is_windows else 'dotnet'
333 # Make sure the temporary directories do not exist. If they do already, delete them.
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):
340 log('Removing existing tree: %s' % dotnetcliPath)
341 shutil.rmtree(dotnetcliPath, onerror=del_rw)
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.')
346 # Looks like the dotnet too is still there; try to run it to shut down the build servers.
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
356 log('Return code = %s' % returncode)
359 log('Trying again to remove existing tree: %s' % dotnetcliPath)
360 shutil.rmtree(dotnetcliPath, onerror=del_rw)
362 log('Failed to remove existing tree')
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)
373 os.makedirs(dotnetcliPath)
374 os.makedirs(jitutilsPath)
375 os.makedirs(asmRootPath)
377 if not os.path.isdir(dotnetcliPath):
378 log('ERROR: cannot create CLI install directory %s' % dotnetcliPath)
380 if not os.path.isdir(jitutilsPath):
381 log('ERROR: cannot create jitutils install directory %s' % jitutilsPath)
383 if not os.path.isdir(asmRootPath):
384 log('ERROR: cannot create diff directory %s' % asmRootPath)
387 log('dotnet CLI install directory: %s' % dotnetcliPath)
388 log('jitutils install directory: %s' % jitutilsPath)
389 log('asm directory: %s' % asmRootPath)
393 log('Downloading .Net CLI')
396 dotnetcliFilename = ""
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')
408 log('ERROR: unknown or unsupported OS %s' % os)
411 log('Downloading: %s => %s' % (dotnetcliUrl, dotnetcliFilename))
414 response = urllib2.urlopen(dotnetcliUrl)
415 request_url = response.geturl()
416 testfile = urllib.URLopener()
417 testfile.retrieve(request_url, dotnetcliFilename)
419 if not os.path.isfile(dotnetcliFilename):
420 log('ERROR: Did not download .Net CLI')
425 log('Unpacking .Net CLI')
429 with zipfile.ZipFile(dotnetcliFilename, "r") as z:
430 z.extractall(dotnetcliPath)
432 tar = tarfile.open(dotnetcliFilename)
433 tar.extractall(dotnetcliPath)
436 if not os.path.isfile(os.path.join(dotnetcliPath, dotnet_tool)):
437 log('ERROR: did not extract .Net CLI from download')
440 # Add dotnet CLI to PATH we'll use to spawn processes.
442 log('Add %s to my PATH' % dotnetcliPath)
443 my_env["PATH"] = dotnetcliPath + os.pathsep + my_env["PATH"]
447 command = 'git clone -b master --single-branch %s %s' % (Jitutils_url, jitutilsPath)
449 returncode = 0 if testing else os.system(command)
451 log('ERROR: cannot clone jitutils');
455 # Build jitutils, including "dotnet restore"
458 # Change directory to the jitutils root
461 log('[cd] %s' % jitutilsPath)
463 os.chdir(jitutilsPath)
465 # Do "dotnet restore"
467 command = ["dotnet", "restore"]
468 log('Invoking: %s' % (' '.join(command)))
470 proc = subprocess.Popen(command, env=my_env)
471 output,error = proc.communicate()
472 returncode = proc.returncode
474 log('Return code = %s' % returncode)
478 command = ['build.cmd' if Is_windows else 'build.sh', '-p']
479 log('Invoking: %s' % (' '.join(command)))
481 proc = subprocess.Popen(command, env=my_env)
482 output,error = proc.communicate()
483 returncode = proc.returncode
485 log('Return code = %s' % returncode)
486 log('ERROR: jitutils build failed')
489 jitutilsBin = os.path.join(jitutilsPath, "bin")
491 if not testing and not os.path.isdir(jitutilsBin):
492 log("ERROR: jitutils not correctly built")
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")
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")
505 # Add jitutils bin to path for spawned processes
507 log('Add %s to my PATH' % jitutilsBin)
508 my_env["PATH"] = jitutilsBin + os.pathsep + my_env["PATH"]
510 # After baseline build, change directory back to where we started
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.
525 # First, generate the diffs
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
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.
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"]
540 # Over which set of assemblies should we generate asm?
541 # TODO: parameterize this
542 asm_source_args = ["--corelib"]
543 # asm_source_args = ["--frameworks"]
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)))
548 proc = subprocess.Popen(command, env=my_env)
549 output,error = proc.communicate()
550 returncode = proc.returncode
552 log('Return code = %s' % returncode)
555 # Did we get any diffs?
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")
562 # Next, generate the baseline asm
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)))
567 proc = subprocess.Popen(command, env=my_env)
568 output,error = proc.communicate()
569 returncode = proc.returncode
571 log('Return code = %s' % returncode)
574 # Did we get any diffs?
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")
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
584 command = ["dotnet", jitAnalyzePath, "--base", baseOutputDir, "--diff", diffOutputDir]
585 log('Invoking: %s' % (' '.join(command)))
587 proc = subprocess.Popen(command, env=my_env)
588 output,error = proc.communicate()
589 returncode = proc.returncode
591 log('Return code = %s' % returncode)
592 log('Compare: %s %s' % (baseOutputDir, diffOutputDir))
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.
597 log('Shutting down build servers')
598 command = ["dotnet", "build-server", "shutdown"]
599 log('Invoking: %s' % (' '.join(command)))
601 proc = subprocess.Popen(command, env=my_env)
602 output,error = proc.communicate()
603 returncode = proc.returncode
605 log('Return code = %s' % returncode)
609 ##########################################################################
611 ##########################################################################
615 global arch, ci_arch, build_type, base_root, diff_root, scratch_root, skip_baseline_build, skip_diffs, target_branch, commit_hash
617 global base_layout_root
618 global diff_layout_root
619 global baseCoreClrPath
622 arch, ci_arch, build_type, base_root, diff_root, scratch_root, skip_baseline_build, skip_diffs, target_branch, commit_hash = validate_args(args)
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)
630 # Check the diff layout directory before going too far.
632 diff_layout_root = os.path.join(diff_root,
635 '%s.%s.%s' % (clr_os, arch, build_type),
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)
643 # Create the scratch root directory
647 os.makedirs(scratch_root)
649 if not os.path.isdir(scratch_root):
650 log('ERROR: cannot create scratch directory %s' % scratch_root)
653 # Set up baseline root directory. If one is passed to us, we use it. Otherwise, we create
654 # a temporary directory.
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'))
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)
666 # Do the baseline build, if needed
668 if not skip_baseline_build and base_root is None:
669 returncode = baseline_build()
673 # Check that the baseline root directory was created.
675 base_layout_root = os.path.join(baseCoreClrPath,
678 '%s.%s.%s' % (clr_os, arch, build_type),
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)
686 # Do the diff run, if needed
689 returncode = do_pmi_diffs()
696 ##########################################################################
698 ##########################################################################
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)