如何在MSBuild中复制生成的文件到发布目录中

总字数:4216字,预计阅读时间 07分 01秒。

如何使用MSBuild将构建过程中生成文件复制到生成目录中?

遇到的问题

最近在尝试在blazor项目中使用tailwindcss作为css工具类的提供工具,而不是使用老旧的bootstrap框架,不过使用tailwindcss需要在项目构建时使用tailwindcss工具扫描文件中使用到的css属性并生成最终的css文件,这就带来了在构建时运行tailwindcss生成并复制到输出目录的需求。

由于我是使用pnpm作为前端管理工具,我在项目的csproj文件中添加了下面的Target来生成文件:

<Target Name="EnsurePnpmInstalled" BeforeTargets="BeforeBuild">
    <Message Importance="low" Text="Ensure pnpm is installed..."/>
    <Exec Command="pnpm --version" ContinueOnError="true">
      <Output TaskParameter="ExitCode" PropertyName="ErrorCode"/>
    </Exec>

    <Error Condition="$(ErrorCode) != 0" Text="Pnpm is not installed which is required for build."/>

    <Message Importance="normal" Text="Installing pakages using pnpm..."/>
    <Exec Command="pnpm install"/>
  </Target>

  <Target Name="TailwindGenerate" AfterTargets="EnsurePnpmInstalled" BeforeTargets="BeforeBuild">
    <Message Importance="normal" Text="Generate css files using tailwind..."/>
     <Exec Command="pnpm tailwindcss -i wwwroot/tailwind.css -o wwwroot/tailwind.g.css"/>
  </Target>

这套生成逻辑在本地工作良好,但是却在CI上运行时出现了问题:CI上打包的Docker镜像中没有tailwind.g.css文件,导致最终部署的站点丢失了所有的格式。

产生问题的原因

经过反复实验,我发现只有在构建之前wwwroot目录中已经存在tailwind.g.css文件的情况下,MSBuild才会将生成的文件复制到最终的输出目录中。但是在CI环境下,因为使用.gitignore没有将*.g.css文件添加到代码管理,因此CI运行构建之前没有该文件,因此构建的结果中也没有该文件。

仔细研究MSBuild的文档和网络上的分享,我意识到这是由于MSBuild的构建流程导致的,MSBuild`的构建流程分成两个大的阶段:

  • 评估阶段(Evaluation Phase)

    在这个阶段,MSBuild将会运行读取所有的配置文件,创建需要的属性,展开所有的glob,建立好整个构建流程。

  • 执行阶段(Execution Phase)

    在这个阶段,MSBuild将按照上一阶段执行的属性执行实际的构建指令。

这两个阶段的划分就导致在生成阶段才生成的文件不会被包含在复制文件的指令中,因此他们不会被拷贝到最终的输出目录中。

这和cmake的构建过程很像,首先调用cmake生成一些构建指令,在调用实际的构建指令构建二进制文件。

因此这类问题的推荐解决办法是手动将这些文件添加到构建流程中,即在BeforeBuild目标调用之前使用ContentNone等项。

解决问题

总结上述的解决问题方法,我在上面的构建流程中添加了如下的None项:

<Target Name="EnsurePnpmInstalled" BeforeTargets="BeforeBuild">
    <Message Importance="low" Text="Ensure pnpm is installed..."/>
    <Exec Command="pnpm --version" ContinueOnError="true">
      <Output TaskParameter="ExitCode" PropertyName="ErrorCode"/>
    </Exec>

    <Error Condition="$(ErrorCode) != 0" Text="Pnpm is not installed which is required for build."/>

    <Message Importance="normal" Text="Installing pakages using pnpm..."/>
    <Exec Command="pnpm install"/>
  </Target>

  <Target Name="TailwindGenerate" AfterTargets="EnsurePnpmInstalled" BeforeTargets="BeforeBuild">
    <Message Importance="normal" Text="Generate css files using tailwind..."/>
    <Exec Command="pnpm tailwindcss -i wwwroot/tailwind.css -o wwwroot/tailwind.g.css"/>

    <!-- Make sure generated file will be copied to output directory-->
    <ItemGroup>
      <Content Include="wwwroot/tailwind.g.css" Visible="false" CopyToOutputDirectory="PreserveNewest"/>
    </ItemGroup>
  </Target>

在运行构建之后,在最终的publish文件夹的wwroot文件夹中就可以找到tailwind.g.css文件。

不过我还想进行一点优化,MSBuild文档中建议将自动生成的文件放在IntermediateOutputPath,也就是obj文件加中,因此这里尝试将tailwind.g.css文件生成到IntermediateOuputPath中,优化之后的Target项长这个样子:

  <Target Name="EnsurePnpmInstalled" BeforeTargets="BeforeBuild">
    <Message Importance="low" Text="Ensure pnpm is installed..."/>
    <Exec Command="pnpm --version" ContinueOnError="true">
      <Output TaskParameter="ExitCode" PropertyName="ErrorCode"/>
    </Exec>

    <Error Condition="$(ErrorCode) != 0" Text="Pnpm is not installed which is required for build."/>

    <Message Importance="normal" Text="Installing pakages using pnpm..."/>
    <Exec Command="pnpm install"/>
  </Target>

  <Target Name="TailwindGenerate" AfterTargets="EnsurePnpmInstalled" BeforeTargets="BeforeBuild">
    <Message Importance="normal" Text="Generate css files using tailwind..."/>
    <Exec Command="pnpm tailwindcss -i wwwroot/tailwind.css -o $(IntermediateOutputPath)tailwind.g.css"/>

    <!-- Make sure generated file will be copied to output directory-->
    <ItemGroup>
      <Content Include="$(IntermediateOutputPath)tailwind.g.css" Visible="false" TargetPath="wwwroot/tailwind.g.css"/>
    </ItemGroup>
  </Target>

经过测试,这套生成逻辑在blazor类库环境下也可以正常运行,类库的文件会被正确地生成到wwwroot/_content/<ProjectName>/文件夹下面。

新的问题

在上述代码合并之后,我在后续开发过程中却遇到的了新的问题:在开发环境下项目运行的目录是源代码目录,而此时的wwwroot目录下面没有tailwind.g.css文件,此时网站再次丢失了样式,而如果使用pnpm tailwindcss -i wwroot/tailwind.css -o wwwroot/tailwind.g.css生成文件的话,却会遇到构建错误:

image-20250325150841442

这是因为.NET SDK也会尝试将已经存在的wwwroot/tailwind.g.css复制到输出文件中,这就会造成冲突。

因此为了让开发环境和测试环境可以共存,我让TailwindGenerate目标只在dotnet publish运行,而在开发环境中使用pnpm tailwindcss手动生成CSS文件。

  <Target Name="EnsurePnpmInstalled" BeforeTargets="BeforeBuild">
    <Message Importance="low" Text="Ensure pnpm is installed..."/>
    <Exec Command="pnpm --version" ContinueOnError="true">
      <Output TaskParameter="ExitCode" PropertyName="ErrorCode"/>
    </Exec>
    <Error Condition="$(ErrorCode) != 0" Text="Pnpm is not installed which is required for build."/>
    <Message Importance="normal" Text="Installing pakages using pnpm..."/>
    <Exec Command="pnpm install"/>
  </Target>
  <Target Name="TailwindGenerate" AfterTargets="EnsurePnpmInstalled" BeforeTargets="BeforeBuild" Condition="'$(_IsPublishing)' == 'yes'">
    <Message Importance="normal" Text="Generate css files using tailwind..."/>
    <Exec Command="pnpm tailwindcss -i wwwroot/tailwind.css -o $(IntermediateOutputPath)tailwind.g.css"/>
    <ItemGroup>
      <Content Include="$(IntermediateOutputPath)tailwind.g.css" Visible="false" TargetPath="wwwroot/tailwind.g.css"/>
    </ItemGroup>
  </Target>
文章作者:Ricardo Ren
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,诸位读者如有兴趣可任意转载,不必征询许可,但请注明“转载自 Ricardo's Blog ”。

2021 - 2025 © Ricardo Ren ,由 .NET 9.0.2 驱动。

Build Commit # a662ecc14b

蜀ICP备2022004429号-1