Last active
February 27, 2020 12:25
-
-
Save chrisoldwood/0b7bc456a3698592ac77ea8446aacfd7 to your computer and use it in GitHub Desktop.
Scans dub.selections.json files looking for package version mismatches.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
.synopsis | |
Scans the dub.selections.json files under a folder looking for | |
inconsistent versions of the same package dependency. | |
.description | |
Ideally your projects should always be using a consistent set of | |
package versions to avoid impedance mismatches. This script analyses | |
all the packages used by your projects (e.g. libraries) and tells you | |
if multiple package versions are being referenced. | |
The script terminates with a non-zero exit code if any duplicates are | |
found so it can be used as part of a build pipeline. | |
.parameter ProjectsFolder | |
The path to the set of Dub projects to analyse. | |
Assumes the CWD by default. | |
.parameter IgnoreProjects | |
A comma-separated list of projects to ignore. | |
.parameter IgnorePackages | |
A comma-separated list of packages to ignore. | |
.parameter Brief | |
Displays the results in a more succint format. | |
.parameter ProjectView | |
Displays the results oriented by project rather than by package. | |
.example | |
CheckDubSelections c:\projects | |
#> | |
Param( | |
[Parameter(Position=0, Mandatory=$false)] | |
[string] $ProjectsFolder, | |
[Parameter(Mandatory=$false)] | |
[string] $IgnoreProjects, | |
[Parameter(Mandatory=$false)] | |
[string] $IgnorePackages, | |
[Parameter(Mandatory=$false)] | |
[switch] $Brief, | |
[Parameter(Mandatory=$false)] | |
[switch] $ProjectView | |
) | |
Set-StrictMode -Version Latest | |
$ErrorActionPreference = 'stop' | |
$folder = '.' | |
if ($ProjectsFolder -ne '') | |
{ | |
$folder = $ProjectsFolder | |
} | |
if (!(Test-Path $folder)) | |
{ | |
throw ("Invalid path: {0}" -f $folder) | |
} | |
$projectExclusions = @{} | |
if ($IgnoreProjects -ne '') | |
{ | |
$IgnoreProjects -split ',' | ForEach-Object { $projectExclusions[$_] = $true } | |
} | |
$packageExclusions = @{} | |
if ($IgnorePackages -ne '') | |
{ | |
$IgnorePackages -split ',' | ForEach-Object { $packageExclusions[$_] = $true } | |
} | |
$packages = @{} | |
$configFiles = @( Get-ChildItem -Path $folder -Recurse | | |
Where-Object { $_.Name -eq 'dub.selections.json' } ) | |
foreach ($file in $configFiles) | |
{ | |
function FormatProjectName($root, $path) | |
{ | |
Push-Location $root | |
$project = (Resolve-Path $path -Relative) -replace '^./','' | |
Pop-Location | |
$project | |
} | |
$parentFolder = Split-Path -Parent $file.FullName | |
$project = FormatProjectName $folder $parentFolder | |
$versions = (Get-Content $file.FullName -Raw | ConvertFrom-Json).versions | |
if (!$projectExclusions.ContainsKey($project)) | |
{ | |
foreach ($selection in $versions.PSObject.Properties) | |
{ | |
# Ignore { "path": "..." } entries | |
if ($selection.TypeNameOfValue -eq 'System.String') | |
{ | |
$packageName = $selection.Name | |
$packageVersion = $selection.Value | |
if (!$packageExclusions.ContainsKey($packageName)) | |
{ | |
$package = New-Object psobject -Property @{ | |
Name = $packageName | |
Version = $packageVersion | |
Project = $project | |
} | |
if (!$packages.ContainsKey($packageName)) | |
{ | |
$packages.Add($packageName, @{}) | |
} | |
if (!$packages[$packageName].ContainsKey($packageVersion)) | |
{ | |
$packages[$packageName].Add($packageVersion, @()) | |
} | |
$packages[$packageName][$packageVersion] += $package | |
} | |
} | |
} | |
} | |
} | |
$mismatches = @( $packages.GetEnumerator() | | |
Where-Object { $_.Value.Count -gt 1 } | | |
Sort-Object Name ) | |
if ($ProjectView) | |
{ | |
$projects = @{} | |
$mismatches | | |
ForEach-Object { $_.Value.GetEnumerator() } | | |
ForEach-Object { $_.Value } | | |
ForEach-Object { | |
$projects[$_.Project] = @() | |
} | |
foreach ($mismatch in $mismatches) | |
{ | |
$versions = $mismatch.Value | |
$oldVersions = $versions.GetEnumerator() | | |
# '$_.Name' is a flawed way to compare semvers but it's only a tiebreaker. | |
Sort-Object -Property @{Expression={ $_.Value.count }},@{Expression={ $_.Name }} -Descending | | |
Select-Object -Skip 1 | |
$oldVersions | | |
ForEach-Object { $_.Value } | | |
ForEach-Object { $projects[$_.Project] += $_ } | |
} | |
$mismatchedProjects = $projects.GetEnumerator() | | |
Where-Object { $_.Value.count -ne 0} | | |
Sort-Object Name | |
foreach ($project in $mismatchedProjects) | |
{ | |
if ($Brief) | |
{ | |
$packages = @($project.Value.GetEnumerator() | | |
ForEach-Object { $_.Name } | |
) -join ' ' | |
Write-Output ('{0}: {1}' -f $project.Name,$packages) | |
} | |
else | |
{ | |
$heading = 'Project: ' + $project.Name | |
Write-Output $heading | |
Write-Output ('=' * $heading.Length) | |
foreach ($package in $project.Value.GetEnumerator()) | |
{ | |
Write-Output ('{0}: {1}' -f $package.Name,$package.Version) | |
} | |
Write-Output '' | |
} | |
} | |
} | |
else | |
{ | |
foreach ($mismatch in $mismatches) | |
{ | |
if ($Brief) | |
{ | |
$package = $mismatch.Name | |
$versions = @($mismatch.Value.GetEnumerator() | | |
ForEach-Object { '({0} : {1})' -f $_.Name,$_.Value.length } | |
) -join ' ' | |
Write-Output ('{0}: {1}' -f $package,$versions) | |
} | |
else | |
{ | |
$heading = 'Package: ' + $mismatch.Name | |
Write-Output $heading | |
Write-Output ('=' * $heading.Length) | |
foreach ($version in $mismatch.Value.GetEnumerator()) | |
{ | |
$versionNumber = $version.Name | |
$projects = ($version.Value | ForEach-Object { $_.Project }) -Join ', ' | |
Write-Output ('{0}: {1}' -f $versionNumber,$projects) | |
} | |
Write-Output '' | |
} | |
} | |
} | |
$mismatchCount = $mismatches.Count | |
if ($mismatchCount -ne 0) | |
{ | |
$term = if ($mismatchCount -eq 1) { 'mismatch' } else { 'mismatches' } | |
Write-Warning "$mismatchCount package $term found" | |
exit 1 | |
} | |
else | |
{ | |
Write-Output "No package mismatches found" | |
exit 0 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Overview
The script recursively looks for the
dub.selections.json
files and compares the version numbers of each package to ensure that the same version of each package is referenced in each project.When a mismatch is found it writes out which projects reference which version, e.g.
The script can be run automatically in a build pipeline as it correctly sets the exit code -
0
for success, or!0
if any mismatches have been found.Options
-IgnoreProjects
: You can exclude one or more projects using a comma-separated list, e.g.-IgnorePackages
: You can also exclude one or more packages using a comma-separated list, e.g.-ProjectView
: Instead of listing packages and their versions this switch pivots the results to list projects and their outdated packages. Here the term "outdated" means "a project which references a package version which is different to what the majority of other projects are using".-Brief
: The results are displayed in quite a verbose format by default, i.e. listing the package names, versions and projects. By using the-Brief
switch the results are displayed in a single-line format by summarising some of the details, e.g....and this can also be used with
-ProjectView
too, e.g.