Build of extensions can fail if path to project contains '#'
See original GitHub issueShort description
During the build CreatePkgDef
will fail if the path to contains a #
.
It doesn’t have to be in the solution folder or project folder itself.
Anywhere in the path will trigger it, e.g. C:\Test#\Samples
, with Samples
being the solution folder.
The following versions of Microsoft.VSSDK.BuildTools
are tested:
17.1.9-preview1
17.4.2119
(latest)
What happended
I couldn’t get https://github.com/VsixCommunity/Samples to build initially. It turned out that it was because of a #
symbol in the path where I cloned the repository.
Example using C:\Test#\Samples
CreatePkgDef : error :
ProvideCodeBaseAttribute: Could not load specified assembly: 'Community.VisualStudio.Toolkit'
reason: Could not load file or assembly 'file:///C:\Community.VisualStudio.Toolkit.dll' or one of its dependencies.
The system cannot find the file specified.
After trying everything else, I finally thought to remove the #
. Then it built without a problem.
I was curious why that would be the case, so I’ve spent a good amount of time tracking it down.
Cause
tldr; System.Reflection.Assembly.CodeBase
is used instead of System.Reflection.Assembly.EscapedCodeBase
I’m not sure whether it’s fair to say the bug is in CreatePkgDef
or ProvideCodeBaseAttribute
, but the way the bug “works” is as follows:
The samples use Community.VisualStudio.Toolkit which has the assembly attribute [assembly: ProvideCodeBase(AssemblyName = “Community.VisualStudio.Toolkit”)]
For brevity, I’ve trimmed the code to show just the steps that involves the path. I’ll also use C# features not available in DotNetFramework. That’s also how it’s been decompiled in VS.
The path to the VSIX assembly is C:\Test#\Samples\InsertGuid\bin\Debug\InsertGuid.dll
The path to the dll is C:\Test#\Samples\InsertGuid\bin\Debug\Community.VisualStudio.Toolkit.dll
CreatePkgDef
public static string DoCreatePkgDef( InputArguments inputArguments )
{
using PkgDefContext pkgDefContext = new PkgDefContext( pkgDefFileHive, registerUsing, CreatePkgDef.consoleMode );
CreatePkgDef.ProcessAssembly( inputArguments.FileName, pkgDefFileHive, pkgDefContext, true, RegistrationMode.PkgDef );
}
public static void ProcessAssembly( string fileName, Hive hive, PkgDefContext context, bool register, RegistrationMode mode )
{
Assembly assembly = Assembly.LoadFrom(fileName); // @"C:\Test#\Samples\InsertGuid\bin\Debug\InsertGuid.dll"
context.ComponentAssembly = assembly;
var sortedList = new SortedList<object, List<RegistrationAttribute>>(AssemblyOrTypeComparer.Default);
foreach ( var attr in assembly.GetCustomAttributes( true ) )
{
if ( attr is RegistrationAttribute )
{
sortedList[assembly].Add( attr );
}
}
foreach ( (_,list) in sortedList )
{
foreach ( var attr in list )
{
attr.Register( context ); // attr is of type ProvideCodeBaseAttribute which is derived from ProvideDependentAssemblyAttribute
}
}
}
PkgDefContext
public sealed class PkgDefContext : RegistrationContext, IDisposable
{
public Assembly ComponentAssembly { get; set; }
public override string CodeBase => ComponentAssembly.CodeBase; // this leads to the bug
}
ProvideDependentAssemblyAttribute
public override void Register(RegistrationContext context)
{
UpdateCurrentAssembly(context.CodeBase); // "file:///C:/Test#/Samples/InsertGuid/bin/Debug/InsertGuid.dll"
}
private void UpdateCurrentAssembly(string targetCodeBase) // "file:///C:/Test#/Samples/InsertGuid/bin/Debug/InsertGuid.dll"
{
targetCodeBase = new Uri(targetCodeBase).LocalPath; // @"C:\Test"
//..
string directoryName = Path.GetDirectoryName(targetCodeBase); // @"C:\"
string path = AssemblyName + ".dll"; // "Community.VisualStudio.Toolkit.dll"
directoryName = Path.Combine(directoryName, path); // @"C:\Community.VisualStudio.Toolkit.dll"
CurrentAssembly = LoadAssembly(directoryName, out errorReason); // this fails because of a FileNotFoundException
}
Fix
RegistrationContext.CodeBase
’s value comes from Assembly.CodeBase
.
Assembly.CodeBase
is be functionally similar to "file:///" + path.Replace('\\','/')
.
Assembly.EscapedCodeBase
is functionally equivalent to new Guid( new Guid( "file:///" ), path).AbsoluteUri
.
assembly = Assembly.LoadFrom( @"C:\Test#\Samples\InsertGuid\bin\Debug\InsertGuid.dll" );
assembly.CodeBase == "file:///C:/Test#/Samples/InsertGuid/bin/Debug/InsertGuid.dll"
assembly.EscapedCodeBase == "file:///C:/Test%23/Samples/InsertGuid/bin/Debug/InsertGuid.dll"
new Uri( assembly.CodeBase ).LocalPath == @"C:\Test"
new Uri( assembly.EscapedCodeBase ).LocalPath == @"C:\Test#\Samples\InsertGuid\bin\Debug\InsertGuid.dll"
Option 1: Change PkgDefContext.CodeBase property
- return this.ComponentAssembly.CodeBase;
+ return this.ComponentAssembly.EscapedCodeBase;
Option 2: Change ProvideDependentAssemblyAttribute.Register method
- UpdateCurrentAssembly(context.CodeBase);
+ UpdateCurrentAssembly(context.EscapedCodeBase);
This would also require changes to RegistrationAttribute.RegistrationContext
since EscapedCodeBase
is not part of that class.
It could be implemented there as public virtual string EscapedCodeBase => CodeBase;
.
PkgDefContext
could then override it public override string EscapedCodeBase => ComponentAssembly.EscapedCodeBase;
.
One advantage with option is that Register
could then try CodeBase
first, and if that fails it could retry using EscapedCodeBase
.
Risks
This could break stuff, so it might be better to try CodeBase
first, and if not found, try EscapedCodeBase
.
Issue Analytics
- State:
- Created 9 months ago
- Comments:6 (3 by maintainers)
Top GitHub Comments
@BertanAygun This is not specific to the Community Toolkit. You can create a regular VSIX project from the standard template, add the
ProvideCodeBase
attribute and it will fail to compile if the directory contains a#
.This is only at build time, but unfortunately, we are not able to change the folder name at this point.
This prevents us from using the Community extensibility template extension. Just means more work for us.