oleg-shilo/cs-script

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

Closed this issue ยท 22 comments

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");

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.

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

dotnet/roslyn#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.

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.

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.

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

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

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.

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.

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.

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.

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

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

image

it's in the code program.cs.

image

image

image

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

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.

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.

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.

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

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

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.

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

It works! Amazing thank you.