oleg-shilo / cs-script

C# scripting platform
http://www.cs-script.net
MIT License
1.61k stars 235 forks source link

Publish to single file gives an empty Assembly.Location which crashes Roslyn Evaluator #343

Closed angrynik closed 12 months ago

angrynik commented 1 year ago

I'm running a .NET 7 WPF app with a reference to CSScriptLib nuget.

My project works great and scripts run right in debug mode. When I publish I get the error:

"Current version of Roslyn-based evaluator does not support referencing assemblies which are not loaded from the file location."

I publish to a single file which merges most of the referenced dlls into a single dll. Looks like this code in Evaluator.Roslyn.cs is having a problem with it:

        public override IEvaluator ReferenceAssembly(Assembly assembly)
        {
            //Microsoft.Net.Compilers.1.2.0 - beta
            if (assembly.Location.IsEmpty())

I checked this in my code when publishing both to many files and to a single file and in the latter case Location is empty.

            if (string.IsNullOrEmpty(typeof(IRunScriptAsync).Assembly.Location))
                throw new System.Exception("Assembly.Location is empty");
oleg-shilo commented 1 year ago

This error means that you are trying to reference (from the script) an assembly that has not been loaded to your app domain from the file but from the memory. I guess this is what runtime does in the "single file" scenario.

It, not a problem for the host assembly execution but it is a problem for Roslyn, which has the limitation that it cannot reference assemblies without the file.

If it is you who is calling ReferenceAssembly then I suggest you try to reference the assembly a file. Replace ReferenceAssembly(Assembly assembly) call with ReferenceAssembly(string assembly).

If it is CS-Script itself when loading domain assemblies, then you should disable referencing domain assemblies and reference them individually by file name. In this case it will be that single file you built. Though I am not sure how you can find its path if Assembly.Location is empty.

angrynik commented 1 year ago

I think it's a Roslyn problem, this issue has gone nowhere for a few years:

https://github.com/dotnet/roslyn/issues/50719

That page links to a workaround here: https://github.com/andersstorhaug/SingleFileScripting

I'm new to this so I don't know if that's something I can do myself, or if it could be used in cs-script ? For now I'm going to switch off single file output, and add a hundred dlls to my Wix installer.

oleg-shilo commented 1 year ago

I am not sure it is a real workaround. We have the problem here that Roslyn scripts cannot reference loaded assemblies but only assembly files. The code there does not show the script that references any of such assemblies. It only references core assembly. You can do it with CS-Script too. Your problem is more fundamental. After merging all your dependency assemblies in a single file it's no longer an assembly that can be referenced.

But... I will play a little with it today to confirm what I just described.

oleg-shilo commented 1 year ago

Looked at the sample more... it might be the way out, actually. give me some time, I think it is something that can be the way out for cs-script in this scenario.

angrynik commented 1 year ago

Thanks for looking, would be very nice....

oleg-shilo commented 1 year ago

I can confirm now that with that work around it is now possible to execute scripts from the host app built as a single self-contaied file. Thank you for sharing the info.

It will take a little time to properly integrate it and release the update. The future syntax will look like this:

var calc = CSScript.Evaluator.Execute("1 + 2");

or

var calc = CSScript.Evaluator
                   .Execute(@"using System;
                              public class Script
                              {
                                  public int Sum(int a, int b)
                                  {
                                      return a+b;
                                  }
                              }
                              return new Script();");

int sum = calc.Sum(1, 2);
oleg-shilo commented 1 year ago

Done. Please update your nuget ref to v4.8.3.

var calc = CSScript.Evaluator
                   .Eval(@"using System;
                           public class Script
                           {
                               public int Sum(int a, int b)
                               {
                                   return a+b;
                               }
                           }
                           return new Script();");

int sum = calc.Sum(1, 2);
Console.WriteLine(sum);

The complete sample can be found here.

angrynik commented 1 year ago

Ok, awesome, I have refactored from LoadMethod to now use Eval, and it works great in debug mode. I publish to a single file and now I get:

CodeBase is not supported on assemblies loaded from a single-file bundle

I'm pretty sure my code is similar to your example, but with lots of "using " statements for other referenced assemblies.

oleg-shilo commented 1 year ago

I did test the code sample (https://github.com/oleg-shilo/cs-script/blob/master/src/CSScriptLib/src/Client.SingleFileBuild/Program.cs) after publishing so there is something new in your case.

Can you share the solution you are testing so I can have a look? A sanitized version of it. Or a hello-world example that demonstrates the problem.

angrynik commented 1 year ago

Right, I added a class library with one class that runs a script.

namespace BreakLib;
public class BreakClass
{
    public static string RunScript()
    {
        var calc = CSScript.Evaluator
                           .Eval(@"using System;
                           public class Script
                           {
                               public int Sum(int a, int b)
                               {
                                   return a+b;
                               }
                           }
                           return new Script();");

        int sum = calc.Sum(1, 2);
        return $"sum is {sum}";
    }
}

Then I reference the project and call that method from the Client.SingleFileBuild sample.

...
...
var result = BreakLib.BreakClass.RunScript();
Console.WriteLine(result);

This is the setup I use, where the script runner is in a dedicated project and I call that from my apps.

oleg-shilo commented 1 year ago

I repeated the test. Seems to work as expected. I have attached the test project cs-script.#343.zip

angrynik commented 1 year ago

Hmmm. Can I ask how you publish? I do it through Visual Studio with these settings, and managed to crash this sample.

image
oleg-shilo commented 1 year ago

it's in the code program.cs.

image

image

image

angrynik commented 1 year ago

It looks like the Deployment Mode must be set to Self-contained.

oleg-shilo commented 1 year ago

Please have a look at "publish" folder I content on the screenshot I provided. It does contain only a single executable. It is published as self-contained simply because self-contained mode is configured in the project file (very first screenshot).

Just to ensure we are on the same page I have rebuilt the project with explicit CLI parameters for self-containment:

dotnet publish -c Release --self-contained true

The outcome is the same.

Can you please share the project sample, and the CLI command to build it? So we are working on the same things.

angrynik commented 1 year ago

Sorry for the confusion, I am using your project and yes it does work in self contained mode. The problem arises when deployed as Framework dependent, or self-contained = false.

I have always used Framework dependent deployments, due to a smaller file size, but I'm thinking self-contained does have benefits, and the bandwidth is not such a big deal these days. So deploying self-contained as the solution is not a problem.

oleg-shilo commented 1 year ago

OK, but... How did you publish your project anyway? I would like to see the scenario that is still not covered, so I can possibly address it.

angrynik commented 1 year ago

dotnet publish -c Release --no-self-contained

oleg-shilo commented 1 year ago

Great, it works.

In the code I analyze if it is a single-file deployment. The analysis is done like this:


public static bool IsSingleFileApplication { get; } = "".GetType().Assembly.Location.IsEmpty();

. . .

catch (Exception ex)
{
#if class_lib
    if (Runtime.IsSingleFileApplication)
        return null; // a single file compilation (published with PublishSingleFile option)
#endif
    throw;
}                

But if the app compiled as in your case the IsSingleFileApplication does not detect "danger of calling CodeBase".

The updated version with the fallback exception handler looks like this:

catch (Exception ex)
{
#if class_lib
    if (Runtime.IsSingleFileApplication)
        return null; // a single file compilation (published with PublishSingleFile option)
    else if (ex.Message.Contains("CodeBase is not supported on assemblies loaded from a single-file bundle")
            || ex.StackTrace.Contains("at System.Reflection.RuntimeAssembly.get_CodeBase()"))
        return null;
#endif
    throw;
}

And it works just fine. image

oleg-shilo commented 1 year ago

I do not want to do premature release so will release this change as a pre-release.

You can probably appreciate now why I always ask for a VS test project 😄 A verbal description of the test actions is never as accurate as code.

oleg-shilo commented 1 year ago

Done, you can get the latest v4.8.4-pre pre-release from nuget.org.

dotnet add package CS-Script --version 4.8.4-pre
angrynik commented 12 months ago

It works! Amazing thank you.