Skip to content

Publish a .csx script as a binary executable #312

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jun 13, 2018
Merged
2 changes: 2 additions & 0 deletions src/Dotnet.Script.Core/Dotnet.Script.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@
<None Remove="Templates\helloworld.csx.template" />
<None Remove="Templates\launch.json.template" />
<None Remove="Templates\omnisharp.json.template" />
<None Remove="Templates\program.publish.template" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="Templates\helloworld.csx.template" />
<EmbeddedResource Include="Templates\launch.json.template" />
<EmbeddedResource Include="Templates\omnisharp.json.template" />
<EmbeddedResource Include="Templates\program.publish.template" />
</ItemGroup>

<ItemGroup>
Expand Down
11 changes: 7 additions & 4 deletions src/Dotnet.Script.Core/ScriptCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Dotnet.Script.Core.Internal;
using Dotnet.Script.DependencyModel.Context;
Expand Down Expand Up @@ -52,7 +53,7 @@ static ScriptCompiler()
};

// see: https://github.com/dotnet/roslyn/issues/5501
protected virtual IEnumerable<string> SuppressedDiagnosticIds => new[] { "CS1701", "CS1702", "CS1705" };
protected virtual IEnumerable<string> SuppressedDiagnosticIds => new[] { "CS1701", "CS1702", "CS1705" };

public CSharpParseOptions ParseOptions { get; } = new CSharpParseOptions(LanguageVersion.Latest, kind: SourceCodeKind.Script);

Expand Down Expand Up @@ -106,7 +107,9 @@ public virtual ScriptCompilationContext<TReturn> CreateCompilationContext<TRetur
Logger.Verbose($"Current runtime is '{_scriptEnvironment.PlatformIdentifier}'.");
RuntimeDependency[] runtimeDependencies = GetRuntimeDependencies(context);

var scriptOptions = CreateScriptOptions(context, runtimeDependencies.ToList());
var encoding = context.Code.Encoding ?? Encoding.UTF8; // encoding is required when emitting debug information
var scriptOptions = CreateScriptOptions(context, runtimeDependencies.ToList())
.WithFileEncoding(encoding);

var loadedAssembliesMap = CreateLoadedAssembliesMap();

Expand Down Expand Up @@ -209,7 +212,7 @@ private void SetOptimizationLevel<TReturn>(ScriptContext context, Script<TReturn
private string GetScriptCode(ScriptContext context)
{
string code;

// when processing raw code, make sure we inject new lines after preprocessor directives
if (context.FilePath == null)
{
Expand Down Expand Up @@ -259,7 +262,7 @@ private Assembly MapUnresolvedAssemblyToRuntimeLibrary(IDictionary<string, Runti
{
if (runtimeAssembly.Name.Version > assemblyName.Version)
{
loadedAssemblyMap.TryGetValue(assemblyName.Name, out var loadedAssembly);
loadedAssemblyMap.TryGetValue(assemblyName.Name, out var loadedAssembly);
if(loadedAssembly != null)
{
Logger.Log($"Redirecting {assemblyName} to already loaded {loadedAssembly.GetName().Name}");
Expand Down
35 changes: 35 additions & 0 deletions src/Dotnet.Script.Core/ScriptEmitResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Microsoft.CodeAnalysis;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;

namespace Dotnet.Script.Core
{
public class ScriptEmitResult
{
private ScriptEmitResult() { }

public ScriptEmitResult(MemoryStream peStream, MemoryStream pdbStream, IEnumerable<MetadataReference> directiveReferences)
{
PeStream = peStream;
PdbStream = pdbStream;
DirectiveReferences = directiveReferences.ToImmutableArray();
}

public MemoryStream PeStream { get; }
public MemoryStream PdbStream { get; }
public ImmutableArray<Diagnostic> Diagnostics { get; private set; } = ImmutableArray.Create<Diagnostic>();
public ImmutableArray<MetadataReference> DirectiveReferences { get; } = ImmutableArray.Create<MetadataReference>();
public bool Success => !Diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error);

public static ScriptEmitResult Error(IEnumerable<Diagnostic> diagnostics)
{
var result = new ScriptEmitResult
{
Diagnostics = diagnostics.ToImmutableArray()
};
return result;
}
}
}
58 changes: 58 additions & 0 deletions src/Dotnet.Script.Core/ScriptEmitter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.CodeAnalysis.Scripting.Hosting;
using System.IO;

namespace Dotnet.Script.Core
{
public class ScriptEmitter
{
private readonly ScriptConsole _scriptConsole;
private readonly ScriptCompiler _scriptCompiler;

public ScriptEmitter(ScriptConsole scriptConsole, ScriptCompiler scriptCompiler)
{
_scriptConsole = scriptConsole;
_scriptCompiler = scriptCompiler;
}

public virtual ScriptEmitResult Emit<TReturn>(ScriptContext context, string assemblyName = null)
{
try
{
var compilationContext = _scriptCompiler.CreateCompilationContext<TReturn, CommandLineScriptGlobals>(context);

var compilation = compilationContext.Script.GetCompilation();
if (!string.IsNullOrEmpty(assemblyName))
{
var compilationOptions = compilationContext.Script.GetCompilation().Options
.WithScriptClassName(assemblyName);
compilation = compilationContext.Script.GetCompilation()
.WithOptions(compilationOptions)
.WithAssemblyName(assemblyName);
}

var peStream = new MemoryStream();
var pdbStream = new MemoryStream();
var result = compilation.Emit(peStream, pdbStream: pdbStream, options: new EmitOptions().
WithDebugInformationFormat(DebugInformationFormat.PortablePdb));

if (result.Success)
{
return new ScriptEmitResult(peStream, pdbStream, compilation.DirectiveReferences);
}

return ScriptEmitResult.Error(result.Diagnostics);
}
catch (CompilationErrorException e)
{
foreach (var diagnostic in e.Diagnostics)
{
_scriptConsole.WritePrettyError(diagnostic.ToString());
}

throw;
}
}
}
}
124 changes: 124 additions & 0 deletions src/Dotnet.Script.Core/ScriptPublisher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
using Dotnet.Script.DependencyModel.Environment;
using Dotnet.Script.DependencyModel.Logging;
using Dotnet.Script.DependencyModel.Process;
using Dotnet.Script.DependencyModel.ProjectSystem;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Scripting;
using System;
using System.IO;
using System.Reflection;

namespace Dotnet.Script.Core
{
public class ScriptPublisher
{
const string AssemblyName = "scriptAssembly";
const string ScriptingVersion = "2.8.2";

private readonly ScriptProjectProvider _scriptProjectProvider;
private readonly ScriptEmitter _scriptEmitter;
private readonly ScriptConsole _scriptConsole;
private readonly ScriptEnvironment _scriptEnvironment;

public ScriptPublisher(ScriptProjectProvider scriptProjectProvider, ScriptEmitter scriptEmitter, ScriptConsole scriptConsole)
{
_scriptProjectProvider = scriptProjectProvider ?? throw new ArgumentNullException(nameof(scriptProjectProvider));
_scriptEmitter = scriptEmitter ?? throw new ArgumentNullException(nameof(scriptEmitter));
_scriptConsole = scriptConsole ?? throw new ArgumentNullException(nameof(scriptConsole));
_scriptEnvironment = ScriptEnvironment.Default;
}

public ScriptPublisher(LogFactory logFactory, ScriptEmitter scriptEmitter)
: this
(
new ScriptProjectProvider(logFactory),
scriptEmitter,
ScriptConsole.Default
)
{
}

public void CreateExecutable(ScriptContext context, LogFactory logFactory)
{
var tempProjectPath = _scriptProjectProvider.CreateProjectForScriptFile(context.FilePath);
var tempProjectDirecory = Path.GetDirectoryName(tempProjectPath);

var scriptAssemblyPath = CreateScriptAssembly(context, tempProjectDirecory);

var projectFile = new ProjectFile(File.ReadAllText(tempProjectPath));
projectFile.AddPackageReference(new PackageReference("Microsoft.CodeAnalysis.Scripting", ScriptingVersion, PackageOrigin.ReferenceDirective));
projectFile.AddAssemblyReference(scriptAssemblyPath);
projectFile.Save(tempProjectPath);

CopyProgramTemplate(tempProjectDirecory);

var runtimeIdentifier = _scriptEnvironment.RuntimeIdentifier;

var commandRunner = new CommandRunner(logFactory);
// todo: may want to add ability to return dotnet.exe errors
var exitcode = commandRunner.Execute("dotnet", $"publish \"{tempProjectPath}\" -c Release -r {runtimeIdentifier} -o {context.WorkingDirectory}");
if (exitcode != 0) throw new Exception($"dotnet publish failed with result '{exitcode}'");
}

private string CreateScriptAssembly(ScriptContext context, string tempProjectDirecory)
{
try
{
var emitResult = _scriptEmitter.Emit<int>(context, AssemblyName);
if (!emitResult.Success)
{
throw new CompilationErrorException("One or more errors occurred when emitting the assembly", emitResult.Diagnostics);
}

var assemblyPath = Path.Combine(tempProjectDirecory, $"{AssemblyName}.dll");
using (var peFileStream = new FileStream(assemblyPath, FileMode.Create))
using (emitResult.PeStream)
{
emitResult.PeStream.WriteTo(peFileStream);
}

var pdbPath = Path.Combine(tempProjectDirecory, $"{AssemblyName}.pdb");
using (var pdbFileStream = new FileStream(pdbPath, FileMode.Create))
using (emitResult.PdbStream)
{
emitResult.PdbStream.WriteTo(pdbFileStream);
}

foreach (var reference in emitResult.DirectiveReferences)
{
if (reference.Display.EndsWith(".NuGet.dll")) continue;
var refInfo = new FileInfo(reference.Display);
var newAssemblyPath = Path.Combine(tempProjectDirecory, refInfo.Name);
File.Copy(refInfo.FullName, newAssemblyPath, true);
}

return assemblyPath;
}
catch (CompilationErrorException ex)
{
_scriptConsole.WritePrettyError(ex.Message);
foreach (var diagnostic in ex.Diagnostics)
{
_scriptConsole.WritePrettyError(diagnostic.ToString());
}
throw;
}
}

private void CopyProgramTemplate(string tempProjectDirecory)
{
const string resourceName = "Dotnet.Script.Core.Templates.program.publish.template";

var resourceStream = typeof(ScriptPublisher).GetTypeInfo().Assembly.GetManifestResourceStream(resourceName);
if (resourceStream == null) throw new FileNotFoundException($"Unable to locate resource '{resourceName}'");

string program;
using (var streamReader = new StreamReader(resourceStream))
{
program = streamReader.ReadToEnd();
}
var programcsPath = Path.Combine(tempProjectDirecory, "Program.cs");
File.WriteAllText(programcsPath, program);
}
}
}
43 changes: 43 additions & 0 deletions src/Dotnet.Script.Core/Templates/program.publish.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;
using Microsoft.CodeAnalysis.CSharp.Scripting.Hosting;
using Microsoft.CodeAnalysis.Scripting.Hosting;
using System.Threading.Tasks;
using static System.Console;

namespace dotnetPublishCode
{
class Program
{
static async Task Main(string[] args)
{
try
{
var globals = new CommandLineScriptGlobals(Console.Out, CSharpObjectFormatter.Instance);
foreach (var arg in args)
globals.Args.Add(arg);

var factoryMethod = typeof(scriptAssembly).GetMethod("<Factory>");
if (factoryMethod == null) throw new Exception("couldn't find factory method to initiate script");

var invokeTask = factoryMethod.Invoke(null, new object[] { new object[] { globals, null } }) as Task<int>;
var invokeResult = await invokeTask;
if (invokeResult != 0) WritePrettyError($"Error result: '{invokeResult}'");
}
catch (Exception ex)
{
if (ex is AggregateException aggregateEx)
{
ex = aggregateEx.Flatten().InnerException;
}
WritePrettyError(ex.ToString());
}
}

public static void WritePrettyError(string value)
{
Console.ForegroundColor = ConsoleColor.Red;
Error.WriteLine(value.TrimEnd(Environment.NewLine.ToCharArray()));
Console.ResetColor();
}
}
}
13 changes: 13 additions & 0 deletions src/Dotnet.Script.DependencyModel/ProjectSystem/ProjectFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ public ProjectFile()
_document = XDocument.Parse(template);
}

public ProjectFile(string xmlText)
{
_document = XDocument.Parse(xmlText);
}

public void AddPackageReference(PackageReference packageReference)
{
var itemGroupElement = _document.Descendants("ItemGroup").Single();
Expand All @@ -24,6 +29,14 @@ public void AddPackageReference(PackageReference packageReference)
itemGroupElement.Add(packageReferenceElement);
}

public void AddAssemblyReference(string assemblyPath)
{
var itemGroupElement = _document.Descendants("ItemGroup").Single();
var packageReferenceElement = new XElement("Reference");
packageReferenceElement.Add(new XAttribute("Include", assemblyPath));
itemGroupElement.Add(packageReferenceElement);
}

public void SetTargetFramework(string targetFramework)
{
var targetFrameworkElement = _document.Descendants("TargetFramework").Single();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFramework>netcoreapp2.0</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
</ItemGroup>
Expand Down
Loading