daveaglick / Scripty

Tools to let you use Roslyn-powered C# scripts for code generation
MIT License
621 stars 69 forks source link

Automatically find Roslyn DLLs for scripts. #41

Open salmelo opened 8 years ago

salmelo commented 8 years ago

I don't know if this feasible within the Roslyn script API, but it would be nice if all #r Microsoft.CodeAnalysis.* directives automatically resolved to the same Roslyn DLLs Visual Studio / Scripty itself are using.

daveaglick commented 8 years ago

This can probably be done. The Roslyn scripting engine allows you to provide a handler for the directives, so if Scripty implements a custom handler it could return the currently used version of the Roslyn libraries. I'll take a look at implementing this next week.

StingyJack commented 7 years ago

This looks like its present on line 56 and added around this same time frame. @salmelo - is it still outstanding?

salmelo commented 7 years ago

From what I can tell that loads Microsoft.CodeAnalysis.Workspaces automatically, but not, for example, Microsoft.CodeAnalysis.CSharp. The suggestion was to have Scripty automatically point your script to the same version of that dll it's using, given the directive #r "Microsoft.CodeAnalysis.CSharp.dll", or the same folder it's looking for roslyn dlls at least, much like you can reference #r "mscorlib.dll" without having to provide a full file path (writing directives from memory, so apologies if the syntax is off.) Just automatically referencing it, and any other relevant dlls, the same way Workspaces is now would probably also suffice.

For reference, the suggestion came from hope that it would help solve #40. I don't know for sure that it would, but even if it doesn't easy access to all the roslyn types seems like something you'd want in a Roslyn centric code generator.

StingyJack commented 7 years ago

So you want to avoid things like this (me too)...

#r "..\..\..\packages\Microsoft.CodeAnalysis.CSharp.dll"

... and use a convention like this...

#r "Microsoft.CodeAnalysis.*"

... and any assemblies that match that name, that are available to ScriptEngine (via something like AppDomain.CurrentDomain.GetAssemblies(); be added to the ScriptOptions..

What would you expect to happen if nothing was found matching that directive? Error and dont execute the script? Howabout if it found at least one?

Would there be any value in attempting to resolve the shorthand references by looking in packages?

salmelo commented 7 years ago

Essentially, yeah.

I guess if it didn't find anything I'd expect a result similar to if it didn't find any other #r directive? I'm not sure what you mean by "if it found at least one."

I don't know if searching in packages would be super helpful or not. The project I was working on when I suggested it, and every other project I've used Scripty in since, didn't reference Roslyn at all; I was only using it for code gen via Scripty. So that wouldn't have helped there. I suspect that would be the case for a lot of other projects as well, but of course I can only speak to my own for sure.

It might be worthwhile to search packages for any relative-path assemblies that aren't resolvable from the script location (assuming that's the default 'working folder',) since wanting code gen to use types that are also used in the project proper probably isn't an unheard of use case, but I think that's a bit separate from the specific issue of getting at Roslyn assemblies whether or not the host project has them.

If you did both I could imagine wanting to skip the packages search for Roslyn assemblies, or only do it after failing to find the assembly in the script engine, to avoid ending up in a weird situation where the script references one version of Microsoft.CodeAnalysis.Workspaces and a mismatched version of Microsoft.CodeAnalysis.CSharp, for example.

daveaglick commented 7 years ago

So, just noticed my original comment "next week"...7 months ago 😬

Why not just automatically add all the Roslyn assemblies that Scripty is referencing as script references, whether they're needed or explicitly referenced or not? That would probably be easier than trying to parse out whether the #r directive is referring to a Roslyn assembly. Would that solve the problem? Any complications I'm not thinking of?

salmelo commented 7 years ago

None that I can think of, unless there's an overhead issue from referencing assemblies that aren't used. I doubt it would be a problem for scripts that are run at design time tho.

StingyJack commented 7 years ago

So this or similar would get the scripty.core references. Any need to get ambient project references too?


            var assembliesToRef = new List<Assembly>
            {
                typeof(object).Assembly, //mscorlib
                typeof(Project).Assembly, // Microsoft.CodeAnalysis.Workspaces
                typeof(Microsoft.Build.Evaluation.Project).Assembly, // Microsoft.Build
                typeof(ScriptEngine).Assembly, // Scripty.Core
            };

            var curasm = Assembly.GetAssembly(typeof(ScriptEngine)).GetReferencedAssemblies();
            foreach (var asm in curasm)
            {
                var rol = Assembly.ReflectionOnlyLoad(asm.FullName);
                assembliesToRef.Add(rol);
            }
StingyJack commented 7 years ago

There is a bit of a time penalty for one of those things. I didnt look farther into the ambient project metadata reference load time to see where the drag is, but something is there.

This is a snip from traces added to some work in progress that adds the above mentioned parts.

Begin debugging source 'E:\Projects\Scripty\src\Scripty.CustomTool.Tests\TestCsx\ScriptToExecute.csx' with direction EverythingBuiltAsClassesAndReffed
ReferenceCollection instance creation took 0ms
ReferenceCollection add by type took 0ms
ReferenceCollection add script engine references creation took 25ms
ReferenceCollection add executing assembly references creation took 1ms
ReferenceCollection add ambient project metadata creation took 3374ms
ReferenceCollection add additional assemblies instance creation took 0ms
ReferenceCollection total operation timetook 3403ms
Script main compilation unit extracted in 195ms
Script reference directives extracted in 16ms
Script load directive removal took 1ms
Script wrapping took 211ms
Script formatting took 527ms
-------------BEGIN BLOCK-----------------
namespace ScriptyDebugNs
{
    using Scripty.Core;
    using Scripty.Core.Output;

    public static class ScriptyDebugCls
    {
        public static void ScriptyDebugMeth()
        {
            var myString = "thisValue";

//Write using supplied ScriptContext
Output.WriteLine("namespace TestNamespace{class TestClass{public void TestMethod(){}}}");

var replaced = myString.Replace("this", "that");
}
    }
}
-------------END BLOCK-----------------
Precompilation details took 1ms
Script to Assembly compilation resulted in True and took 2491ms

code where those get generated

        /// <summary>
        ///     Builds the usual assembly reference collection, with references to mscorlib, CodeAnalysis, Scripty.Core, MSBuild, 
        /// anything Scripty.Core is referencing, anything the currently executing assembly is referencing, anything the ambient
        /// project is referencing, and whatever is in the additional assemblies parameter.
        /// </summary>
        /// <param name="additionalAssemblies">Any additional assemblies</param>
        /// <returns></returns>
        protected ReferenceCollection BuildReferenceCollection(IEnumerable<Assembly> additionalAssemblies = null)
        {
            var swTotal = Stopwatch.StartNew();
            var swStep = Stopwatch.StartNew();
            var assembliesToRef = new ReferenceCollection();
            swStep.Stop();
            Wt($"ReferenceCollection instance creation took {swStep.ElapsedMilliseconds}ms");

            swStep.Restart();
            assembliesToRef.Add(typeof(object), typeof(Project), typeof(Microsoft.Build.Evaluation.Project), typeof(ScriptEngine));
            swStep.Stop();
            Wt($"ReferenceCollection add by type took {swStep.ElapsedMilliseconds}ms");

            swStep.Restart();
            assembliesToRef.Add(CompilationHelpers.GetReferencedAssemblies(typeof(ScriptEngine).Assembly));
            swStep.Stop();
            Wt($"ReferenceCollection add script engine references creation took {swStep.ElapsedMilliseconds}ms");

            swStep.Restart();
            assembliesToRef.Add(CompilationHelpers.GetReferencedAssemblies(Assembly.GetExecutingAssembly()));
            swStep.Stop();
            Wt($"ReferenceCollection add executing assembly references creation took {swStep.ElapsedMilliseconds}ms");

            swStep.Restart();
            assembliesToRef.Add(ProjectRoot.Analysis.MetadataReferences);
            swStep.Stop();
            Wt($"ReferenceCollection add ambient project metadata creation took {swStep.ElapsedMilliseconds}ms");

            swStep.Restart();
            assembliesToRef.Add(additionalAssemblies);
            swStep.Stop();
            Wt($"ReferenceCollection add additional assemblies instance creation took {swStep.ElapsedMilliseconds}ms");

            swTotal.Stop();
            Wt($"ReferenceCollection total operation time took {swTotal.ElapsedMilliseconds}ms");
            return assembliesToRef;
        }

        protected void Wt(string message)
        {
            Trace.WriteLine(message);
        }

        //from CompilationHelpers
        public static List<Assembly> GetReferencedAssemblies(Assembly targetAssembly)
        {
            var result = new List<Assembly>();

            foreach (var asm in targetAssembly.GetReferencedAssemblies())
            {
                var rol = Assembly.ReflectionOnlyLoad(asm.FullName);
                result.Add(rol);
            }
            return result;
        }
StingyJack commented 7 years ago

full code of above @ https://github.com/StingyJack/Scripty/tree/debug_attempt_1