VS2015 MSBuild 项目上下文中的 Powershell 变量范围和临时文件

Powershell variable scope and temporary files within the context of a VS2015 MSBuild project

问题

两个独立的 Powershell 脚本,运行 来自两个独立的项目(Chrome 和 Firefox),显然写入同一个临时文件。

这种情况并不经常发生,显然每隔几个月左右发生一次。但是,当它确实发生时,这两个独立的项目每个最终都会将它们的 manifest.jsonpackage.json 文件混合在一起,这样每个文件都会有来自另一个文件的随机行散布在其中。

知道为什么会这样吗?

详情

我有一个包含 25 个项目的 VS2015 解决方案。其中一个项目称为 "Deploy",并已声明对多个其他项目的依赖性。对这个问题有重要意义的两个依赖项是 "ChromeExtension" 和 "FirefoxAddOn" 项目。

这些项目中的每一个都包含一个构建目标,用于在适当的清单文件中设置版本信息。这是 Chrome 目标:

<Target Name="SetVersion">
  <PropertyGroup>
    <PowerShellExe>powershell.exe</PowerShellExe>
    <PreBuildScript>$(SolutionDir)powershell\ChromePreBuild.ps1</PreBuildScript>
  </PropertyGroup>
  <Exec Command="$(PowerShellExe) -NonInteractive -ExecutionPolicy Unrestricted -command &quot;&amp; { &amp;'$(PreBuildScript)' -solutionDir '$(SolutionDir)\' }&quot;" />
</Target>

Firefox 目标是相同的,除了它执行脚本 FirefoxPreBuild.ps1

脚本比较简单。 Chrome 脚本:

param($solutionDir)

. "${solutionDir}powershell\BuildConfig.ps1"
. "${solutionDir}powershell\ReplaceVersion.ps1"
. "${solutionDir}powershell\ReplaceName.ps1"
. "${solutionDir}powershell\ChromeOptions.ps1"

Write-Host "Performing pre-build actions for Chrome add-on $chromeExtensionVersion"

ReplaceVersion "${solutionDir}ChromeExtension\manifest.json" $chromeExtensionVersion
ReplaceName "${solutionDir}ChromeExtension\manifest.json" "$chromeExtensionName"

以及 Firefox 脚本:

param($solutionDir)

. "${solutionDir}powershell\BuildConfig.ps1"
. "${solutionDir}powershell\ReplaceVersion.ps1"

Write-Host "Performing pre-build actions for Firefox add-on $firefoxExtensionVersion"

ReplaceVersion "${solutionDir}FirefoxAddOn\package.json" $firefoxExtensionVersion

从两个脚本调用的 ReplaceVersion 函数非常丑陋,但可以完成工作:

function ReplaceVersion {
    $file = $args[0]
    $replacementVersion = $args[1]
    Write-Host "- Updating version number in $file to ""$replacementVersion"""
    $tmp = [System.IO.Path]::GetTempFileName();
    if(Test-Path $tmp -PathType Leaf) {
        Remove-Item $tmp
    }
    Get-Content $file | Foreach-Object -process {
        $line = $_
        if ( ($line -match 'AssemblyVersion.*(\d+\.\d+\.\d+\.\d+)') -or
             ($line -match 'AssemblyFileVersion.*(\d+\.\d+\.\d+\.\d+)') -or
             ($line -match 'AppFolderName.*(\d+\.\d+\.\d+\.\d+)') -or
             ($line -match 'SupportedSyncFeatureSetRevision\s*=\s*(\d+);') -or
             ($line -match 'this.AddinName = ".* v(\d+\.\d+\.\d+\.\d+)";') -or
             ($line -match 'this.Text = ".* v(\d+\.\d+\.\d+\.\d+)";') -or
             ($line -match '"version":.*"(\d+\.\d+\.\d+\.?\d*)"') ) {
             $version = $matches[1]
             $line = $line -replace "$version", "$replacementVersion"
        }
        $line | Add-Content $tmp
    }
    #replace the old file with the new one
    Remove-Item -force $file
    Move-Item $tmp $file -Force -Confirm:$false
}

并且 ReplaceName 函数(Chrome-only)是,缺乏想象力和非 DRY,完全相同的东西,除了匹配不同的模式:

function ReplaceName {
    $file = $args[0]
    $replacementName = $args[1]
    Write-Host "- Updating extension name in $file to ""$replacementName"""
    $tmp = [System.IO.Path]::GetTempFileName();
    if(Test-Path $tmp -PathType Leaf) {
        Remove-Item $tmp
    }
    Get-Content $file | Foreach-Object -process {
        $line = $_
        if ( ($line -match '"name":.*"(.*)"') ) {
             $name = [regex]::escape($matches[1])
             $line = $line -replace "$name", "$replacementName"
        }
        $line | Add-Content $tmp
    }
    #replace the old file with the new one
    Remove-Item -force $file
    Move-Item $tmp $file -Force -Confirm:$false
}

您应该从脚本中删除该部分:

if(Test-Path $tmp -PathType Leaf) {
    Remove-Item $tmp
}

documentation

所述

Creates a uniquely named, zero-byte temporary file on disk and returns the full path of that file.

[System.IO.Path]::GetTempFileName() 将为您创建空文件,因此您不必删除并 re-create 它。

如果您查看 source of [System.IO.Path]::GetTempFileName(), then you can see that it implemented by calling to GetTempFileName WinAPI 函数,其中 uUnique 等于零。这是文档的相关部分:

If uUnique is zero, the function attempts to form a unique file name using the current system time. If the file already exists, the number is increased by one and the functions tests if this file already exists. This continues until a unique filename is found; the function creates a file by that name and closes it.

如您所见,GetTempFileName使用文件存在作为信号来创建不同的文件名。通过删除文件,您打开了后续调用可能 return 临时文件的相同文件名的可能性。