Skip to content

Instantly share code, notes, and snippets.

@chrisoldwood
Last active February 27, 2020 12:25
Show Gist options
  • Save chrisoldwood/0b7bc456a3698592ac77ea8446aacfd7 to your computer and use it in GitHub Desktop.
Save chrisoldwood/0b7bc456a3698592ac77ea8446aacfd7 to your computer and use it in GitHub Desktop.
Scans dub.selections.json files looking for package version mismatches.
<#
.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
}
@chrisoldwood
Copy link
Author

chrisoldwood commented Feb 25, 2020

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.

> CheckDubSelections.ps1
Package: vibe-core
==================
1.2.0: Project-C
1.4.3: Project-B
1.8.1: Project-A

WARNING: 1 package mismatch found

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.

> CheckDubSelections.ps1 -IgnoreProjects 'project-a,folder/project-b'
...

-IgnorePackages: You can also exclude one or more packages using a comma-separated list, e.g.

> CheckDubSelections.ps1 -IgnorePackages 'vibe-core,vibe-d'
...

-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".

> CheckDubSelections.ps1 -ProjectView
Project: Project-C
=============
vibe-core: 1.2.0

Project: Project-B
=============
vibe-core: 1.4.3

WARNING: 1 package mismatch found

-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.

> CheckDubSelections.ps1 -Brief
vibe-core: (1.2.0 : 1) (1.8.1 : 2)
WARNING: 1 package mismatch found

...and this can also be used with -ProjectView too, e.g.

> CheckDubSelections.ps1 -Brief -ProjectView
Project-C: vibe-core
Project-B: vibe-core
WARNING: 1 package mismatch found

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment