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

Conversation

Sharpiro
Copy link
Contributor

@Sharpiro Sharpiro commented Jun 9, 2018

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:

dotnet script publish C:\Users\sharpiro\Desktop\temp\tempScript\main.csx -d -o C:\Users\sharpiro\Desktop\temp\publishTemp

Sharpiro added 5 commits June 9, 2018 18:29
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
@filipw
Copy link
Member

filipw commented Jun 11, 2018

Thanks a lot, this is a great idea, and this is definitely a feature that we want.
At the high level, this implementation is also along the lines of what we'd want to have (compile, reference the DLL and run precompiled code using <Factory> method).

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 ScriptEmitter from that commit; this would decouple the process of emitting from the publish, which will be helpful in the future.

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);
Copy link
Member

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?

Copy link
Contributor Author

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));
Copy link
Member

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);
Copy link
Member

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.

Copy link
Contributor Author

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);
Copy link
Member

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
Copy link
Member

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)
Copy link
Member

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you mean?

Copy link
Contributor Author

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?

Copy link
Contributor Author

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

Copy link
Member

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)
Copy link
Member

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;
                }

Copy link
Contributor Author

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.

Copy link
Member

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

@Sharpiro
Copy link
Contributor Author

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
@Sharpiro
Copy link
Contributor Author

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();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

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

@Sharpiro
Copy link
Contributor Author

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.

@seesharper
Copy link
Collaborator

The need for suppressing CS1705 actually went away with #261. We decided to keep it nevertheless in case we still had assembly loading issues.
We are probably okay not suppressing it 👍

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();
Copy link
Collaborator

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";
Copy link
Collaborator

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 👍

@seesharper
Copy link
Collaborator

Left a few nit picking comments. Otherwise LGTM. Oh and the branch needs updating 😄

@filipw
Copy link
Member

filipw commented Jun 13, 2018

I noticed you're suppressing CS1705

this has been historically suppressed in the .NET CLI as well as in scripting, because the runtime should redirect to the correct version.
OmniSharp suppresses those too https://github.com/OmniSharp/omnisharp-roslyn/blob/76810f8655fbd198187b8ef0eae8bd6a3bee3c4c/src/OmniSharp.Roslyn/Utilities/CompilationOptionsHelper.cs#L10-L16

suppressing warnings on failure
misc pr review changes
Copy link
Member

@filipw filipw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@seesharper
Copy link
Collaborator

@Sharpiro This all looks good although I would love to see some basic testing before merging 😎

@Sharpiro
Copy link
Contributor Author

just added a few simple tests and am now using absolute file paths in the same way the default app command is.

@Sharpiro
Copy link
Contributor Author

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?

@seesharper
Copy link
Collaborator

seesharper commented Jun 13, 2018

@Sharpiro Okay, a couple of things here. When compiling a self contained binary on *nix, they don't get the .exe extension. Also I would like to have a test that actually executes the generated binary 👍

testing that generated exectuables execute correctly
@seesharper
Copy link
Collaborator

seesharper commented Jun 13, 2018

Now we are in business 🎸 Thanks 👍

@seesharper seesharper changed the title Publish a .csx script as .exe Publish a .csx script as a binary executable Jun 13, 2018
@seesharper seesharper merged commit 2015425 into dotnet-script:master Jun 13, 2018
@Sharpiro Sharpiro deleted the feature/console-app branch June 14, 2018 14:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy