如何在MSBuild中复制生成的文件到发布目录中
如何使用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
目标调用之前使用Content
和None
等项。
解决问题
总结上述的解决问题方法,我在上面的构建流程中添加了如下的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
生成文件的话,却会遇到构建错误:
这是因为.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>
2021 - 2025 © Ricardo Ren ,由 .NET 9.0.2 驱动。
Build Commit # a662ecc14b