Compile SERP (#35729)
authorSimon Nattress <nattress@gmail.com>
Tue, 5 May 2020 05:55:27 +0000 (22:55 -0700)
committerGitHub <noreply@github.com>
Tue, 5 May 2020 05:55:27 +0000 (22:55 -0700)
Add R2RTest support for compiling specific larger applications.

src/coreclr/src/tools/r2rtest/BuildOptions.cs
src/coreclr/src/tools/r2rtest/CommandLineOptions.cs
src/coreclr/src/tools/r2rtest/Commands/CompileSerpCommand.cs [new file with mode: 0644]
src/coreclr/src/tools/r2rtest/CompilerRunner.cs
src/coreclr/src/tools/r2rtest/ComputeManagedAssemblies.cs
src/coreclr/src/tools/r2rtest/CpaotRunner.cs
src/coreclr/src/tools/r2rtest/PathHelpers.cs

index 57c25f4..eec3f5b 100644 (file)
@@ -39,6 +39,7 @@ namespace R2RTest
         public FileInfo CrossgenResponseFile { get; set; }
         public DirectoryInfo[] RewriteOldPath { get; set; }
         public DirectoryInfo[] RewriteNewPath { get; set; }
+        public DirectoryInfo AspNetPath { get; set; }
         public bool MeasurePerf { get; set; }
         public string InputFileSearchString { get; set; }
         public string ConfigurationSuffix => (Release ? "-ret.out" : "-chk.out");
index 39b718b..5f6d3d9 100644 (file)
@@ -19,7 +19,8 @@ namespace R2RTest
                 .AddCommand(CompileSubtree())
                 .AddCommand(CompileFramework())
                 .AddCommand(CompileNugetPackages())
-                .AddCommand(CompileCrossgenRsp());
+                .AddCommand(CompileCrossgenRsp())
+                .AddCommand(CompileSerp());
 
             return parser;
 
@@ -147,6 +148,19 @@ namespace R2RTest
                     },
                     handler: CommandHandler.Create<BuildOptions>(CompileFromCrossgenRspCommand.CompileFromCrossgenRsp));
 
+            Command CompileSerp() =>
+                new Command("compile-serp", "Compile existing application",
+                    new Symbol[]
+                    {
+                        InputDirectory(),
+                        OutputDirectory(),
+                        DegreeOfParallelism(),
+                        CoreRootDirectory(),
+                        AspNetPath(),
+                        Composite(),
+                    },
+                    handler: CommandHandler.Create<BuildOptions>(CompileSerpCommand.CompileSerpAssemblies));
+
             // Todo: Input / Output directories should be required arguments to the command when they're made available to handlers
             // https://github.com/dotnet/command-line-api/issues/297
             Option InputDirectory() =>
@@ -247,6 +261,12 @@ namespace R2RTest
             //
             Option PackageList() =>
                 new Option(new[] { "--package-list", "-pl" }, "Text file containing a package name on each line", new Argument<FileInfo>().ExistingOnly());
+
+            //
+            // compile-serp specific options
+            //
+            Option AspNetPath() =>
+                new Option(new[] { "--asp-net-path", "-asp" }, "Path to SERP's ASP.NET Core folder", new Argument<DirectoryInfo>().ExistingOnly());
         }
     }
 }
diff --git a/src/coreclr/src/tools/r2rtest/Commands/CompileSerpCommand.cs b/src/coreclr/src/tools/r2rtest/Commands/CompileSerpCommand.cs
new file mode 100644 (file)
index 0000000..af6b7ba
--- /dev/null
@@ -0,0 +1,190 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace R2RTest
+{
+    class CompileSerpCommand
+    {
+        public static int CompileSerpAssemblies(BuildOptions options)
+        {
+            if (options.InputDirectory == null)
+            {
+                Console.Error.WriteLine("Specify --response-file or --input-directory containing multiple response files.");
+                return 1;
+            }
+
+            if (options.CoreRootDirectory == null)
+            {
+                Console.Error.WriteLine("--core-root-directory (--cr) is a required argument.");
+                return 1;
+            }
+
+
+            // This command does not work in the context of an app, just a loose set of rsp files so don't execute anything we compile
+            options.NoJit = true;
+            options.NoEtw = true;
+
+            string serpDir = options.InputDirectory.FullName;
+            if (!File.Exists(Path.Combine(serpDir, "runserp.cmd")))
+            {
+                Console.Error.WriteLine($"Error: InputDirectory must point at a SERP build. Could not find {Path.Combine(serpDir, "runserp.cmd")}");
+                return 1;
+            }
+
+            string whiteListFilePath = Path.Combine(serpDir, "WhitelistDlls.txt");
+            if (!File.Exists(whiteListFilePath))
+            {
+                Console.Error.WriteLine($"File {whiteListFilePath} was not found");
+                return 1;
+            }
+
+            if (!File.Exists(Path.Combine(options.AspNetPath.FullName, "Microsoft.AspNetCore.dll")))
+            {
+                Console.Error.WriteLine($"Error: Asp.NET Core path must contain Microsoft.AspNetCore.dll");
+                return 1;
+            }
+
+            string binDir = Path.Combine(serpDir, "bin");
+
+            // Remove existing native images
+            foreach (var file in Directory.GetFiles(Path.Combine(serpDir, "App_Data\\Answers\\Services\\Packages"), "*.dll", SearchOption.AllDirectories))
+            {
+                if (file.EndsWith(".ni.dll") || file.EndsWith(".ni.exe"))
+                {
+                    File.Delete(file);
+                }
+            }
+
+            foreach (var file in Directory.GetFiles(binDir, "*.dll", SearchOption.AllDirectories))
+            {
+                if (file.EndsWith(".ni.dll") || file.EndsWith(".ni.exe"))
+                {
+                    File.Delete(file);
+                }
+            }
+
+            // Add all assemblies from the various SERP packages (filtered by ShouldInclude)
+            List<string> binFiles = Directory.GetFiles(Path.Combine(serpDir, "App_Data\\Answers\\Services\\Packages"), "*.dll", SearchOption.AllDirectories)
+                .Where((string x) => ShouldInclude(x))
+                .ToList();
+
+            // Add a whitelist of assemblies from bin
+            foreach (string item in new HashSet<string>(File.ReadAllLines(whiteListFilePath)))
+            {
+                binFiles.Add(Path.Combine(binDir, item));
+            }
+
+            HashSet<string> referenceAssemblyDirectories = new HashSet<string>();
+            foreach (var binFile in binFiles)
+            {
+                var directory = Path.GetDirectoryName(binFile);
+                if (!referenceAssemblyDirectories.Contains(directory))
+                    referenceAssemblyDirectories.Add(directory);
+            }
+
+            // TestILC needs a list of all directories containing assemblies that are referenced from crossgen
+            List<string> referenceAssemblies = new List<string>();
+            HashSet<string> simpleNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+
+            // Reference all managed assemblies in /bin and /App_Data/answers/services/packages
+            foreach (string binFile in ResolveReferences(referenceAssemblyDirectories))
+            {
+                simpleNames.Add(Path.GetFileNameWithoutExtension(binFile));
+                referenceAssemblies.Add(binFile);
+            }
+
+            referenceAssemblies.AddRange(ComputeManagedAssemblies.GetManagedAssembliesInFolderNoSimpleNameDuplicates(simpleNames, options.AspNetPath.FullName, "*.dll"));
+
+            // Add CoreRoot last because it contains various non-framework assemblies that are duplicated in SERP and we want SERP's to be used
+            referenceAssemblies.AddRange(ComputeManagedAssemblies.GetManagedAssembliesInFolderNoSimpleNameDuplicates(simpleNames, options.CoreRootDirectory.FullName, "System.*.dll"));
+            referenceAssemblies.AddRange(ComputeManagedAssemblies.GetManagedAssembliesInFolderNoSimpleNameDuplicates(simpleNames, options.CoreRootDirectory.FullName, "Microsoft.*.dll"));
+            referenceAssemblies.Add(Path.Combine(options.CoreRootDirectory.FullName, "mscorlib.dll"));
+            referenceAssemblies.Add(Path.Combine(options.CoreRootDirectory.FullName, "netstandard.dll"));
+
+            List<ProcessInfo> fileCompilations = new List<ProcessInfo>();
+            if (options.Composite)
+            {
+                string serpDll = Path.Combine(binDir, "Serp.dll");
+                var runner = new CpaotRunner(options, referenceAssemblies);
+                var compilationProcess = new ProcessInfo(new CompilationProcessConstructor(runner, Path.ChangeExtension(serpDll, ".ni.dll"), referenceAssemblies));
+                fileCompilations.Add(compilationProcess);
+            }
+            else
+            {
+                var runner = new CpaotRunner(options, referenceAssemblies);
+                foreach (string assemblyName in binFiles)
+                {
+                    var compilationProcess = new ProcessInfo(new CompilationProcessConstructor(runner, Path.ChangeExtension(assemblyName, ".ni.dll"), new string[] {assemblyName}));
+                    fileCompilations.Add(compilationProcess);
+                }
+            }
+            
+            ParallelRunner.Run(fileCompilations, options.DegreeOfParallelism);
+            
+            bool success = true;
+            int compilationFailures = 0;
+            foreach (var compilationProcess in fileCompilations)
+            {
+                if (!compilationProcess.Succeeded)
+                {
+                    success = false;
+                    compilationFailures++;
+
+                    Console.WriteLine($"Failed compiling {compilationProcess.Parameters.OutputFileName}");
+                }
+            }
+
+            Console.WriteLine("Serp Compilation Results");
+            Console.WriteLine($"Total compilations: {fileCompilations.Count}");
+            Console.WriteLine($"Compilation failures: {compilationFailures}");
+
+            return success ? 0 : 1;
+        }
+
+        private static bool ShouldInclude(string file)
+        {
+            if (!string.IsNullOrEmpty(file))
+            {
+                if (file.EndsWith(".ni.dll", StringComparison.OrdinalIgnoreCase) || file.EndsWith(".ni.exe", StringComparison.OrdinalIgnoreCase))
+                {
+                    return false;
+                }
+                if (file.EndsWith("Shared.Exports.dll", StringComparison.OrdinalIgnoreCase))
+                {
+                    return true;
+                }
+                if (file.EndsWith(".parallax.dll", StringComparison.OrdinalIgnoreCase))
+                {
+                    return false;
+                }
+                if (!file.EndsWith("Exports.dll", StringComparison.OrdinalIgnoreCase))
+                {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        private static IEnumerable<string> ResolveReferences(IEnumerable<string> folders)
+        {
+            foreach (string referenceFolder in folders)
+            {
+                foreach (string reference in ComputeManagedAssemblies.GetManagedAssembliesInFolder(referenceFolder))
+                {
+                    if (reference.EndsWith(".ni.dll"))
+                        continue;
+                    yield return reference;
+                }
+            }
+        }
+    }
+}
index 662dec6..1c91757 100644 (file)
@@ -83,12 +83,18 @@ namespace R2RTest
         public const int R2RDumpTimeoutMilliseconds = 60 * 1000;
 
         protected readonly BuildOptions _options;
-        protected readonly IEnumerable<string> _referenceFolders;
-
-        public CompilerRunner(BuildOptions options, IEnumerable<string> referenceFolders)
+        protected readonly List<string> _referenceFolders = new List<string>();
+        public CompilerRunner(BuildOptions options, IEnumerable<string> references)
         {
             _options = options;
-            _referenceFolders = referenceFolders;
+
+            foreach (var reference in references)
+            {
+                if (Directory.Exists(reference))
+                {
+                    _referenceFolders.Add(reference);
+                }
+            }
         }
 
         public IEnumerable<string> ReferenceFolders => _referenceFolders;
index 94dd078..e9be5cc 100644 (file)
@@ -11,9 +11,9 @@ using System.Reflection.PortableExecutable;
 
 class ComputeManagedAssemblies
 {
-    public static IEnumerable<string> GetManagedAssembliesInFolder(string folder)
+    public static IEnumerable<string> GetManagedAssembliesInFolder(string folder, string fileNamePattern = "*.*")
     {
-        foreach (string file in Directory.EnumerateFiles(folder))
+        foreach (var file in Directory.GetFiles(folder, fileNamePattern, SearchOption.TopDirectoryOnly))
         {
             if (IsManaged(file))
             {
@@ -22,6 +22,26 @@ class ComputeManagedAssemblies
         }
     }
 
+    /// <summary>
+    /// Returns a list of assemblies in "folder" whose simple name does not exist in "simpleNames". Each discovered
+    /// assembly is added to "simpleNames" to build a list without duplicates. When collecting multiple folders of assemblies
+    /// consider the call order for this function so duplicates are ignored as expected. For exmaple, CORE_ROOT's S.P.CoreLib
+    /// should normally win.
+    /// </summary>
+    /// <param name="simpleNames">Used to keep track of which simple names were used across multiple invocations of this method</param>
+    public static IEnumerable<string> GetManagedAssembliesInFolderNoSimpleNameDuplicates(HashSet<string> simpleNames, string folder, string fileNamePattern = "*.*")
+    {
+        foreach (var file in GetManagedAssembliesInFolder(folder, fileNamePattern))
+        {
+            var simpleName = Path.GetFileNameWithoutExtension(file);
+            if (!simpleNames.Contains(simpleName))
+            {
+                yield return file;
+                simpleNames.Add(simpleName);
+            }
+        }
+    }
+
     static ConcurrentDictionary<string, bool> _isManagedCache = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
 
     public static bool IsManaged(string file)
index 86bd24c..2e9697d 100644 (file)
@@ -20,12 +20,29 @@ namespace R2RTest
         protected override string CompilerRelativePath => "";
 
         protected override string CompilerFileName => "corerun".AppendOSExeSuffix();
+        protected readonly List<string> _referenceFiles = new List<string>();
 
         private string Crossgen2Path => Path.Combine(_options.CoreRootDirectory.FullName, "crossgen2", "crossgen2.dll");
 
-        public CpaotRunner(BuildOptions options, IEnumerable<string> referencePaths)
-            : base(options, referencePaths)
+        public CpaotRunner(BuildOptions options, IEnumerable<string> references)
+            : base(options, references)
         {
+            // Some scenarios are easier to express when we give Crossgen2 a list of reference assemblies instead of directories,
+            // so allow an override here.
+            foreach (var reference in references)
+            {
+                if (File.Exists(reference))
+                {
+                    if (_referenceFolders.Count > 0)
+                    {
+                        // There's nothing wrong with this per se, but none of our current scenarios need it, so this is 
+                        // just a consistency check.
+                        throw new ArgumentException($"A mix of files and directories was found in {references}");
+                    }
+                    _referenceFiles.Add(reference);
+                }
+            }
+
             // Set R2RTest parallelism to a low enough value that ensures that each Crossgen2 invocation gets to use its parallelism
             if (options.DegreeOfParallelism == 0)
                 options.DegreeOfParallelism = 2;
@@ -94,20 +111,34 @@ namespace R2RTest
                 }
             }
 
-            StringComparer pathComparer = (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal);
-            HashSet<string> uniqueFolders = new HashSet<string>(pathComparer);
-
-            foreach (string assemblyFileName in assemblyFileNames)
+            if (_referenceFiles.Count == 0)
             {
-                uniqueFolders.Add(Path.GetDirectoryName(assemblyFileName));
-            }
+                // Use reference folders and find the managed assemblies we want to reference from them.
+                // This is the standard path when we want to compile folders and compare crossgen1 and crossgen2.
+                StringComparer pathComparer = PathExtensions.OSPathCaseComparer;
+                HashSet<string> uniqueFolders = new HashSet<string>(pathComparer);
 
-            uniqueFolders.UnionWith(_referenceFolders);
-            uniqueFolders.Remove(frameworkFolder);
+                foreach (string assemblyFileName in assemblyFileNames)
+                {
+                    uniqueFolders.Add(Path.GetDirectoryName(assemblyFileName));
+                }
 
-            foreach (string reference in ResolveReferences(uniqueFolders, _options.Composite ? 'u' : 'r'))
+                uniqueFolders.UnionWith(_referenceFolders);
+                uniqueFolders.Remove(frameworkFolder);
+
+                foreach (string reference in ResolveReferences(uniqueFolders, _options.Composite ? 'u' : 'r'))
+                {
+                    yield return reference;
+                }
+            }
+            else
             {
-                yield return reference;
+                // Use an explicit set of reference assemblies.
+                // This is useful for crossgen2-specific scenarios since crossgen2 expects a list of files unlike crossgen1
+                foreach (var reference in _referenceFiles)
+                {
+                    yield return (_options.Composite ? "-u:" : "-r:") + reference;
+                }
             }
         }
 
index 3aa7a4b..87ad279 100644 (file)
@@ -288,4 +288,6 @@ static class PathExtensions
             return false;
         }
     }
+
+    public static StringComparer OSPathCaseComparer => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
 }