Files
YaeBlog/YaeBlog/source/posts/msbuild-generate-files.md
jackfiled 4df3b98e6d
All checks were successful
Build blog docker image / Build-Blog-Image (push) Successful in 1m41s
blog: msbuild-generate-files
blog: fix title in hpc-2025-cpu-architecture
2025-03-20 22:35:19 +08:00

5.8 KiB
Raw Blame History

title, date, tags
title date tags
如何在MSBuild中复制生成的文件到发布目录中 2025-03-20T22:33:21.6955385+08:00
技术笔记
dotnet

如何使用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>/文件夹下面。