tirsdag 30. desember 2008

Why it is an error for MSBuild to build clean in the same order as it builds regularily

In a previous posting I wrote about the problem you sometimes face when using MSBuild to build VS solution files.

The investigation of this problem is now complete from my side and I have produced a short and sweet demo that illustrates the problem. The following necessary and sufficienct conditions apply:

- Two assemblies (Level0 and Level1)
- Level1 has a project reference to Level0 with Private=false (CopyLocal=false)
- Both projects have set OutputPath to a common directory (CopyLocal is an abomination and I will blog about why in the future)
- Level1 has "Register for COM interop" and [assembly: ComVisible(true)] in assemblyinfo.cs
- Level1 containing a type that inherits from a type in Level0. The inheriting type must have [ComVisible(false)] or the solution will not build.

The problem is of course that the inheritance across the assembly barrier means that the RegisterAssembly() and UnRegisterAssembly() methods of RegisterServices needs to load Level0 in order to resolve the types in Level1, even though the type that needs resolving is not to be registered for COM interop.

Inside the VS IDE this is no problem since VS in fact does build clean in the proper order, as the following output clearly shows:

------ Clean started: Project: Level1, Configuration: Debug Any CPU ------
------ Clean started: Project: Level0, Configuration: Debug Any CPU ------
========== Clean: 2 succeeded, 0 failed, 0 skipped ==========

MSBuild however does not do this properly, as the following ouput shows:

C:\Test\BuildDependencies>msbuild BuildDependencies.sln /t:clean /v:n
Microsoft (R) Build Engine Version 3.5.30729.1[Microsoft .NET Framework, Version 2.0.50727.3053]Copyright (C) Microsoft Corporation 2007. All rights reserved.
Build started 12/30/2008 2:56:51 PM.Project "C:\Test\BuildDependencies\BuildDependencies.sln" on node 0 (clean target(s)). Building solution configuration "DebugAny CPU".Project "C:\Test\BuildDependencies\BuildDependencies.sln" (1) is building "C:\Test\BuildDependencies\Level0\Level0.csproj" (2) on node 0 (Clean target(s)). Deleting file "C:\Test\BuildDependencies\Binaries\Level0.dll". Deleting file "C:\Test\BuildDependencies\Binaries\Level0.pdb". Deleting file "C:\Test\BuildDependencies\Level0\obj\Debug\Level0.dll". Deleting file "C:\Test\BuildDependencies\Level0\obj\Debug\Level0.pdb".EntityClean: Successfully cleaned the output for 0 EDMX files.Done Building Project "C:\Test\BuildDependencies\Level0\Level0.csproj" (Clean target(s)).
Project "C:\Test\BuildDependencies\BuildDependencies.sln" (1) is building "C:\Test\BuildDependencies\Level1\Level1.csproj" (3) on node 0 (Clean target(s)).C:\Windows\Microsoft.NET\Framework\v3.5\Microsoft.Common.targets(928,9): error MSB3395: Cannot unregister assembly "C:\Test\BuildDependencies\Binaries\Level1.dll". Could not load file or assembly 'Level0, Version=, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.Done Building Project "C:\Test\BuildDependencies\Level1\Level1.csproj" (Clean target(s)) -- FAILED.
Done Building Project "C:\Test\BuildDependencies\BuildDependencies.sln" (clean target(s)) -- FAILED.
"C:\Test\BuildDependencies\BuildDependencies.sln" (clean target) (1) ->"C:\Test\BuildDependencies\Level1\Level1.csproj" (Clean target) (3) ->(UnmanagedUnregistration target) -> C:\Windows\Microsoft.NET\Framework\v3.5\Microsoft.Common.targets(928,9): error MSB3395: Cannot unregister assembly "C:\Test\BuildDependencies\Binaries\Level1.dll". Could not load file or assembly 'Level0, Version=, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.
0 Warning(s) 1 Error(s)
Time Elapsed 00:00:00.54

Which prooves my point (look closely at the output and you will see that the projects are cleaned Level0 first then Level1 which is the opposite of what VS does). The MSBuild team, while copying the internal MSBuild model from VS, obviously forgot to copy the way VS handles the level info.

2 kommentarer:

Brumlemann sa...
Denne kommentaren har blitt fjernet av forfatteren.
Brumlemann sa...

Thanks to Lutz Roeders Reflector (now RedGate) I did a walk of the MSBuild Engine and found this:

private static string GetProjectDependencies(SolutionParser solution, ProjectInSolution project, string subTargetName)
ErrorUtilities.VerifyThrow(project != null, "We should always have a project for this method");
StringBuilder builder = new StringBuilder();
foreach (string str in project.Dependencies)
if (builder.Length != 0)
string projectUniqueNameByGuid = solution.GetProjectUniqueNameByGuid(str);
ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(projectUniqueNameByGuid != null, "SubCategoryForSolutionParsingErrors", new BuildEventFileInfo(solution.SolutionFile), "SolutionParseProjectDepNotFoundError", new object[] { project.ProjectGuid, str });
if ((subTargetName != null) && (subTargetName.Length > 0))
return builder.ToString();

The calling code is inside CreateSolutionProject(...) through calls to the AddTargetsFor<ProjectType> methods (where <ProjectType> is not a type parameter but short for the different project types the engine handles).

Now the problem with the GetProjectDependencies(...) method is that, even though it has a subTargetName parameter (attains values such as "Build", "Clean" etc), it always produce the same dependencies. But as I have, hopefully, clearly demonstrated the dependencies should be reversed when subTargetName == "Clean".

If this was open source I would have just gone ahead and fixed it before submitting it for peer review, but as it stands I have to wait for some of you guys in the MSBuild product group to fix this (alas!).