1
0
mirror of https://github.com/fadden/6502bench.git synced 2024-05-31 22:41:37 +00:00
6502bench/SourceGen/Sandbox/PluginDllCache.cs
Andy McFadden b280fb58ba Rename PluginDll dir to PluginDllCache
The new name is more indicative of the purpose of the directory.

Updated the docs to point out that you can delete the contents any
time you want, so long as SourceGen isn't running at the time.

Also, change the default column widths for the exporter.
2020-07-16 10:36:58 -07:00

251 lines
11 KiB
C#

/*
* Copyright 2019 faddenSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.CodeDom.Compiler;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using CommonUtil;
using PluginCommon;
namespace SourceGen.Sandbox {
/// <summary>
/// This manages the PluginDllCache directory, which holds the compiled form of the extension
/// scripts. When a script is requested, this checks to see if the compiled form
/// already exists. If not, or the script source file is newer than the DLL file, the
/// compiler is executed.
///
/// This is global -- it's not tied to an active project.
///
/// If an assembly is still loaded, the file on disk will be locked by the operating
/// system and can't be replaced. So long as the plugins run in an AppDomain sandbox,
/// the locks will be cleared when the AppDomain is unloaded.
/// </summary>
public static class PluginDllCache {
private const string PLUGIN_DIR_NAME = "PluginDllCache";
/// <summary>
/// List of assemblies for the CompilerParameters.ReferencedAssemblies argument.
/// </summary>
private static readonly string[] sRefAssem = new string[] {
// Need this for various things to work, like System.Collections.Generic.
"netstandard.dll",
// Plugins are implemented in terms of interfaces defined here.
"PluginCommon.dll",
// Common utility functions.
"CommonUtil.dll",
};
/// <summary>
/// Path to plugin directory.
/// </summary>
private static string sPluginDirPath;
/// <summary>
/// Computes the path to the plugin directory. Does not attempt to verify that it exists.
/// </summary>
/// <returns>Plugin directory path, or null if we can't find the application data
/// area.</returns>
public static string GetPluginDirPath() {
if (sPluginDirPath == null) {
string runtimeUp = Path.GetDirectoryName(RuntimeDataAccess.GetDirectory());
if (runtimeUp == null) {
return null;
}
sPluginDirPath = Path.Combine(runtimeUp, PLUGIN_DIR_NAME);
}
return sPluginDirPath;
}
/// <summary>
/// Prepares the plugin directory. Creates it and copies PluginCommon.dll in.
/// Throws an exception if something fails.
/// </summary>
/// <exception cref="IOException">Various failures from file/dir operations.</exception>
public static void PreparePluginDir() {
string dstDir = GetPluginDirPath();
if (File.Exists(dstDir) && !Directory.Exists(dstDir)) {
throw new IOException(
string.Format(Res.Strings.ERR_FILE_EXISTS_NOT_DIR_FMT, dstDir));
}
Directory.CreateDirectory(dstDir);
// TODO(someday): try to remove *.dll where the modification date is more than a
// week old -- this will prevent us from accreting stuff indefinitely.
// Copy PluginCommon and CommonUtil over.
CopyIfNewer(typeof(PluginCommon.PluginManager).Assembly.Location, dstDir);
CopyIfNewer(typeof(CommonUtil.CRC32).Assembly.Location, dstDir);
}
/// <summary>
/// Copies a DLL file if it's not present in the destination directory, or
/// if it's newer than what's in the destination directory.
/// </summary>
/// <param name="srcDll">Full path to DLL file.</param>
/// <param name="dstDir">Destination directory.</param>
private static void CopyIfNewer(string srcDll, string dstDir) {
string dstFile = Path.Combine(dstDir, Path.GetFileName(srcDll));
if (FileUtil.IsFileMissingOrOlder(dstFile, srcDll)) {
Debug.WriteLine("Copying " + srcDll + " to " + dstFile);
File.Copy(srcDll, dstFile, true);
}
// Should we copy the .pdb files too, if they exist? If they don't exist in
// the source directory, do we need to remove them from the destination directory?
}
/// <summary>
/// Prepares the DLL for the specified script, compiling it if necessary.
/// </summary>
/// <param name="scriptIdent">Script identifier.</param>
/// <param name="projectPathName">Project file name, used for naming project-local
/// files. May be empty if the project hasn't been named yet (in which case
/// project-local files will cause a failure).</param>
/// <param name="report">Report with errors and warnings.</param>
/// <returns>Full path to DLL, or null if compilation failed.</returns>
public static string GenerateScriptDll(string scriptIdent, string projectPathName,
out FileLoadReport report) {
ExternalFile ef = ExternalFile.CreateFromIdent(scriptIdent);
if (ef == null) {
Debug.Assert(false);
report = new FileLoadReport("CreateFromIdent failed");
return null;
}
string projectDir = string.Empty;
if (!string.IsNullOrEmpty(projectPathName)) {
projectDir = Path.GetDirectoryName(projectPathName);
}
string srcPathName = ef.GetPathName(projectDir);
// Fail if the source script doesn't exist. If a previously-compiled DLL is present
// we could just continue to use it, but that seems contrary to expectation, and
// means that you won't notice that your project is broken until you clear out
// the DLL directory.
if (!File.Exists(srcPathName)) {
report = new FileLoadReport(srcPathName);
report.Add(FileLoadItem.Type.Error,
string.Format(Res.Strings.ERR_FILE_NOT_FOUND_FMT, srcPathName));
return null;
}
string destFileName = ef.GenerateDllName(projectPathName);
string destPathName = Path.Combine(GetPluginDirPath(), destFileName);
// Compile if necessary. We do this if the source code is newer, or if the
// DLLs that plugins depend on have been updated. (We're checking the dates on
// the DLLs the app uses, not the copy the plugins use, but earlier we made sure
// that they were the same. This test doesn't handle the case where the DLLs
// get rolled back, but that's probably not interesting for us.)
bool needCompile = FileUtil.IsFileMissingOrOlder(destPathName, srcPathName) ||
FileUtil.IsFileMissingOrOlder(destPathName,
typeof(PluginCommon.PluginManager).Assembly.Location) ||
FileUtil.IsFileMissingOrOlder(destPathName,
typeof(CommonUtil.CRC32).Assembly.Location);
if (needCompile) {
Debug.WriteLine("Compiling " + srcPathName + " to " + destPathName);
Assembly asm = CompileCode(srcPathName, destPathName, out report);
if (asm == null) {
return null;
}
} else {
Debug.WriteLine("NOT recompiling " + srcPathName);
report = new FileLoadReport(srcPathName);
}
return destPathName;
}
/// <summary>
/// Compiles the script from the specified pathname into an Assembly.
/// </summary>
/// <param name="scriptPathName">Script pathname.</param>
/// <param name="dllPathName">Full pathname for output DLL.</param>
/// <param name="report">Errors and warnings reported by the compiler.</param>
/// <returns>Reference to script instance, or null on failure.</returns>
private static Assembly CompileCode(string scriptPathName, string dllPathName,
out FileLoadReport report) {
report = new FileLoadReport(scriptPathName);
// To get C#6 (and later) features, a NuGet package must be installed, and
// some "black magic" must be invoked.
// See https://stackoverflow.com/a/40311406/294248 and nearby answers.
Microsoft.CSharp.CSharpCodeProvider csProvider =
new Microsoft.CSharp.CSharpCodeProvider();
CompilerParameters parms = new CompilerParameters();
// We want a DLL, not an EXE.
parms.GenerateExecutable = false;
// Save to disk so other AppDomain can load it.
parms.GenerateInMemory = false;
// Be vocal about warnings.
parms.WarningLevel = 3;
// Optimization is nice.
parms.CompilerOptions = "/optimize";
// Output file name. Must be named appropriately so it can be found.
parms.OutputAssembly = dllPathName;
// Add dependencies.
parms.ReferencedAssemblies.AddRange(sRefAssem);
#if DEBUG
// This creates a .pdb file, which allows breakpoints to work.
parms.IncludeDebugInformation = true;
#endif
// Using the "from file" version has an advantage over the "from source"
// version in that the debugger can find the source file, so things like
// breakpoints work correctly.
CompilerResults cr = csProvider.CompileAssemblyFromFile(parms, scriptPathName);
CompilerErrorCollection cec = cr.Errors;
foreach (CompilerError ce in cr.Errors) {
report.Add(ce.Line, ce.Column,
ce.IsWarning ? FileLoadItem.Type.Warning : FileLoadItem.Type.Error,
ce.ErrorText);
}
if (cr.Errors.HasErrors) {
return null;
} else {
Debug.WriteLine("Compilation successful");
return cr.CompiledAssembly;
}
}
/// <summary>
/// Finds the first concrete class that implements IPlugin, and
/// constructs an instance.
/// </summary>
public static IPlugin ConstructIPlugin(Assembly asm) {
foreach (Type type in asm.GetExportedTypes()) {
// Using a System.Linq extension method.
if (type.IsClass && !type.IsAbstract &&
type.GetInterfaces().Contains(typeof(IPlugin))) {
ConstructorInfo ctor = type.GetConstructor(Type.EmptyTypes);
IPlugin iplugin = (IPlugin)ctor.Invoke(null);
Debug.WriteLine("Created instance: " + iplugin);
return iplugin;
}
}
return null;
}
}
}