diff --git a/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj b/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj index d27a5fb43d7..5ff7b83c675 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj +++ b/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj @@ -67,7 +67,7 @@ - + diff --git a/test/common/package/package.tests.ps1 b/test/common/package/package.tests.ps1 new file mode 100644 index 00000000000..c7663ec1fe2 --- /dev/null +++ b/test/common/package/package.tests.ps1 @@ -0,0 +1,68 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +$moduleRootFilePath = Split-Path -Path $PSScriptRoot -Parent + +# Identify the repository root path of the resource module +$repoRootPath = (Resolve-Path -LiteralPath (Join-path $moduleRootFilePath "../..")).ProviderPath + +Import-Module "$repoRootPath/tools/releaseTools.psm1" + +Describe 'Common Tests - Package Reference' -Tag 'CI' { + BeforeAll { + $testCases = @() + Get-NewOfficalPackage -IncludeAll | ForEach-Object { + $testCases += @{ + CsProj = $_.CsProj + PackageName = $_.PackageName + CsProjVersion = $_.CsProjVersion + NuGetRevision = $_.NuGetRevision + NuGetVersion = $_.NuGetVersion + } + } + } + + # This test should always be enabled + It " reference to should not need to be updated by a revision" -TestCases $testCases { + param( + [string] + $CsProj, + + [string] + $PackageName, + + [string] + $CsProjVersion, + + [string] + $NuGetRevision, + + [string] + $NuGetVersion + ) + + $NuGetRevision | Should -BeExactly $CsProjVersion + } + + # This test should be enabled when we are developing + It " reference to should not need to be updated by a new version" -TestCases $testCases { + param( + [string] + $CsProj, + + [string] + $PackageName, + + [string] + $CsProjVersion, + + [string] + $NuGetRevision, + + [string] + $NuGetVersion + ) + + $NuGetVersion | Should -BeExactly $CsProjVersion + } +} diff --git a/tools/Xml/Xml.psm1 b/tools/Xml/Xml.psm1 new file mode 100644 index 00000000000..1184dfc7a6a --- /dev/null +++ b/tools/Xml/Xml.psm1 @@ -0,0 +1,101 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# Adds an attribute to a XmlElement +function New-XmlAttribute +{ + param( + [Parameter(Mandatory)] + [string]$Name, + [Parameter(Mandatory)] + [object]$Value, + [Parameter(Mandatory)] + [System.Xml.XmlElement]$Element, + [Parameter(Mandatory)] + [System.Xml.XmlDocument]$XmlDoc + ) + + $attribute = $XmlDoc.CreateAttribute($Name) + $attribute.Value = $value + $null = $Element.Attributes.Append($attribute) +} + +# Adds an XmlElement to an XmlNode +# Returns the new Element +function New-XmlElement +{ + param( + [Parameter(Mandatory)] + [string]$LocalName, + [Parameter(Mandatory)] + [System.Xml.XmlDocument]$XmlDoc, + [Parameter(Mandatory)] + [System.Xml.XmlNode]$Node, + [Switch]$PassThru, + [string]$NamespaceUri + ) + + if($NamespaceUri) + { + $newElement = $XmlDoc.CreateElement($LocalName, $NamespaceUri) + } + else + { + $newElement = $XmlDoc.CreateElement($LocalName) + } + + $null = $Node.AppendChild($newElement) + if($PassThru.IsPresent) + { + return $newElement + } +} + +# Removes an XmlElement and it's parent if it is empty +function Remove-XmlElement +{ + param( + [Parameter(Mandatory)] + [System.Xml.XmlElement]$Element, + [Switch]$RemoveEmptyParents + ) + + $parentNode = $Element.ParentNode + $null = $parentNode.RemoveChild($Element) + if(!$parentNode.HasChildNodes -and $RemoveEmptyParent.IsPresent) + { + Remove-XmlElement -Element $parentNode -RemoveEmptyParents + } +} + +# Get a node by XPath +# Returns null if the node is not found +function Get-XmlNodeByXPath +{ + param( + [Parameter(Mandatory)] + [System.Xml.XmlDocument] + $XmlDoc, + [System.Xml.XmlNamespaceManager] + $XmlNsManager, + [Parameter(Mandatory)] + [string] + $XPath + ) + + if($XmlNsManager) + { + return $XmlDoc.SelectSingleNode($XPath,$XmlNsManager) + } + else + { + return $XmlDoc.SelectSingleNode($XPath) + } +} + +Export-ModuleMember -Function @( + 'Get-XmlNodeByXPath' + 'Remove-XmlElement' + 'New-XmlElement' + 'New-XmlAttribute' +) diff --git a/tools/appveyor.psm1 b/tools/appveyor.psm1 index 6ae6ef4b040..5a6f4649c83 100644 --- a/tools/appveyor.psm1 +++ b/tools/appveyor.psm1 @@ -626,6 +626,7 @@ function Invoke-AppveyorFinish } catch { Write-Host -Foreground Red $_ + Write-Host -Foreground Red $_.ScriptStackTrace throw $_ } } diff --git a/tools/packaging/packaging.psm1 b/tools/packaging/packaging.psm1 index 0f1adc723a3..63fd068aec1 100644 --- a/tools/packaging/packaging.psm1 +++ b/tools/packaging/packaging.psm1 @@ -3,6 +3,7 @@ $Environment = Get-EnvironmentInformation $packagingStrings = Import-PowerShellDataFile "$PSScriptRoot\packaging.strings.psd1" +Import-Module "$PSScriptRoot\..\Xml" -ErrorAction Stop -Force $DebianDistributions = @("ubuntu.14.04", "ubuntu.16.04", "ubuntu.17.10", "ubuntu.18.04", "debian.8", "debian.9") function Start-PSPackage { @@ -2554,15 +2555,19 @@ function Test-FileWxs $filesAssetString = (Get-Content -Raw -Path $FilesWxsPath).Replace('$(var.FileArchitecture)',$env:FileArchitecture) [xml] $filesAssetXml = $filesAssetString + [xml] $newFilesAssetXml = $filesAssetString + $xmlns=[System.Xml.XmlNamespaceManager]::new($newFilesAssetXml.NameTable) + $xmlns.AddNamespace('Wix','http://schemas.microsoft.com/wix/2006/wi') + [xml] $heatFilesXml = Get-Content -Raw -Path $HeatFilesWxsPath $assetFiles = $filesAssetXml.GetElementsByTagName('File') $heatFiles = $heatFilesXml.GetElementsByTagName('File') - $indexedHeatFiles = @() + $heatNodesByFile = @{} # Index the list of files generated by heat foreach($file in $heatFiles) { - $indexedHeatFiles += $file.Source + $heatNodesByFile.Add($file.Source, $file) } # Index the files from the asset wxs @@ -2572,42 +2577,218 @@ function Test-FileWxs foreach($file in $assetFiles) { $name = $file.Source - if($indexedHeatFiles -inotcontains $name) + if($heatNodesByFile.Keys -inotcontains $name) { $passed = $false Write-Warning "{$name} is no longer in product and should be removed from {$FilesWxsPath}" + $componentId = $file.ParentNode.Id + $componentXPath = '//Wix:Component[@Id="{0}"]' -f $componentId + $componentNode = Get-XmlNodeByXPath -XmlDoc $newFilesAssetXml -XmlNsManager $xmlns -XPath $componentXPath + if($componentNode) + { + # Remove the Component + Remove-XmlElement -Element $componentNode -RemoveEmptyParents + # Remove teh ComponentRef + Remove-ComponentRefNode -Id $componentId -XmlDoc $newFilesAssetXml -XmlNsManager $xmlns + } + else + { + Write-Warning "Could not remove this node!" + } } $indexedAssetFiles += $name } # verify that no files have been added. - foreach($file in $indexedHeatFiles) + foreach($file in $heatNodesByFile.Keys) { if($indexedAssetFiles -inotcontains $file) { $passed = $false - Write-Warning "new file {$file} need to be added to {$FilesWxsPath}" + $folder = Split-Path -Path $file + $name = Split-Path -Path $file -Leaf + $heatNode = $heatNodesByFile[$file] + $compGroupNode = Get-ComponentGroupNode -XmlDoc $newFilesAssetXml -XmlNsManager $xmlns + $filesNode = Get-DirectoryNode -Node $heatNode -XmlDoc $newFilesAssetXml -XmlNsManager $xmlns + # Create new Component + $newComponent = New-XmlElement -XmlDoc $newFilesAssetXml -LocalName 'Component' -Node $filesNode -PassThru -NamespaceUri 'http://schemas.microsoft.com/wix/2006/wi' + $componentId = New-WixId -Prefix 'cmp' + New-XmlAttribute -XmlDoc $newFilesAssetXml -Element $newComponent -Name 'Id' -Value $componentId + New-XmlAttribute -XmlDoc $newFilesAssetXml -Element $newComponent -Name 'Guid' -Value "{$(New-Guid)}" + # Crete new File in Component + $newFile = New-XmlElement -XmlDoc $newFilesAssetXml -LocalName 'File' -Node $newComponent -PassThru -NamespaceUri 'http://schemas.microsoft.com/wix/2006/wi' + New-XmlAttribute -XmlDoc $newFilesAssetXml -Element $newFile -Name 'Id' -Value (New-WixId -Prefix 'fil') + New-XmlAttribute -XmlDoc $newFilesAssetXml -Element $newFile -Name 'KeyPath' -Value "yes" + New-XmlAttribute -XmlDoc $newFilesAssetXml -Element $newFile -Name 'Source' -Value $file + # Create new ComponentRef + $newComponentRef = New-XmlElement -XmlDoc $newFilesAssetXml -LocalName 'ComponentRef' -Node $compGroupNode -PassThru -NamespaceUri 'http://schemas.microsoft.com/wix/2006/wi' + New-XmlAttribute -XmlDoc $newFilesAssetXml -Element $newComponentRef -Name 'Id' -Value $componentId + + Write-Warning "new file in {$folder} with name {$name} in a {$($filesNode.LocalName)} need to be added to {$FilesWxsPath}" } } if(!$passed) { + $newXmlFileName = Join-Path -Path $env:TEMP -ChildPath ([System.io.path]::GetRandomFileName() + '.xml') + $newFilesAssetXml.Save($newXmlFileName) + $newXml = Get-Content -raw $newXmlFileName + $newXml = $newXml -replace 'amd64', '$(var.FileArchitecture)' + $newXml = $newXml -replace 'x86', '$(var.FileArchitecture)' + $newXml | Out-File -FilePath $newXmlFileName -Encoding ascii + Write-Log -message "Update xml saved to $newXmlFileName" if($env:appveyor) { try { - Push-AppveyorArtifact $HeatFilesWxsPath + Push-AppveyorArtifact $newXmlFileName } catch { Write-Warning -Message "Pushing MSI File fragment failed." } } + elseif($env:TF_BUILD -and $env:BUILD_REASON -ne 'PullRequest') + { + Write-Host "##vso[artifact.upload containerfolder=wix;artifactname=wix]$newXmlFileName" + } throw "Current files to not match {$FilesWxsPath}" } } +# Removes a ComponentRef node in the files.wxs Xml Doc +function Remove-ComponentRefNode +{ + param( + [Parameter(Mandatory)] + [System.Xml.XmlDocument] + $XmlDoc, + [Parameter(Mandatory)] + [System.Xml.XmlNamespaceManager] + $XmlNsManager, + [Parameter(Mandatory)] + [string] + $Id + ) + + $compRefXPath = '//Wix:ComponentRef[@Id="{0}"]' -f $Id + $node = Get-XmlNodeByXPath -XmlDoc $XmlDoc -XmlNsManager $XmlNsManager -XPath $compRefXPath + if($node) + { + Remove-XmlElement -element $node + } + else + { + Write-Warning "could not remove node" + } +} + +# Get the ComponentGroup node in the files.wxs Xml Doc +function Get-ComponentGroupNode +{ + param( + [Parameter(Mandatory)] + [System.Xml.XmlDocument] + $XmlDoc, + [Parameter(Mandatory)] + [System.Xml.XmlNamespaceManager] + $XmlNsManager + ) + + if(!$XmlNsManager.HasNamespace('Wix')) + { + throw 'Namespace manager must have "wix" defined.' + } + + $compGroupXPath = '//Wix:ComponentGroup' + $node = Get-XmlNodeByXPath -XmlDoc $XmlDoc -XmlNsManager $XmlNsManager -XPath $compGroupXPath + return $node +} + +# Gets the Directory Node the files.wxs Xml Doc +# Creates it if it does not exist +function Get-DirectoryNode +{ + param( + [Parameter(Mandatory)] + [System.Xml.XmlElement] + $Node, + [Parameter(Mandatory)] + [System.Xml.XmlDocument] + $XmlDoc, + [Parameter(Mandatory)] + [System.Xml.XmlNamespaceManager] + $XmlNsManager + ) + + if(!$XmlNsManager.HasNamespace('Wix')) + { + throw 'Namespace manager must have "wix" defined.' + } + + $pathStack = [System.Collections.Stack]::new() + + [System.Xml.XmlElement] $dirNode = $Node.ParentNode.ParentNode + $dirNodeType = $dirNode.LocalName + if($dirNodeType -eq 'DirectoryRef') + { + return Get-XmlNodeByXPath -XmlDoc $XmlDoc -XmlNsManager $XmlNsManager -XPath "//Wix:DirectoryRef" + } + if($dirNodeType -eq 'Directory') + { + while($dirNode.LocalName -eq 'Directory') { + $pathStack.Push($dirNode.Name) + $dirNode = $dirNode.ParentNode + } + $path = "//" + [System.Xml.XmlElement] $lastNode = $null + while($pathStack.Count -gt 0){ + $dirName = $pathStack.Pop() + $path += 'Wix:Directory[@Name="{0}"]' -f $dirName + $node = Get-XmlNodeByXPath -XmlDoc $XmlDoc -XmlNsManager $XmlNsManager -XPath $path + + if(!$node) + { + if(!$lastNode) + { + # Inserting at the root + $lastNode = Get-XmlNodeByXPath -XmlDoc $XmlDoc -XmlNsManager $XmlNsManager -XPath "//Wix:DirectoryRef" + } + + $newDirectory = New-XmlElement -XmlDoc $XmlDoc -LocalName 'Directory' -Node $lastNode -PassThru -NamespaceUri 'http://schemas.microsoft.com/wix/2006/wi' + New-XmlAttribute -XmlDoc $XmlDoc -Element $newDirectory -Name 'Name' -Value $dirName + New-XmlAttribute -XmlDoc $XmlDoc -Element $newDirectory -Name 'Id' -Value (New-WixId -Prefix 'dir') + $lastNode = $newDirectory + } + else + { + $lastNode = $node + } + if ($pathStack.Count -gt 0) + { + $path += '/' + } + } + return $lastNode + } + + throw "unknown element type: $dirNodeType" +} + +# Creates a new Wix Id in the proper format +function New-WixId +{ + param( + [Parameter(Mandatory)] + [string] + $Prefix + ) + + $guidPortion = (New-Guid).Guid.ToUpperInvariant() -replace '\-' ,'' + "$Prefix$guidPortion" +} + # Builds coming out of this project can have version number as 'a.b.c' OR 'a.b.c-d-f' # This function converts the above version into major.minor[.build[.revision]] format function Get-PackageVersionAsMajorMinorBuildRevision diff --git a/tools/releaseTools.psm1 b/tools/releaseTools.psm1 index 95c74dcd606..30395ed6683 100644 --- a/tools/releaseTools.psm1 +++ b/tools/releaseTools.psm1 @@ -223,6 +223,9 @@ function Get-ChangeLog #.PARAMETER Path #The path to check for csproj files with packagse # +#.PARAMETER IncludeAll +#Include packages that don't need to be updated +# #.OUTPUTS #Objects which represet the csproj package ref, with the current and new version ############################## @@ -230,11 +233,13 @@ function Get-NewOfficalPackage { param( [String] - $Path = (Join-path -Path $PSScriptRoot -ChildPath '..') + $Path = (Join-path -Path $PSScriptRoot -ChildPath '..\src'), + [Switch] + $IncludeAll ) # Calculate the filter to find the CSProj files $filter = Join-Path -Path $Path -ChildPath '*.csproj' - $csproj = Get-ChildItem $filter -Recurse + $csproj = Get-ChildItem $filter -Recurse -Exclude 'PSGalleryModules.csproj' $csproj | ForEach-Object{ $file = $_ @@ -246,25 +251,58 @@ function Get-NewOfficalPackage $packages=$csprojXml.Project.ItemGroup.PackageReference # check to see if there is a newer package for each refernce - foreach($package in $packages) + foreach ($package in $packages) { # Get the name of the package $name = $package.Include - # don't pull 'Microsoft.Management.Infrastructure' from nuget - if($name -and $name -ne 'Microsoft.Management.Infrastructure') + if ($name) { # Get the current package from nuget - $newPackage = find-package -Name $name -Source https://nuget.org/api/v2/ -ErrorAction SilentlyContinue + $versions = find-package -Name $name -Source https://nuget.org/api/v2/ -ErrorAction SilentlyContinue -AllVersions | + Add-Member -Type ScriptProperty -Name Published -Value { $this.Metadata['published']} -PassThru | + Where-Object { Test-IncludePackageVersion -NewVersion $_.Version -Version $package.version} + + $revsionRegEx = Get-MatchingMajorMinorRegEx -Version $package.version + $newPackage = $versions | + Sort-Object -Descending | + Select-Object -First 1 + + # Get the newest matching revision + $newRevision = $versions | + Where-Object {$_.Version -match $revsionRegEx } | + Sort-Object -Descending | + Select-Object -First 1 # If the current package has a different version from the version in the csproj, print the details - if($newPackage -and $newPackage.Version.ToString() -ne $package.version) + if ($newRevision -and $newRevision.Version.ToString() -ne $package.version -or $newPackage -and $newPackage.Version.ToString() -ne $package.version -or $IncludeAll.IsPresent) { + if ($newRevision) + { + $newRevisionString = $newRevision.Version + } + else + { + # We don't have a new Revision, report the current version + $newRevisionString = $package.Version + } + + if ($newPackage) + { + $newVersionString = $newPackage.Version + } + else + { + # We don't have a new Version, report the current version + $newVersionString = $package.Version + } + [pscustomobject]@{ - Csproj = $file + Csproj = (Split-Path -Path $file -Leaf) PackageName = $name CsProjVersion = $Package.Version - NuGetVersion = $newPackage.Version + NuGetRevision = $newRevisionString + NuGetVersion = $newVersionString } } } @@ -272,6 +310,91 @@ function Get-NewOfficalPackage } } +############################## +#.SYNOPSIS +# Returns True if NewVersion is newer than Version +# Pre release are ignored if the current version is not pre-release +# If the current version is pre-release, this function only determines if the version portion is NewReleaseTag +# The calling function is responsible for sorting prelease version by publish date (as find-package gives them to you) +# and returning the newest. +# +#.PARAMETER Version +# The current Version +# +#.PARAMETER NewVersion +# The potention replacement version +# +#.OUTPUTS +# True if NewVersion should be considere as a replacement +############################## +function Test-IncludePackageVersion +{ + param( + [string] + $NewVersion, + [string] + $Version + ) + + $simpleCompare = $Version -notlike '*-*' + + if($simpleCompare -and $NewVersion -like '*-*') + { + # We are using a stable and the new version is pre-release + return $false + } + elseif($simpleCompare -and [Version]$NewVersion -ge [Version] $Version) + { + # Simple comparison says the new version is newer + return $true + } + elseif($simpleCompare) + { + # Simple comparison was done, but it was not newer + return $false + } + elseif($NewVersion -notlike '*-*') + { + # Our current version is a pre-release but the new is not + # make sure the new version is newer than the version part of the current version + $versionOnly = ($Version -Split '\-')[0] + if([Version]$NewVersion -ge [Version] $versionOnly) + { + return $true + } + else + { + return $false + } + } + else + { + # Not sure, include it + return $true + } +} + +############################## +#.SYNOPSIS +# Get a RegEx based on a version that will match the major and minor +# +#.PARAMETER Version +# The version to match +# +############################## +function Get-MatchingMajorMinorRegEx +{ + param( + [Parameter(Mandatory)] + $Version + ) + + $parts = $Version -split '\.' + + $regEx = "^$($parts[0])\.$($parts[1])\..*" + return $regEx +} + ############################## #.SYNOPSIS # Update the version number in code