James Ko
James Ko

Reputation: 34499

How do I run an MSBuild task before compilation starts, but after intermediary files are generated?

Background: StyleCop is complaining that an auto-generated file has poor formatting, leading to many warnings when I try to build my project. The auto-generated file is in the obj/ directory of my project, and I want to create an MSBuild task that prepends // <auto-generated/> to this file before compilation (but after it is generated) so that StyleCop doesn't complain.

Problem: I have the following MSBuild code

<!-- StyleCop complains about a file that's auto-generated by the designer,
    so we need to prepend 'auto-generated' to it beforehand. -->
<Target Name="BeforeCompile" DependsOnTargets="MarkGeneratedFiles" />

<Target Name="MarkGeneratedFiles">
  <PropertyGroup>
    <GeneratedFilePath>$(MSBuildThisFileDirectory)obj\$(Configuration)\$(TargetFramework)\$(MSBuildProjectName).Program.cs</GeneratedFilePath>
  </PropertyGroup>

  <InsertIntoFile FilePath="$(GeneratedFilePath)" LineNumber="1" Text="// &lt;auto-generated/&gt;" />
</Target>

<!-- Code taken from http://stackoverflow.com/a/21500030/4077294 -->
<UsingTask
  TaskName="InsertIntoFile"
  TaskFactory="CodeTaskFactory"
  AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
  <ParameterGroup>
    <FilePath ParameterType="System.String" Required="true" />
    <LineNumber ParameterType="System.Int32" Required="true" />
    <Text ParameterType="System.String" Required="true" />
  </ParameterGroup>
  <Task>
    <Using Namespace="System" />
    <Using Namespace="System.IO" />
    <Code Type="Fragment" Language="cs">
      <![CDATA[
        // By tradition, text file line numbering is 1-based
        var lines = File.Exists(FilePath) 
                              ? File.ReadAllLines(FilePath).ToList() 
                              : new List<String>(1);
        lines.Insert(Math.Min(LineNumber - 1, lines.Count), Text);
        File.WriteAllLines(FilePath, lines);
        return true;
      ]]>
    </Code>
  </Task>
</UsingTask>

The file that I want to modify has the filename obj/Debug/netcoreapp1.0/BasicCompiler.Tests.Program.cs. In the above snippet, I have a BeforeCompile target that depends on MarkGeneratedFiles, which goes ahead and tries to insert // <auto-generated/> before the first line of that file.

I have tested, and this seems to work fine if the generated file is already present. However, if I remove the obj/ directory or I build from another machine, I get this error:

"C:\cygwin64\home\james\Code\cs\BasicCompiler\src\BasicCompiler.Tests\BasicCompiler.Tests.csproj" (default target) (1) ->
(MarkGeneratedFiles target) ->
  C:\cygwin64\home\james\Code\cs\BasicCompiler\src\BasicCompiler.Tests\BasicCompiler.Tests.csproj(68,5): error MSB4018: The "InsertIntoFile" task failed unexpectedly.\r
C:\cygwin64\home\james\Code\cs\BasicCompiler\src\BasicCompiler.Tests\BasicCompiler.Tests.csproj(68,5): error MSB4018: System.IO.DirectoryNotFoundException: Could not find
a part of the path 'C:\cygwin64\home\james\Code\cs\BasicCompiler\src\BasicCompiler.Tests\obj\Debug\netcoreapp1.0\BasicCompiler.Tests.Program.cs'.\r

Basically it seems like the target is getting run before the file is getting generated, so there's nothing to prepend the text to. Is there a way to run it after this file gets generated, but before compilation?

Additional notes: So far, I have looked through all of the special target names here and tried using both BeforeBuild and BeforeCompile.

Also, since I am using the "new" StyleCop, I cannot put <ExcludeFromStyleCop> in my project file. See https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/1145

Upvotes: 2

Views: 1627

Answers (1)

James Ko
James Ko

Reputation: 34499

I managed to work around this; see @stijn's super-helpful comment here.

  • First, I ran msbuild /v:detailed from the command line. This increases the verbosity of MSBuild so that it gives you a more detailed overview of what's going on, e.g. you can see the name of each target that's being run.

  • I searched through the log for the name of the target that was generating the file. In my case, it turned out to be GenerateProgramFiles.

  • I marked my custom target with AfterTargets="GenerateProgramFiles" so it ran after the file was generated.

Upvotes: 2

Related Questions