-
Notifications
You must be signed in to change notification settings - Fork 175
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
Publish a .csx script as a binary executable #312
Conversation
dotnet executable dll now functional adding temporary project builder solution adding temp script code working on console app ref restore preparing to add dotnet cli tools now outputting to exe with args and printing errors
removed testing data
Thanks a lot, this is a great idea, and this is definitely a feature that we want. You can have a look at my PoC for #227 which is really very similar, it's here e677cd6 I think what I'd like to see is if you incorporate the Can you also add some tests? I'll also add some comments inline - thanks! |
.WithScriptClassName(AssemblyName); | ||
var scriptCompilation = compilationContext.Script.GetCompilation() | ||
.WithOptions(scriptOptions) | ||
.WithAssemblyName(AssemblyName); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is the assembly name reset really necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll check on this and make sure
|
||
var projectFile = new ProjectFile(File.ReadAllText(tempProjectPath)); | ||
// todo: grab version in a better way? | ||
projectFile.AddPackageReference(new PackageReference("Microsoft.CodeAnalysis.Scripting", "2.8.2", PackageOrigin.ReferenceDirective)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's ok to hardcode a version. You could technically omit it, with .net core SDK 2.1.300 it would just pull the latest but it might conflict with something. Let's make a global constant for the version
|
||
foreach (var reference in scriptCompilation.DirectiveReferences) | ||
{ | ||
var refInfo = new FileInfo(reference.Display); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you may want to check for nuget
pragma here, if it's a nuget reference there is nothing to copy (it resolves to a fake DLL) so you can skip it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
k I'll add something for this, I noticed what you're talking about
} | ||
|
||
var assemblyPath = $"{tempProjectDirecory}/{AssemblyName}.dll"; | ||
var emitResult = scriptCompilation.Emit(assemblyPath); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's incorporate ScriptEmitter
instead e677cd6
var factoryMethod = typeof(scriptAssembly).GetMethod("<Factory>"); | ||
if (factoryMethod == null) throw new Exception("couldn't find factory method to initiate script"); | ||
|
||
// todo: not sure what the second parameter is, using 'null' for now |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is OK - the second index in the array is for submission continuations (REPL scenarios)
{ | ||
class Program | ||
{ | ||
static void Main(string[] args) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can change it to async Main
and then there is no need to block in the code
var invokeResult = invokeTask.Result; | ||
if (invokeResult != 0) WritePrettyError($"Error result: '{invokeResult}'"); | ||
} | ||
// todo: not getting the full script stack trace |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what do you mean?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so i noticed that when i would throw an error in the script, I wouldn't get any of the script stack trace information to print out. I can see if that was because I wasn't using your emitter with debug information maybe?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for example, if i threw an error on line 25 of the main.csx file, when writing out the error it wouldn't give me any indication that the error actually occurred on 25, or if there was previous stack trace data
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is probably because you emit without symbols - if you compare the emitter code I mentioned before, it emits symbols too and line numbers are available in the symbols only
if (invokeResult != 0) WritePrettyError($"Error result: '{invokeResult}'"); | ||
} | ||
// todo: not getting the full script stack trace | ||
catch (AggregateException ex) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you want something like this
if (e is AggregateException aggregateEx)
{
e = aggregateEx.Flatten().InnerException;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wouldn't i need to still loop through the flattened "InnerExceptions" of a flattened aggregate exception typically? I think you're right though in this case there will only ever be 1 inner exception in that aggregate exception.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think on these code paths there can only ever be one
Thanks for the feedback. I'll try to update this code to use your script emitter, make adjustments based on your comments, and add a few tests hopefully tomorrow. |
adding scriptEmitter
I just committed code based on your feedback. I found that I did indeed need to modify the assembly name since prior to that it was an unspeakable name and would also be an invalid file name. Even when the assembly name was kept the same, but just written to a valid file name, there would be an error where the code would be looking for that unspeakable assembly name. I'll go ahead and start working on some tests soon. |
public MemoryStream PdbStream { get; } | ||
public ImmutableArray<Diagnostic> Diagnostics { get; private set; } = ImmutableArray.Create<Diagnostic>(); | ||
public ImmutableArray<MetadataReference> DirectiveReferences { get; } = ImmutableArray.Create<MetadataReference>(); | ||
public bool IsErrored => Diagnostics.Any(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need any suppressions here?
https://github.com/filipw/dotnet-script/blob/4b72e5186966a1dbbc1d24c6816d85f5d9632457/src/Dotnet.Script.Core/ScriptCompiler.cs#L55
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also, diagnostics would also contains warnings. I think you should just check if there are any diagnostics that are error level
So how does error suppression work here? I noticed you're suppressing CS1705 which is an error, not a warning like the others. Even though you are supprsssing this error when evaluating the script diagnostics, won't this error just cause the script or emit to fail later on anyways? I tested a different error and tried to suppress it through your process, but it just causes the emit to fail in the script emitter later on. Also I don't see any current reason to suppress warnings. From what I can see dotnet script has no functionality or perhaps no desire to log or print warnings to the user. It simply logs the diagnostics it suppressed and then errors and logs any unsuppressed errors if any were encountered. |
The need for suppressing CS1705 actually went away with #261. We decided to keep it nevertheless in case we still had assembly loading issues. |
public MemoryStream PdbStream { get; } | ||
public ImmutableArray<Diagnostic> Diagnostics { get; private set; } = ImmutableArray.Create<Diagnostic>(); | ||
public ImmutableArray<MetadataReference> DirectiveReferences { get; } = ImmutableArray.Create<MetadataReference>(); | ||
public bool IsErrored => Diagnostics.Any(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Success
might be a better name her since that aligns with EmitResult.Success
throw new CompilationErrorException("One or more errors occurred when emitting the assembly", emitResult.Diagnostics); | ||
} | ||
|
||
var assemblyPath = $"{tempProjectDirecory}/{AssemblyName}.dll"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use Path.Combine to ensure correctness across all operating systems 👍
Left a few nit picking comments. Otherwise LGTM. Oh and the branch needs updating 😄 |
this has been historically suppressed in the .NET CLI as well as in scripting, because the runtime should redirect to the correct version. |
suppressing warnings on failure misc pr review changes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
@Sharpiro This all looks good although I would love to see some basic testing before merging 😎 |
added several simple tests
…net-script into feature/console-app
just added a few simple tests and am now using absolute file paths in the same way the default app command is. |
Looks like the Travis CI build didn't like my tests The tests work on my machine as well as AppVeyor. Anything I need to know about Travis CI? |
@Sharpiro Okay, a couple of things here. When compiling a self contained binary on *nix, they don't get the |
testing that generated exectuables execute correctly
Now we are in business 🎸 Thanks 👍 |
This PR allows for publishing a script as an executable.
I found myself every now and then having a good use case to basically take some scripts I've created and just have them outputted to an executable file. I thought this would be kind of cool and looked relevant to the current issue: #227. However this is slightly different in that this isn't currently concerned with outputting scripts to re-usable libraries, and just executes as if
dotnet script main.csx
were called, except from a .exe file. If this isn't something you guys want in the main code base, I understand. I also did my best to modify as few files as possible and follow any coding styles I noticed while learning the code base.I created a new "publish" command that takes in a script argument, an optional publish directory, and a debug flag. It uses a lot of code that you guys have already written, but goes a bit further with modifying the generated temp project, adding a Program.cs template, generating an assembly from the scripts, and moving around any referenced .dll files as needed.
dotnet publish
is then executed from your command runner class and this creates the executable.Let me know if you have any questions or if there is an obvious better way to do this. Thanks.
Example usage: