Cannot reference dependency assemblies in T4 template when using TransformOnBuild - c#

We're trying to use T4 with Visual Studio 2010 (SP1) to build scripts for another language that are based upon some of our existing C# classes. I'm hoping for the following:
The template needs to load our existing assembly and use objects from a namespace in that assembly.
The transformation needs to run on every build on every development machine and build server without any additional installations.
(1) and (2) need to work together.
(1) was fairly straightforward:
<## assembly name="$(TargetDir)RequiredProject.dll" #>
<## import namespace="RequiredProject.RequiredNamespace" #>
Using the $(TargetDir) macro allowed me to reference the dll with a fully qualified UNC path (per the instructions found here).
(2) is a bit roundabout, but I think I've got it solved: I installed the required text transformation SDKs on a different machine and copied the required .targets and .dlls into a folder in my solution and then updated my .csproj file to reference the local .targets file.
(3) is where I run into problems. It seems like the <TransformOnBuild>true</TransformOnBuild> property doesn't play nicely when a referenced assembly needs to be built prior to the transformation. Everytime I enable transform on build with referenced assemblies, I get the following error:
Compiling transformation: Metadata file '$(TargetDir)RequiredProject.dll' could not be found.
However, I'm using the same assembly instruction that I was using in (1) to reference the assembly. In fact, going to the .tt template directly and saving it still produces the expected output -- it just doesn't work during the "build" step. Am I doing something wrong, or is there a way to ensure that the template transformations occur after the assemblies they depend on are built? (Or, more simply, that template transformations occur last?)

Unfortunately, the msbuild T4 host doesn't yet support embedded macro or msbuild variables in assembly names.
However, it does support Windows environment variables "%foo%", so although it means some machine-level setup, you can get something that works across in-IDE and build time transforms.

My understanding is that Visual Studio 2013 will finally solve this problem, but that doesn't do me much good as I'm still on Visual Studio 2012. After a lot of effort I finally ran across a solution.
In the project that has the template you wish to run, add the following as a pre-build step on the Build Events tab of the project properties page.
set textTransformPath="%CommonProgramFiles(x86)%\Microsoft Shared\TextTemplating\$(VisualStudioVersion)\TextTransform.exe"
if %textTransformPath%=="\Microsoft Shared\TextTemplating\$(VisualStudioVersion)\TextTransform.exe" set textTransformPath="%CommonProgramFiles%\Microsoft Shared\TextTemplating\$(VisualStudioVersion)\TextTransform.exe"
set ProjectDir=$(ProjectDir)
%textTransformPath% "%ProjectDir%StringGenerator.tt"
The first two lines take care of the differences between locating TextTransform.exe on 32-bit and 64-bit systems. The third line is the key. I need the path to the project location inside my template, so I set a local environment variable equal to the value of the build's $(ProjectDir) property. Inside my template, just use the following:
var projectDir = Environment.GetEnvironmentVariable("ProjectDir");
This has solved my issue.

I created a seperate solution that contained my needed referenced assemblies. The I had my buildscript build the reference solution first, then transform the templates, then build the solution containing the generated code.

If you want to reference dependency assemblies within a T4 script using macros and have text templating succeed during build-time, then you have to use project properties.
Within your project:
<Import Project="$(ProgramFiles)\Microsoft Visual Studio\2017\Enterprise\MSBuild\Microsoft\VisualStudio\v15.0\TextTemplating\Microsoft.TextTemplating.targets" />
<PropertyGroup>
<T4ProjectDir>$(ProjectDir)</T4ProjectDir>
</PropertyGroup>
<ItemGroup>
<T4ParameterValues Include="T4ProjectDir">
<Value>$(T4ProjectDir)</Value>
<Visible>false</Visible>
</T4ParameterValues>
</ItemGroup>
Where the path to your text templating environment may be different.
Then use $(T4ProjectDir) as you would use any other macro in your text template.
Or you could also simply refer to existing properties:
<ItemGroup>
<T4ParameterValues Include="ProjectDir">
<Value>$(ProjectDir)</Value>
<Visible>false</Visible>
</T4ParameterValues>
</ItemGroup>

Related

How to invoke a target of a msbuild project without recursively calling the same target on sub projects

I created bunch of targets in msbuild project. Lets call this project TopLevelProject. Lets say this has a target called CollectNZip. TopLevelProject depends on SubProjectA, SubProjectB and SubProjectC.
I have a solutions targets file Directory.Solution.targets that contains all the projects below its folder including TopLevelProject.
As part of a target in this file say BuildAll, I like to invoke CollectNZip target of TopLevelProject. So I added TopLevelProject:CollectNZip as dependency.
When I invoke BuildAll, I do see TopLevelProject is invoked with target CollectNZip. But this sucker as part of dependency started invoking SubProjectA:CollectNZip, SubProjectB:CollectNZip etc. As those sub projects don't have CollectNZip, the buildall target is failing.
What is the trick to invoke a target of a project, but don't invoke the sub projects as part of the invocation?
If I understand the scenario:
There is a solution file with a set of projects. Let's say the solution is named 'MySolution.sln'.
I assume the solution file was created by either Visual Studio or the dotnet tool.
The set of projects in the solution include: 'TopLevelProject.csproj', 'SubProjectA.csproj', 'SubProjectB.csproj', and 'SubProjectC.csproj'.
I assume the project files were created as C# projects by either Visual Studio or the dotnet tool.
The project 'TopLevelProject' has ProjectReferences to 'SubProjectA', 'SubProjectB', and 'SubProjectC'.
The project 'TopLevelProject' also has a target named 'CollectNZip'.
There is a 'Directory.Solution.targets' file that is a peer of 'MySolution.sln' or in a parent directory.
'Directory.Solution.targets' contains a 'BuildAll` target.
The 'Directory.Solution.targets' file is ignored by Visual Studio so the 'BuildAll' target is only available when running from the command line.
Projects can be added to a solution file (SLN) but can't be added to an MSBuild file. The 'Directory.Solution.targets' file is an MSBuild file. It can't be a container for projects. I don't know what the following statement means:
I have a solutions targets file Directory.Solution.targets that contains all the projects below its folder including TopLevelProject.
Note that the Import element is a textual include. It doesn't "add" a project; it adds the content of the file in the Project attribute into the content of the current project.
From the command line, you can invoke the 'CollectNZip' target of project 'TopLevelProject' via the solution file.
e.g.
msbuild MySolution.sln /t:TopLevelProject:CollectNZip
This will invoke only the 'CollectNZip' target on only the TopLevelProject project. It will not run other projects from the solution.
I don't know what the following statement means:
As part of a target in this file say BuildAll, I like to invoke CollectNZip target of TopLevelProject. So I added TopLevelProject:CollectNZip as dependency.
The <ProjectName>:<TargetName> syntax is supported for the command line /target switch. It is not supported within the code of an MSBuild file. TopLevelProject:CollectNZip can't be a dependency of a target.
MSBuild doesn't have any notion of "sub projects" although there are two mechanisms which can add dependencies between projects.
A project dependency can be added to the solution file. The solution level project dependency effects the build order -- and does nothing else. It does not share files.
A ProjectReference can be added to a project file. The ProjectReference is an ItemGroup and is part of the C# project build system that is built on the general MSBuild build engine. ProjectReference is specific to certain targets of the C# build system, most importantly the build and clean targets. build and clean will evaluate the ProjectReference ItemGroup, will run the referenced projects, and on a build will copy in the product of the referenced project.
If I add a target named 'Fred' to all the projects and I invoke 'Fred' on one project via the solution, 'Fred' will not be called on projects in the ProjectReference ItemGroup.
A project is an encapsulation. It doesn't know its 'caller' and, excepting ProjectReference, it doesn't know about other projects.
The described behavior is not how MSBuild works and I'm guessing that the description is imprecise and/or there is pertinent code not shown.
If 'CollectNZip' should only run within the 'TopLevelProject' project, then only add the target to that project. If you want to be able to build with and without 'CollectNZip', define a property that can be used as a flag, e.g. add an 'EnableCollectNZip' property and add a Condition on the target that tests the value of the 'EnableCollectNZip' property.

Include NuGet Utility Package when building csproj

I need to use a NuGet package containing a utility for my project. It contains several binaries (EXEs and DLLs).
I've added it to my project successfully but I suspect the nupkg isn't formed correctly because I cannot use any of its DLLs or EXEs in my project without manually pointing to the package in my local NuGet cache. When compiling, none of its resources are added to the output (I assume this is because nothing is referenced in my code).
I'd like to create a wrapper project to call the binaries but I'd also like other project devs to be able to compile the solution without adjusting directory variables. Ideally, I could configure the csproj to pull in the bits directly from the local package cache. I think this would be possible by setting the Generate Path Property value to Yes in Visual Studio, but the variable cannot be found when I attempt to use an <Include/> statement in the csproj file.
Is what I'm asking possible? Namely, reference the NuGet package bits within my csproj to ensure the binaries are dropped in the compilation output? Can I do this with the Path Property, or is there something else I can do without directly committing the package's binaries into my project?
(I realize I need to work with the developer to fix whatever issue they have with their package, but I have no direct influence at the moment so this is the best I can do at the moment).
I figured this out, mostly due to misunderstanding how some of the different tags and attributes are meant to be used.
To achieve the desired effect, I did the following:
<ItemGroup>
<Content Include="$(Pkg{PackageId})\**">
<Link>{NameOfSolutionDirectory}\%(RecursiveDir)%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
Where {PackageId} is the name of the NuGet package (this step requires setting 'Generate Path Property' to 'Yes' in the package properties via Solution Explorer), and {NameOfSolutionDirectory} is the name of a folder within the solution I'd like to use for containing those bits, if you're as concerned about keeping the project as organized as I am. The {} should be excluded when replacing these values.
If you want to scope to a specific directory within the package contents, do it within the Include attribute. The ** is necessary if you want to include all files within that directory, or else you can scope by extension or whatever additional pattern you'd like.

Project references break after adding a nuget import project (".targets") file

We have a project that requires a pre-build task - signing a DLL without source code. For this, we're using Brutal Dev StrongNameSigner, which works well.
Recently, we've added this package from nuget - this adds an entry in the csproj such as the following:
<Import Project="..\packages\Brutal.Dev.StrongNameSigner.2.1.3\build\Brutal.Dev.StrongNameSigner.targets" Condition="Exists('..\packages\Brutal.Dev.StrongNameSigner.2.1.3\build\Brutal.Dev.StrongNameSigner.targets')" />
That line picks up information such as environment variables needed for compilation. Unfortunately, that line also breaks all our project references, such as below:
I've checked their properties; it's all empty.
Removing the Import line will cause all the DLLs to show up correctly, but fail to build due to missing environment variables.
Oddly enough, it seems that the project still builds!
Thanks

Suppress warning on solution level. Treat warning as error on solution level

I am trying to set global rules for my team. We are using VS2012 with TFS for our C# projects. I'd like to suppress some of the warnings and also treat some of the warnings as errors. I found the way to do it on the project level - project properties -> build tab.
But we have solution with more than hundred projects and I am looking for some easier way to set those rules globally.
A solution is just a (pretty dumb) container for projects. If you open it in a text editor you'll quickly see you can't extend it, only add projects/items.
What you want is one or more common msbuild files specifying all needed options for compiler/linker/whatever tools you use, and Import it in every single project. We've been using this for years and it's very convenient (though part of the convenience is probably we also wrote a small tool to generate project files to automatically import the global properties so we don't have to mess with them manually)
Alternatively you could add a machine wide file, look in $(MSBuildToolsPath)\Microsoft.CSharp.targets to see where to place those files. I'm not going to copy/paste the content here, but the very first lines basically check if there are user definded files in eg $(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\$(MSBuildThisFile)\ImportBefore and if so they're all imported before all common msbuild stuff. Likewise the end of the Microsoft.CSharp.targets contains similar logic to import files after all common msbuild stuff.
As of MSBuild 15 (circa 2017) you can use Directory.Build.Props file in the top folder of your solution. The syntax is the same as csproj, fsproj, or vbproj file and the entries are treated as though they are injected into all project files. You will need to restart Visual Studio to apply the changes. (thanks Jumbo!)
<Project>
<PropertyGroup>
<WarningsAsErrors>CS4014, CS1998</WarningsAsErrors>
</PropertyGroup>
</Project>

Rename .NET 2.0 Executable

Does anyone know of any gotchas when changing a C# .NET 2.0 executable file name on a post build event, given that the executable is strong named and has an embedded manifest? Additionally, the executable will be signed by a third party before being packaged in a installer.
I know any associated .config files also need to be renamed to reflect the new executable name.
Am I also right in guessing that the best solution is to change the assembly name in the project properties, rather than renaming the executable file name? The problem is Visual Studio doesn't play nice with conditional assembly names. (i.e. adding a condition attribute to the tag in the .csproj)
VS loads project once and then stores it in memory. If you want to build two assemblies from VS you can add AfterBuild target and call MSBuild to build your assembly again but with different parameters:
<ProperttyGroup Condition="'$(BuildAgain)'==''">
<!-- Default parameters to VS -->
<AssemblyName>Name1,Default</AssemblyName>
<ProperttyGroup>
<ProperttyGroup Condition="'$(BuildAgain)'=='true'">
<!-- Overrided parameters -->
<AssemblyName>Name2.Custom</AssemblyName>
<ProperttyGroup>
<Target Name="AfterBuild"
Condition="'$(BuildAgain)'==''">
<MSBuild Projects="$(MSBuildProjectFullPath)"
Properties="BuildAgain=true;Configuration=$(Configuration);Platform=$(Platform)"
Targets="Rebuild"
</Target>
There is no real benefit to strong naming an exe. The benefit to strong naming a dll, is that someone cannot replace it with their own version of a malicious one (and you can put it in the GAC). Unless you are referencing your exe in another project as if it were a dll (which would be strange), you don't need to strong name it.

Categories