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=1.0.0.0, 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.
Build 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=1.0.0.0, 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
C:\Test\BuildDependencies>

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.

mandag 29. desember 2008

MSbuild of VS solution under target clean is just plain wrong

One of our solutions has been bothering us for a long time so I finally decided to make a simple demo of the problem.

In my demo solution I have three library type C# projects Level0, Level1 and Level2. Adding and removing a (project) reference from Level1 to Level0 and building with both default and clean targets using MSBuild illustrates the problem. Beneath is the ouput of the clean build without the reference in place:

C:\Test\BuildDependencies>msbuild C:\Test\BuildDependencies\BuildDependencies.sln /t:clean
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/29/2008 4:01:39 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\Level0\bin\Debug\Level0.dll". Deleting file "C:\Test\BuildDependencies\Level0\bin\Debug\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)). Deleting file "C:\Test\BuildDependencies\Level1\bin\Debug\Level1.dll". Deleting file "C:\Test\BuildDependencies\Level1\bin\Debug\Level1.pdb". Deleting file "C:\Test\BuildDependencies\Level1\bin\Debug\Level1.tlb". Deleting file "C:\Test\BuildDependencies\Level1\obj\Debug\Level1.dll". Deleting file "C:\Test\BuildDependencies\Level1\obj\Debug\Level1.pdb".EntityClean: Successfully cleaned the output for 0 EDMX files.Done Building Project "C:\Test\BuildDependencies\Level1\Level1.csproj" (Clean target(s)).
Project "C:\Test\BuildDependencies\BuildDependencies.sln" (1) is building "C:\Test\BuildDependencies\Level2\Level2.csproj" (4) on node 0 (Clean target(s)). Deleting file "C:\Test\BuildDependencies\Level2\bin\Debug\Level2.dll". Deleting file "C:\Test\BuildDependencies\Level2\bin\Debug\Level2.pdb". Deleting file "C:\Test\BuildDependencies\Level2\obj\Debug\Level2.dll". Deleting file "C:\Test\BuildDependencies\Level2\obj\Debug\Level2.pdb".EntityClean: Successfully cleaned the output for 0 EDMX files.Done Building Project "C:\Test\BuildDependencies\Level2\Level2.csproj" (Clean target(s)).
Done Building Project "C:\Test\BuildDependencies\BuildDependencies.sln" (clean target(s)).
Build succeeded. 0 Warning(s) 0 Error(s)


Here is the output with the reference (Level1 -> Level0) in place:

C:\Test\BuildDependencies>msbuild C:\Test\BuildDependencies\BuildDependencies.sln /t:clean
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/29/2008 4:02:14 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\Level0\bin\Debug\Level0.dll". Deleting file "C:\Test\BuildDependencies\Level0\bin\Debug\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\Level2\Level2.csproj" (3) on node 0 (Clean target(s)). Deleting file "C:\Test\BuildDependencies\Level2\bin\Debug\Level2.dll". Deleting file "C:\Test\BuildDependencies\Level2\bin\Debug\Level2.pdb". Deleting file "C:\Test\BuildDependencies\Level2\obj\Debug\Level2.dll". Deleting file "C:\Test\BuildDependencies\Level2\obj\Debug\Level2.pdb".EntityClean: Successfully cleaned the output for 0 EDMX files.Done Building Project "C:\Test\BuildDependencies\Level2\Level2.csproj" (Clean target(s)).
Project "C:\Test\BuildDependencies\BuildDependencies.sln" (1) is building "C:\Test\BuildDependencies\Level1\Level1.csproj" (4) on node 0 (Clean target(s)). Deleting file "C:\Test\BuildDependencies\Level1\bin\Debug\Level1.dll". Deleting file "C:\Test\BuildDependencies\Level1\bin\Debug\Level1.pdb". Deleting file "C:\Test\BuildDependencies\Level1\bin\Debug\Level1.tlb". Deleting file "C:\Test\BuildDependencies\Level1\bin\Debug\Level0.dll". Deleting file "C:\Test\BuildDependencies\Level1\bin\Debug\Level0.pdb". Deleting file "C:\Test\BuildDependencies\Level1\obj\Debug\ResolveAssemblyReference.cache". Deleting file "C:\Test\BuildDependencies\Level1\obj\Debug\Level1.dll". Deleting file "C:\Test\BuildDependencies\Level1\obj\Debug\Level1.pdb".EntityClean: Successfully cleaned the output for 0 EDMX files.Done Building Project "C:\Test\BuildDependencies\Level1\Level1.csproj" (Clean target(s)).
Done Building Project "C:\Test\BuildDependencies\BuildDependencies.sln" (clean target(s)).

Now, the point is that allthough adding the reference alters the build order MSBuild does not build the clean targets in the correct order.

If you don't beleive me have a look at the .sln.cache that gets generated (if you use MSBuild V3 and VS2008). Compare the Clean and the Build targets and you will see that the build order is the same (BuildLevel0 is done before BuildLevel1 etc.).

Cleaning a referer after the referee (is that the right term?) will fail in some cases. Specifically it will fail if you need the refered assembly to clean the current assembly.

When do you need the referred assembly to clean the current one? Well our build breaks during clean with the "MSB3395: Cannot unregister assembly" error. It seems our referring assembly needs some type in the referred assembly to properly unregister. I am currently investigating why this is the case but the result doesn't change the fact that MSBuild has an issue here.

It's obvious if you think about it. If you have access to dependency information you should always clean in the opposite order of the way you normally build. Am I the only one getting this?!? Shouldn't this be obvious to the guys in redmond too?!?

tirsdag 16. desember 2008

Good overview on the motivations behind choice of Team Build structure

http://blog.nwcadence.com/2007/11/12/how-many-team-build-types-should-i-have-for-one-solution/

AdditionalReferencePaths can/will screw up your Team Build

Just spent several days pondering over a Team Build that only builds the first time after a thorough clean of the build directory. Prelimenary conclusion is as follows:

It seems like the AdditionalReferencePaths option is given a higher priority than the framework itself. This leads to resolving things like System.dll from the output path instead of from the relevant target framework(!!).

The reason I started thinking this is that the it was obvious from the build log that our output path was beeing preferred for stuff like System.dll, System.Data.dll etc. We are deliberately not using CopyLocal and simultaneously have set all our $(Output) to one directory. All file references are picked up from this folder.

I tested out this conclusion by simply removing the one AdditionalReferencePaths inclusion we had and it did the trick.

Now, this preference of AdditionalReferencePaths over target framework path cannot be a good descicion from MS can it?

tirsdag 9. desember 2008

Bug in how LINQ2SQL tracks changes?

Just realized (through a reported bug in my code) that you have to be careful with the Attach to DataContext pattern (Note to self. Remember to blog on this).

It seems that even though attaching a newly created POCO to your DataContext will start change tracking, setting properties to the "type default" value doesn't add an entry to the change log for the DataContext.

I will try to verify this partly by taking alook under the hood of LINQ2SQL (thanks Lutz ;)) and partly by working around this in my code. Should this be considered a bug in LINQ2SQL? I for one am open for debate.

Will be back with more on this.