Skip to content

Commit b686515

Browse files
romangolevjmcouffin
authored andcommitted
feat: change assembly generation module to Roslyn
1 parent cffdb8a commit b686515

File tree

6 files changed

+139
-122
lines changed

6 files changed

+139
-122
lines changed

dev/pyRevitLoader/Directory.Build.targets

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@
4545
<ItemGroup Condition="'$(UseIronPython)' != 'false'">
4646
<EmbeddedResource Include="$(IronPythonStdLibDir)\$(IronPythonStdLib)"/>
4747
</ItemGroup>
48-
48+
49+
<ItemGroup Condition="'$(UseRoslyn)' == 'true'">
50+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" />
51+
</ItemGroup>
52+
4953
<Target Name="Deploy" AfterTargets="AfterBuild">
5054
<ItemGroup>
5155
<AllFilesToCopy Include="$(TargetDir)\**\*.dll"
Lines changed: 54 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
using System;
22
using System.IO;
3-
using System.Reflection;
4-
using System.Reflection.Emit;
3+
using System.Text;
4+
using System.Collections.Generic;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp;
57
using pyRevitAssemblyBuilder.SessionManager;
8+
using System.Reflection;
69

710
namespace pyRevitAssemblyBuilder.AssemblyMaker
811
{
@@ -30,58 +33,75 @@ public ExtensionAssemblyInfo BuildExtensionAssembly(WrappedExtension extension)
3033
Directory.CreateDirectory(outputDir);
3134

3235
string outputPath = Path.Combine(outputDir, fileName);
36+
string code = _typeGenerator.GenerateExtensionCode(extension);
37+
38+
File.WriteAllText(Path.Combine(outputDir, $"{extension.Name}_Generated.cs"), code);
39+
40+
var syntaxTree = CSharpSyntaxTree.ParseText(code);
3341

34-
// TODO: Put the right version here
35-
var asmName = new AssemblyName(extension.Name)
42+
var references = new List<MetadataReference>
3643
{
37-
Version = new Version(1, 0, 0, 0)
44+
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
45+
MetadataReference.CreateFromFile(typeof(Console).Assembly.Location),
46+
MetadataReference.CreateFromFile(@"C:\Program Files\Autodesk\Revit 2025\RevitAPI.dll"),
47+
MetadataReference.CreateFromFile(@"C:\Program Files\Autodesk\Revit 2025\RevitAPIUI.dll"),
48+
MetadataReference.CreateFromFile(@"C:\Users\Equipo\dev\romangolev\pyRevit\bin\netcore\engines\IPY342\pyRevitLabs.PyRevit.Runtime.2025.dll")
3849
};
3950

40-
string fileNameWithoutExt = Path.GetFileNameWithoutExtension(outputPath);
51+
string runtimePath = Path.Combine(Path.GetDirectoryName(typeof(object).Assembly.Location), "System.Runtime.dll");
52+
if (File.Exists(runtimePath))
53+
references.Add(MetadataReference.CreateFromFile(runtimePath));
4154

42-
#if NETFRAMEWORK
43-
var domain = AppDomain.CurrentDomain;
44-
var asmBuilder = domain.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.RunAndSave, outputDir);
45-
var moduleBuilder = asmBuilder.DefineDynamicModule(fileNameWithoutExt, fileName);
46-
#else
47-
var asmBuilder = AssemblyBuilder.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run);
48-
var moduleBuilder = asmBuilder.DefineDynamicModule(fileNameWithoutExt);
49-
#endif
50-
foreach (var cmd in extension.GetAllCommands())
55+
var compilation = CSharpCompilation.Create(
56+
Path.GetFileNameWithoutExtension(outputPath),
57+
syntaxTrees: new[] { syntaxTree },
58+
references: references,
59+
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, optimizationLevel: OptimizationLevel.Release));
60+
61+
using (var dllStream = new FileStream(outputPath, FileMode.Create))
5162
{
52-
_typeGenerator.DefineCommandType(extension, cmd, moduleBuilder);
63+
var result = compilation.Emit(dllStream);
64+
if (!result.Success)
65+
{
66+
Console.WriteLine("=== Roslyn Compilation Errors ===");
67+
foreach (var diagnostic in result.Diagnostics)
68+
{
69+
if (diagnostic.Severity == DiagnosticSeverity.Error)
70+
{
71+
Console.WriteLine($"ERROR {diagnostic.Id}: {diagnostic.GetMessage()}");
72+
Console.WriteLine($"Location: {diagnostic.Location.GetLineSpan()}");
73+
}
74+
else if (diagnostic.Severity == DiagnosticSeverity.Warning)
75+
{
76+
Console.WriteLine($"WARNING {diagnostic.Id}: {diagnostic.GetMessage()}");
77+
}
78+
}
79+
Console.WriteLine("=================================");
80+
throw new Exception("Assembly compilation failed");
81+
}
5382
}
54-
#if NETFRAMEWORK
55-
asmBuilder.Save(fileName);
56-
#else
57-
var generator = new Lokad.ILPack.AssemblyGenerator();
58-
generator.GenerateAssembly(asmBuilder, outputPath);
59-
#endif
6083

6184
return new ExtensionAssemblyInfo(
6285
name: extension.Name,
6386
location: outputPath,
64-
isReloading: CheckIfExtensionAlreadyLoaded(extension.Name)
87+
isReloading: false
6588
);
6689
}
6790

68-
private bool CheckIfExtensionAlreadyLoaded(string extensionName)
69-
{
70-
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
71-
{
72-
if (asm.GetName().Name == extensionName)
73-
return true;
74-
}
75-
return false;
76-
}
77-
7891
private static string GetStableHash(string input)
7992
{
8093
using (var sha1 = System.Security.Cryptography.SHA1.Create())
8194
{
82-
var hash = sha1.ComputeHash(System.Text.Encoding.UTF8.GetBytes(input));
95+
var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(input));
8396
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
8497
}
8598
}
99+
public void LoadAssembly(ExtensionAssemblyInfo assemblyInfo)
100+
{
101+
if (!File.Exists(assemblyInfo.Location))
102+
throw new FileNotFoundException("Assembly file not found", assemblyInfo.Location);
103+
104+
Assembly.LoadFrom(assemblyInfo.Location);
105+
}
86106
}
87107
}
Lines changed: 76 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,96 @@
11
using System;
2-
using System.ComponentModel;
32
using System.IO;
3+
using System.Text;
44
using System.Linq;
5-
using System.Reflection;
6-
using System.Reflection.Emit;
75
using pyRevitAssemblyBuilder.SessionManager;
6+
using Autodesk.Revit.Attributes;
87

98
namespace pyRevitAssemblyBuilder.AssemblyMaker
109
{
1110
public class CommandTypeGenerator
1211
{
13-
private static Type _scriptCommandBaseType;
14-
15-
public CommandTypeGenerator()
12+
public string GenerateExtensionCode(WrappedExtension extension)
1613
{
17-
var runtimeAssembly = AppDomain.CurrentDomain.GetAssemblies()
18-
.FirstOrDefault(a => a.FullName.StartsWith("pyRevitLabs.PyRevit.Runtime"));
19-
20-
if (runtimeAssembly == null)
21-
throw new InvalidOperationException("ScriptCommand base types could not be resolved. Ensure PyRevitLabs.PyRevit.Runtime is loaded.");
22-
23-
_scriptCommandBaseType = runtimeAssembly.GetType("PyRevitLabs.PyRevit.Runtime.ScriptCommand")
24-
?? throw new InvalidOperationException("ScriptCommand type not found in runtime assembly.");
14+
var sb = new StringBuilder();
15+
sb.AppendLine("#nullable disable");
16+
//sb.AppendLine("using System;");
17+
//sb.AppendLine("using System.ComponentModel;");
18+
sb.AppendLine("using Autodesk.Revit.Attributes;");
19+
//sb.AppendLine("using Autodesk.Revit.UI;");
20+
sb.AppendLine("using PyRevitLabs.PyRevit.Runtime;");
21+
foreach (var cmd in extension.GetAllCommands())
22+
{
23+
// Replace invalid characters with underscores for valid C# identifiers
24+
string safeClassName = SanitizeClassName(cmd.UniqueId);
25+
string originalUniqueName = cmd.UniqueId;
26+
string scriptPath = cmd.ScriptPath;
27+
string searchPaths = string.Join(";", new[] {
28+
Path.GetDirectoryName(cmd.ScriptPath),
29+
Path.Combine(extension.Directory, "lib"),
30+
Path.Combine(extension.Directory, "..", "..", "pyrevitlib"),
31+
Path.Combine(extension.Directory, "..", "..", "site-packages")
32+
});
33+
string tooltip = cmd.Tooltip ?? "";
34+
string name = cmd.Name;
35+
string bundle = Path.GetFileName(Path.GetDirectoryName(cmd.ScriptPath));
36+
string extName = extension.Name;
37+
string ctrlId = $"CustomCtrl_%{extName}%{bundle}%{name}";
38+
string engineCfgs = @"{""clean"": false, ""persistent"": false, ""full_frame"": false}";
39+
sb.AppendLine();
40+
sb.AppendLine("[Regeneration(RegenerationOption.Manual)]");
41+
sb.AppendLine("[Transaction(TransactionMode.Manual)]");
42+
sb.AppendLine($"public class {safeClassName} : ScriptCommand");
43+
sb.AppendLine("{");
44+
sb.AppendLine($" public {safeClassName}()");
45+
sb.AppendLine(" : base(");
46+
sb.AppendLine($" @\"{Escape(scriptPath)}\",");
47+
sb.AppendLine($" @\"{Escape(scriptPath)}\",");
48+
sb.AppendLine($" @\"{Escape(searchPaths)}\",");
49+
sb.AppendLine(" \"\",");
50+
sb.AppendLine(" \"\",");
51+
sb.AppendLine($" @\"{Escape(tooltip)}\",");
52+
sb.AppendLine($" \"{Escape(name)}\",");
53+
sb.AppendLine($" \"{Escape(bundle)}\",");
54+
sb.AppendLine($" \"{Escape(extName)}\",");
55+
sb.AppendLine($" \"{originalUniqueName}\",");
56+
sb.AppendLine($" \"{Escape(ctrlId)}\",");
57+
sb.AppendLine(" \"(zero-doc)\",");
58+
sb.AppendLine($" @\"{EscapeJsonForVerbatimString(engineCfgs)}\"");
59+
sb.AppendLine(" )");
60+
sb.AppendLine(" {");
61+
sb.AppendLine(" }");
62+
sb.AppendLine("}");
63+
}
64+
return sb.ToString();
2565
}
2666

27-
public void DefineCommandType(WrappedExtension extension, FileCommandComponent command, ModuleBuilder moduleBuilder)
67+
private static string SanitizeClassName(string name)
2868
{
29-
// TODO: try to build assemblies in the isolated context with referencies
30-
//var typeBuilder = moduleBuilder.DefineType(
31-
// fullTypeName,
32-
// TypeAttributes.Public | TypeAttributes.Class,
33-
// _scriptCommandBaseType
34-
//);
35-
36-
var typeBuilder = moduleBuilder.DefineType(
37-
command.UniqueId,
38-
TypeAttributes.Public | TypeAttributes.Class
39-
);
40-
// Add Description attribute
41-
var descAttrCtor = typeof(DescriptionAttribute).GetConstructor(new[] { typeof(string) });
42-
var descAttr = new CustomAttributeBuilder(descAttrCtor, new object[] { command.Tooltip ?? "" });
43-
typeBuilder.SetCustomAttribute(descAttr);
44-
45-
var ctorParams = Enumerable.Repeat(typeof(string), 13).ToArray();
46-
47-
var baseCtor = _scriptCommandBaseType.GetConstructor(
48-
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
49-
null, ctorParams, null
50-
);
51-
52-
if (baseCtor == null)
53-
throw new InvalidOperationException("Could not find ScriptCommand base constructor.");
54-
55-
var ctorBuilder = typeBuilder.DefineConstructor(
56-
MethodAttributes.Public,
57-
CallingConventions.Standard,
58-
Type.EmptyTypes
59-
);
60-
61-
var il = ctorBuilder.GetILGenerator();
62-
63-
// Setup constructor arguments
64-
string scriptPath = command.ScriptPath;
65-
string configScriptPath = null;
66-
string searchPaths = string.Join(";", new[]
67-
{
68-
Path.GetDirectoryName(command.ScriptPath),
69-
Path.Combine(extension.Directory, "lib"),
70-
Path.Combine(extension.Directory, "..", "..", "pyrevitlib"),
71-
Path.Combine(extension.Directory, "..", "..", "site-packages")
72-
});
73-
string args = "";
74-
string help = "";
75-
string tooltip = command.Tooltip ?? "";
76-
string name = command.Name;
77-
string bundle = Path.GetFileName(Path.GetDirectoryName(command.ScriptPath));
78-
string extName = extension.Name;
79-
string uniqueName = command.UniqueId;
80-
string ctrlId = $"CustomCtrl_%{extName}%{bundle}%{name}";
81-
string context = "(zero-doc)";
82-
string engineCfgs = "{\"clean\": false, \"persistent\": false, \"full_frame\": false}";
83-
84-
foreach (var arg in new[]
85-
{
86-
scriptPath, configScriptPath, searchPaths, args, help,
87-
tooltip, name, bundle, extName, uniqueName, ctrlId, context, engineCfgs
88-
})
69+
var sb = new StringBuilder();
70+
foreach (char c in name)
8971
{
90-
il.Emit(OpCodes.Ldstr, arg ?? string.Empty);
72+
sb.Append(char.IsLetterOrDigit(c) ? c : '_');
9173
}
74+
return sb.ToString();
75+
}
9276

93-
il.Emit(OpCodes.Call, baseCtor);
94-
il.Emit(OpCodes.Ret);
77+
private static string Escape(string str)
78+
{
79+
return str?
80+
.Replace("\"", "\"\"") // for verbatim strings
81+
.Replace("\r", "")
82+
.Replace("\n", "\\n")
83+
?? "";
84+
}
85+
86+
private static string EscapeJsonForVerbatimString(string jsonStr)
87+
{
88+
// This method specifically handles JSON strings in verbatim string literals
89+
// It properly escapes quotes by doubling them
90+
if (string.IsNullOrEmpty(jsonStr))
91+
return "";
9592

96-
typeBuilder.CreateType();
93+
return jsonStr.Replace("\"", "\"\"");
9794
}
9895
}
9996
}

dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/HookManager.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using System.Collections.Generic;
33
using System.IO;
44
using System.Linq;
5-
using pyRevitAssemblyBuilder.Shared;
65

76
namespace pyRevitAssemblyBuilder.SessionManager
87
{

dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/SessionManagerService.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
using System;
2-
using System.Diagnostics;
3-
using System.Threading.Tasks;
4-
using pyRevitAssemblyBuilder.AssemblyMaker;
1+
using pyRevitAssemblyBuilder.AssemblyMaker;
52

63
namespace pyRevitAssemblyBuilder.SessionManager
74
{
@@ -31,6 +28,7 @@ public void LoadSession()
3128
foreach (var ext in extensions)
3229
{
3330
var assmInfo = _assemblyBuilder.BuildExtensionAssembly(ext);
31+
_assemblyBuilder.LoadAssembly(assmInfo);
3432
_uiManager.BuildUI(ext, assmInfo);
3533
_hookManager.RegisterHooks(ext);
3634
}

dev/pyRevitLoader/pyRevitAssemblyBuilder/pyRevitAssemblyBuilder.csproj

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@
1010
<TargetFrameworks>net48;net8.0-windows</TargetFrameworks>
1111
<IronPythonVersion>IPY342</IronPythonVersion>
1212
<Version>3.4.2</Version>
13+
<UseRoslyn>true</UseRoslyn>
14+
<LangVersion>10.0</LangVersion>
1315
</PropertyGroup>
14-
<ItemGroup>
15-
<PackageReference Include="Lokad.ILPack" Version="0.3.0" />
16-
</ItemGroup>
1716
</Project>

0 commit comments

Comments
 (0)