* [XamlG] builds incrementally, add MSBuild integration tests
Context: https://github.com/xamarin/Xamarin.Forms/pull/2230
The main performance problem with the collection of MSBuild targets in
`Xamarin.Forms.targets` is they don't build incrementally. I addressed
this with `XamlC` using a "stamp" file; however, it is not quite so
easy to setup the same thing with `XamlG`.
They way "incremental" builds are setup in MSBuild, is by specifying
the `Inputs` and `Outputs` of a `<Target />`. MSBuild will partially
build a target when some outputs are not up to date, and skip it
entirely if they are all up to date.
The best docs I can find on MSBuild incremental builds:
https://msdn.microsoft.com/en-us/library/ms171483.aspx
Unfortunately a few things had to happen to make this work for
`XamlG`:
- Define a new target `_FindXamlGFiles` that is invoked before `XamlG`
- `_FindXamlGFiles` defines the `_XamlGInputs` and `_XamlGOutputs`
`<ItemGroup />`'s
- `_FindXamlGFiles` must also define `<Compile />` and `<FileWrites />`,
in case the `XamlG` target is skipped
- `XamlGTask` now needs to get passed in a list of `OutputFiles`,
since we have computed these paths ahead of time
- `XamlGTask` should validate the lengths of `XamlFiles` and
`OutputFiles` match, used error message from MSBuild proper:
a691a44f0e/src/Tasks/Copy.cs (L505)
`XamlG` now builds incrementally!
To give some context on how much improvement we can see with build
times, consider the following command:
msbuild Xamarin.Forms.ControlGallery.Android/Xamarin.Forms.ControlGallery.Android.csproj
If you run it once, it will take a while--this change will not improve
the first build. On the second build with the exact same command, it
*should* be much faster.
Before this commit, the second build on my machine takes:
40.563s
After the change:
23.692s
`XamlG` has cascading impact on build times when it isn't built
incrementally:
- The C# assembly always changes
- Hence, `XamlC` will always run
- Hence, `GenerateJavaStubs` will always run
- Hence, `javac.exe` and `dx.jar` will always run
I am making other improvements like this in Xamarin.Android itself,
that will further improve these times, such as:
https://github.com/xamarin/xamarin-android/pull/1693
~~ New MSBuild Integration Tests ~~
Added some basic MSBuild testing infrastructure:
- Tests write project files to `bin/Debug/temp/TestName`
- Each test has an `sdkStyle` flag for testing the new project system
versus the old one
- `[TearDown]` deletes the entire directory, with a retry for
`IOException` on Windows
- Used the `Microsoft.Build.Locator` NuGet package for locating
`MSBuild.exe` on Windows
- These tests take 2-5 seconds each
So for example, the simplest test, `BuildAProject` writes to
`Xamarin.Forms.Xaml.UnitTests\bin\Debug\temp\BuildAProject(False)\test.csproj`:
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration>Debug</Configuration>
<Platform>AnyCPU</Platform>
<OutputType>Library</OutputType>
<OutputPath>bin\Debug</OutputPath>
<TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
</PropertyGroup>
<ItemGroup>
<Reference Include="mscorlib" />
<Reference Include="System" />
<Reference Include="Xamarin.Forms.Core.dll">
<HintPath>..\..\Xamarin.Forms.Core.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Forms.Xaml.dll">
<HintPath>..\..\Xamarin.Forms.Xaml.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="AssemblyInfo.cs" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Import Project="..\..\..\..\..\.nuspec\Xamarin.Forms.targets" />
<ItemGroup>
<EmbeddedResource Include="MainPage.xaml" />
</ItemGroup>
</Project>
Invokes `msbuild`, and checks the intermediate output for files being
generated.
Tested scenarios:
- Build a simple project
- Build, then build again, and make sure targets were skipped
- Build, then clean, make sure files are gone
- Build, with linked files
- Design-time build
- Call `UpdateDesignTimeXaml` directly
- Build, add a new file, build again
- Build, update timestamp on a file, build again
- XAML file with random XML content
- XAML file with invalid XML content
- A general `EmbeddedResource` that shouldn't go through XamlG
Adding these tests found a bug! `IncrementalClean` was deleting
`XamlC.stamp`. I fixed this by using `<ItemGroup />`, which will be
propery evaluated even if the target is skipped.
~~ Other Changes ~~
- `FilesWrite` is actually supposed to be `FileWrites`, see canonical
source of how `Clean` works and what `FileWrites` is here:
https://github.com/Microsoft/msbuild/issues/2408#issuecomment-321082997
- Moved `DummyBuildEngine` into `MSBuild` directory--makes sense?
maybe don't need to?
- Added a `XamlGDifferentInputOutputLengths` test to check the error
message
- Expanded `DummyBuildEngine` so you can assert against log messages
- Changed a setting in `.Xamarin.Forms.Android.sln` so the unit test
project is built
- My VS IDE monkeyed with a few files, and I kept any *good* (or
relevant) changes: `Xamarin.Forms.UnitTests.csproj`,
`Xamarin.Forms.Xaml.UnitTests\app.config`, etc.
There were some checks for `%(TargetPath)` being blank in the C# code
of `XamlGTask`. In that case it was using `Path.GetRandomFileName`,
but we can't do this if we are setting up inputs and outputs for
`XamlG`. I presume this is from the designer and/or design-time builds
before `DependsOnTargets="PrepareResourceNames"` was added. I tested
design-time builds in VS on Windows, and `$(TargetPath)` was set. To
be sure we don't break anything here, I exclude inputs to `XamlG` if
`%(TargetPath)` is somehow blank.
See relevant MSBuild code for `%(TargetPath)` here:
0515178090/src/Tasks/Microsoft.Common.CurrentVersion.targets (L2822)
~~ Future changes ~~
CssG needs the exact same setup, as it was patterned after `XamlG`.
This should probably be done in a future PR.
* [msbuild] improved lookup of Xamarin.Forms.targets in integration tests
Context: https://devdiv.visualstudio.com/DevDiv/_build?buildId=1717939
Context: https://devdiv.visualstudio.com/DevDiv/_build?buildId=1718306
It looks like the VSTS builds for release branches are running tests
in a staging directory. This means we can't reliably import
`Xamarin.Forms.targets` as what was working locally in the
Xamarin.Forms source tree.
So to fix this:
- Look for `.nuspec/Xamarin.Forms.targets`, at the default location
and then using the `BUILD_SOURCESDIRECTORY` environment variable as
a fallback
- Copy all `*.targets` files to the test directory
- Our `*.csproj` files under test can import the file from there.
We have to copy the targets files here to be sure that MSBuild can
load `Xamarin.Forms.Build.Tasks.dll`, which is also referenced by the
unit tests.
I also made the tests abort earlier if they can't find
`Xamarin.Forms.targets`.