NuGet / Home

Repo for NuGet Client issues
Other
1.5k stars 253 forks source link

Imports and EnsureNuGetPackageBuildImports target not being added to project when installed #7496

Closed manuelxmarquez closed 4 years ago

manuelxmarquez commented 5 years ago

I created a custom project type and when I add a NuGet package in Visual Studio, the imports and EnsureNuGetPackageBuildImports target is not being added to the project file. The project is using a packages.config file.

I was debugging with dotPeek and found that in VsMSBuildProjectSystem, the imports were being added correctly and I could see it in the Xml property of MSBuildEvaluationProject. But once the install was complete, the project file was not updated and no changes were saved.

I downloaded this repo and built commit version 5.0.0.5495 to debug. I added a save in MicrosoftBuildEvaluationProjectUtility.AddImportStatement AddEnsureImportedTarget(msBuildProject, targetsPath); msBuildProject.ReevaluateIfNecessary(); msBuildProject.Save(); // Added this line

and the project is saved correctly with the imports!

However, at the end of NuGetPackageManager.ExecuteNuGetProjectActionsAsync it calls await nuGetProject.SaveAsync(token); and the changes are reverted back.

I checked the history of VsMSBuildProjectSystem and found that this commit fixes the problem! https://github.com/NuGet/NuGet.Client/commit/4102a9a95ed1d1da3b10391fdfdf3395b284ad56 However, it appears it was reverted shortly after.

I'm using Visual Studio Community 15.8.9

If you're wondering why I used that commit it's because the configure powershell script was not working. I reverted back until it did work. It appears it was trying to run MSBuild from the Enterprise folder, but I only have Community edition. Must have broke somewhere since 5.0.0.5495. Let me know if you would like me to create another issue for that.

jainaashish commented 5 years ago

The commit you mentioned was for a different issue which was specific to Xamarin projects https://bugzilla.xamarin.com/show_bug.cgi?id=61113

It seems your issue could also be specific to your project type. May be some other packages install.ps1 is changing project after AddImports so when it finally try to save the project, it had different data.

Can you debug through await nuGetProject.SaveAsync(token)? this internally calls VsMSBuildProjectSystem.SaveProjectAsync() only...

manuelxmarquez commented 5 years ago

I verified that the package does not have an issue by installing into a VB and CS project. It is also a package I created and know there are no powershell scripts in it.

I poked around the code a bit more and found a few solutions, but only from within NuGet assemblies. It's probably worth noting that I am using CPS since there are some checks for it's compatibility. A little info into what kind of project I am creating; I have a content creation project that builds content using a 'processor' attached to each content item. The 'processors' come from assemblies that implement an interface and are added as assembly references and NuGet packages. It is similar to how XNA Game Content project worked. I developed this project type many years ago, and I'm trying to update it to CPS and include NuGet. I've revisited this a few times in the past couple years but could never get it working, deciding to just edit the project and NuGet files manually.

I found the class CpsProjectSystem which overrides the import methods and has the same fix I found earlier. It seems it was a CPS project issue and this class needs to be created. However, it is an abstract class and only used by Native and JS projects. Also, it overrides the AddGacReference method which I would actually like to behave as it does in the base class. I moved the override into the Native and JS project class, removed the abstract class modifier, and in MSBuildNuGetProjectSystemFactory I added code to create this project type. Since it is the last provider I just check for CPS compatibility before falling back to the default.

// If the project supports CPS then use a CPS project system
var hierarchy = vsProjectAdapter.VsHierarchy;

if (hierarchy != null && hierarchy.IsCapabilityMatch("CPS"))
    return new CpsProjectSystem(vsProjectAdapter, nuGetProjectContext);

// Fall back to the default if we have no special project types
return new VsMSBuildProjectSystem(vsProjectAdapter, nuGetProjectContext);

And it all works fine. The imports are added to the project, references are added, and all references are resolved. However, I don't think there is any way to accomplish this without modifying the NuGet assembly.

So I decided to try getting the PackageReference method to work but ran into a few issues. In order for this to work I need to have LegacyPackageReferenceProjectProvider create the NuGetProject. This requires my project to satisfy the following condition in LegacyPackageReferenceProjectProvider.cs:

PackageReference.Equals(restoreProjectStyle, StringComparison.OrdinalIgnoreCase) || (asVSProject4.PackageReferences?.InstalledPackages?.Length ?? 0) > 0)

However, in order to get to this point, the previous provider NetCorePackageReferenceProjectProvider must fail. In order for that to fail, one of the following if statements must return null in NetCorePackageReferenceProjectProvider.cs:

if (!(string.IsNullOrEmpty(restoreProjectStyle) || restoreProjectStyle.Equals(PackageReference, StringComparison.OrdinalIgnoreCase)))
{
    return null;
}
else if (string.IsNullOrEmpty(targetFramework) && string.IsNullOrEmpty(targetFrameworks))
{
    return null;
}

So the last condition of TargetFramework and TargetFrameworks must be empty, since restoreProjectStyle must be PackageReference for Legacy to succeed. However, all the MSBuild scripts require all three to be set properly for build, reference resolution, etc.

I found a smiliar issued raised here https://github.com/NuGet/Home/issues/5277

If I set all three properties, NetCore will create the project but I managed to set RestoreProjectStyle to a dummy value within VS by implementing IProjectGlobalPropertiesProvider and using:

Empty.PropertiesMap.SetItem("RestoreProjectStyle", "Dummy")

This will prevent NetCore but also prevent Legacy from creating the prject, except when a package reference is installed in the project. Once I add a package reference item by hand, then Legacy creates the NuGetProject and everything works. Doing the same for TargetFramework and TargetFrameworks breaks some other MSBuild tasks and causes errors in VS.

I though maybe I could create my own INuGetProjectProvider and set everything up myself. LegacyPackageReferenceProjectProvider is public but it depends on creating VsManagedLanguagesProjectSystemServices which is private. However, the bigger issue is that when I use this code:

[Export(typeof(NuGet.PackageManagement.VisualStudio.INuGetProjectProvider))]
[Name(nameof(TestProjectProvider))]
[Order(After = nameof(NuGet.PackageManagement.VisualStudio.NetCorePackageReferenceProjectProvider))]
public sealed class TestProjectProvider : NuGet.PackageManagement.VisualStudio.INuGetProjectProvider
{
    public RuntimeTypeHandle ProjectType => throw new NotImplementedException(); // Breakpoint never reaches here!

    public Task<NuGetProject> TryCreateNuGetProjectAsync(IVsProjectAdapter project, NuGet.PackageManagement.VisualStudio.ProjectProviderContext context, bool forceProjectType)
    {
        throw new NotImplementedException(); // Beakpoint never reaches here!
    }
}

I get a bunch of MEF exceptions (and it's not because the code throws exceptions, I put breakpoints there and it's never reached). I couldn't figure out why so I added the same code within the NuGet assemblies and it worked. I have no idea why this won't work when defined in my assemblies... I even tried chaning the namespace to not be within NuGet.

Similar to that, in MSBuildNuGetProjectSystemFactory there is a factory dictionary that maps a project GUID to a project creation delegate, but the dictionary is private. I once tried using reflection to hook this up actually :) and use reflection to create the private classes needed. Seems like the only viable hack left!

Some or most of these issues may be specific to my case but I imagine a lot of project creators are going to have these same issues. I also wanted to write this out to help guide other people on a similar journey. I found very little documentation on how to get this working over my on-again, off-again investigation the past ~1.5 years. I also wanted to document it for myself so I don't have to rediscover this!

I attached two projects, one for a CPS project using packages.config and one using PackageReference. Each has a sample project in a Test folder. They are by no means complete, but might be a good start for someone to build a project, or see how to add extensibility points to NuGet assemblies.

TestProjectPackageReference.zip TestProjectPackagesConfig.zip

Are there any extensibility points I have missed that could possibly work?

nkolev92 commented 4 years ago

Hey @manuelxmarquez

Apologies for the super late replies on this issue.

CPS based projects are expected to work with PackageReference. The imports in PackageReference are handled through different means, which youcan read about here: https://docs.microsoft.com/en-us/nuget/consume-packages/package-references-in-project- files. At this point we don't have any plans to support packages.config + CPS scenarios

manuelxmarquez commented 4 years ago

Thanks for the reply!

I just recently got this working. In case someone is looking to do the same thing, the trick was to implement INuGetProjectProvider like I mentioned above but the provider must be before NetCorePackageReferenceProjectProvider. I then check for my custom project extension and create the project services and NuGetProject object.

LegacyPackageReferenceProjectProvider is a good reference as well as LegacyPackageReferenceProject. Unfortunately, VsManagedLanguagesProjectSystemServices and some other service classes are internal but I just copied that code to my own solution with slight modifications.

This will get NuGet working in your custom project type but the next bit is creating your own target framework and validating NuGet packages. I was able to add items to my own TF and have them verified so only packages with content in a folder related to my custom TF are installed.

Unfortunately, parsing target frameworks is hard coded and I didn't see a way to add a custom parser so the long format must be used. Set the target framework when restoring packages and also use the folder name when creating your packages. NuGet Package Explorer should be able to parse the name as well.

The format must be <TargetFramework>[Name],Version=v[Version],Profile=[Profile]</TargetFramework> I think the Profile can be left out.

Example <TargetFramework>MyFramework,Version=v1.0,Profile=CoolStuff</TargetFramework>

And pack your NuGet package in the correspondign folder. For example: \lib\MyFramework,Version=v1.0,Profile=CoolStuff\AwesomeCode.dll

And AwesomeCode.dll should be added as a reference when building your custom project type.