private static bool CompileOnce(List <string> sourceFiles, HPluginCompilationConfiguration configuration, CompilerParameters compilerParams, CSharpCodeProvider csProvider, HPluginCompilationResult result) { CompilerResults compileResult = csProvider.CompileAssemblyFromFile(compilerParams, sourceFiles.ToArray()); bool compileSuccess = true; if (compileResult.Errors.HasErrors) { foreach (CompilerError error in compileResult.Errors) { result.CompileErrors.Add(error); } compileSuccess = false; } else { // Get the compiled assembly Assembly asm = compileResult.CompiledAssembly; result.CompiledAssemblies.Add(asm); // Then go through all compiled HPlugin types and deduce the relative path of each one so that it can be associated with its savedata // Because of some less-than-great techniques required to do this, it may fail. If that happens, we can at least let the plugin run anyways, just without access to persistent savedata. try { // We need Cecil to do this. Possible solutions with Reflection and CompilerServices get close (i.e. using StackTrace or CallerFilePath), but don't work on unknown subclassed code. byte[] asmBytes = asm.ToByteArray(); // We could load the on-disk assembly, but this should be much faster, especially for large assemblies with embedded resources. // Turn the compiled assembly into a stream (so we can load it with cecil) using (MemoryStream memStream = new MemoryStream(asmBytes)) { // Also load the generated pdb file (which is always placed on the disk and is not held in memory). The pdb should be in the working directory. string pdbFilePath = null; int spot = compilerParams.OutputAssembly.LastIndexOf(".dll"); if (spot > -1) { pdbFilePath = compilerParams.OutputAssembly.Substring(0, spot) + ".pdb"; } using (FileStream pdbStream = new FileStream(pdbFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)) { // Load the assembly with its symbols from the pdb ReaderParameters readerParameters = new ReaderParameters(); readerParameters.ReadSymbols = true; readerParameters.SymbolStream = pdbStream; AssemblyDefinition cecilAsmDef = AssemblyDefinition.ReadAssembly(memStream, readerParameters); // Find the source file path of each type, using SequencePoints foreach (Type type in asm.GetTypes().Where(t => t.IsClass && t.IsSubclassOf(typeof(HPlugin))).ToList()) { string relPath = ""; try { string foundSourcePath = null; // Use any method defined in the user's plugin file to get a SequencePoint and thus the source file path TypeDefinition typeDef = cecilAsmDef.MainModule.GetTypes().Where(x => x.FullName == type.FullName).FirstOrDefault(); foreach (MethodDefinition methodDef in typeDef.Methods) { if (foundSourcePath != null) { break; } foreach (Instruction ins in methodDef.Body.Instructions) { if (foundSourcePath != null) { break; } SequencePoint seqPoint = ins.SequencePoint; if (seqPoint != null) { if (seqPoint.Document != null) { if (!String.IsNullOrEmpty(seqPoint.Document.Url)) { foundSourcePath = seqPoint.Document.Url; } } } } } string standardizedSourcePath = Path.GetFullPath(foundSourcePath).ToLowerInvariant(); string standardizedRootDir = Path.GetFullPath(configuration.UserFilesRootDirectory).ToLowerInvariant(); int spot2 = standardizedSourcePath.IndexOf(standardizedRootDir); if (spot2 >= 0) { relPath = (standardizedSourcePath.Substring(0, spot2) + standardizedSourcePath.Substring(spot2 + standardizedRootDir.Length)).TrimStart('\\', '/'); } } catch (Exception e2) { } // Just swallow it. The plugin probably broke some protocol and thus will not have persistent savedata. result.CompiledTypesSourceFileRelativePaths[type.FullName] = relPath; } } } } catch (Exception e) { } // Swallow. Persistent savedata will not work, but the plugin(s) themself will be fine. } return(compileSuccess); }
/// <summary> /// Compiles and returns a list of assemblies using the provided configuration. /// </summary> /// <param name="configuration">The configuration to use when compiling.</param> /// <returns>The compiled assemblies.</returns> public static HPluginCompilationResult Compile(HPluginCompilationConfiguration configuration) { HPluginCompilationResult result = new HPluginCompilationResult(); // Output directory string outputDir = configuration.DiskOutputDirectory ?? DefaultOutputFilesDirectory; result.OutputDirectory = outputDir; try { // Load all potentially required assemblies into our appdomain TryLoadDotNetTerrariaReferences(); // Compiler configuration CompilerParameters compilerParams = new CompilerParameters(); compilerParams.GenerateInMemory = true; // This just affects the compilation process (should be faster than using the disk). The output assembly and its pdb are always written to a file. compilerParams.GenerateExecutable = false; compilerParams.CompilerOptions = configuration.CompilerArguments; compilerParams.IncludeDebugInformation = true; compilerParams.TreatWarningsAsErrors = false; // References on disk foreach (string filePath in configuration.ReferencesOnDisk) { compilerParams.ReferencedAssemblies.Add(filePath); } // References in memory if (configuration.ReuseTemporaryFiles) { if (Directory.Exists(TemporaryFilesDirectory)) { foreach (string refAsmPath in Directory.GetFiles(TemporaryFilesDirectory)) { compilerParams.ReferencedAssemblies.Add(refAsmPath); } } } else { int refAsmNum = 0; foreach (byte[] asmBytes in configuration.ReferencesInMemory) { Directory.CreateDirectory(TemporaryFilesDirectory); string asmFullPath = Path.Combine(Directory.GetCurrentDirectory(), TemporaryFilesDirectory, "RefAsm" + refAsmNum + ".dll"); File.WriteAllBytes(asmFullPath, asmBytes); compilerParams.ReferencedAssemblies.Add(asmFullPath); refAsmNum++; } } // Reference self (TTPlugins) compilerParams.ReferencedAssemblies.Add(Assembly.GetExecutingAssembly().Location); // Reference whatever is loaded in our appdomain, which will likely contain most of the common types and namespaces from System. foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies()) { if (!asm.IsDynamic && !asm.ReflectionOnly && !String.IsNullOrEmpty(asm.Location)) { compilerParams.ReferencedAssemblies.Add(asm.Location); } } CSharpCodeProvider csProvider = new CSharpCodeProvider(); // If output directory already exists, clear it if (Directory.Exists(outputDir)) { DirectoryInfo outputDirInfo = new DirectoryInfo(outputDir); try { outputDirInfo.Delete(true); } catch (Exception e) { } // Swallow (for now) } Directory.CreateDirectory(outputDir); // Setup output and compile if (configuration.SingleAssemblyOutput) { string dllName = "TTPlugins_CompiledConglomerate.dll"; string pdbName = Path.GetFileNameWithoutExtension(dllName) + ".pdb"; string dllOutput = Path.Combine(outputDir, dllName); string pdbOutput = Path.Combine(outputDir, pdbName); compilerParams.OutputAssembly = dllOutput; if (CompileOnce(configuration.SourceFiles, configuration, compilerParams, csProvider, result)) { result.OutputFilesOnDisk.Add(dllOutput); result.OutputFilesOnDisk.Add(pdbOutput); } } else { foreach (string sourceFile in configuration.SourceFiles) { int numShift = 0; string originDllName = "TTPlugins_CompiledAsm_" + Path.GetFileNameWithoutExtension(sourceFile).Replace(' ', '_'); string dllName = originDllName; string checkDllOutput = Path.Combine(outputDir, dllName + ".dll"); while (File.Exists(checkDllOutput)) // Ensure no file conflicts { numShift++; dllName = originDllName + numShift; checkDllOutput = Path.Combine(outputDir, dllName + ".dll"); } dllName += ".dll"; string pdbName = Path.GetFileNameWithoutExtension(dllName) + ".pdb"; string dllOutput = Path.Combine(outputDir, dllName); string pdbOutput = Path.Combine(outputDir, pdbName); compilerParams.OutputAssembly = dllOutput; if (CompileOnce(new List <string>() { sourceFile }, configuration, compilerParams, csProvider, result)) { result.OutputFilesOnDisk.Add(dllOutput); result.OutputFilesOnDisk.Add(pdbOutput); } } } } catch (Exception e) { result.GenericCompilationFailure = true; } // Clear temporary reference assembly files if config says to or if there was a compile failure if (configuration.ClearTemporaryFilesWhenDone || result.GenericCompilationFailure) { ClearTemporaryCompileFiles(); } // Clear output files if config says to or if there was a compile failure if (configuration.DeleteOutputFilesFromDiskWhenDone || result.GenericCompilationFailure) { TryRemoveDirectory(outputDir); result.OutputFilesOnDisk.Clear(); } return(result); }