Skip to content

Instantly share code, notes, and snippets.

@JPRuskin
Last active October 16, 2025 14:26
Show Gist options
  • Save JPRuskin/4c3b2d1442b42b7a79e03cf9066d5cb4 to your computer and use it in GitHub Desktop.
Save JPRuskin/4c3b2d1442b42b7a79e03cf9066d5cb4 to your computer and use it in GitHub Desktop.
A class and function to output a basic dependency graph for installed Chocolatey packages.
<#
.Synopsis
Contains a class and function to output dependencies for one or all packages on a given system.
.Description
Contains a PackageDependencyChain class that will find dependencies for locally installed, or CCR available, packages.
This can be used manually, or a tree can be shown via the Get-ChocolateyDependencies function.
.Example
.\Get-ChocolateyDependencies.ps1
# Should just output a not-very-pretty dependency tree for all installed packages.
.Example
. .\Get-ChocolateyDependencies.ps1 # Dot-sourcing or pasting the file into a terminal
Get-ChocolateyDependencies -Id vmwareworkstation
# Displays a tree for a given package.
.Example
. .\Get-ChocolateyDependencies.ps1 # Dot-sourcing or pasting the file into a terminal
[PackageDependencyChain]::new("vcredist2015").OutDependencies()
# Lists a deduped list of total dependencies a given package has.
#>
class PackageDependencyChain {
[string]$Id
#[Nuget.Versioning.VersionRange]$Version # Ignore this for now
[PackageDependencyChain[]]$Dependencies
PackageDependencyChain ($Id) {
$NuspecPath = "$env:ChocolateyInstall\lib\$Id\$Id.nuspec"
if (Test-Path $NuspecPath) {
[xml]$Nuspec = Get-Content $NuspecPath
$Namespace = [Xml.XmlNamespaceManager]::new($Nuspec.NameTable)
$Namespace.AddNamespace("nuspec", $Nuspec.package.xmlns)
$this.Id = $Nuspec.package.metadata.id
$this.Dependencies = $Nuspec.SelectNodes("//nuspec:dependency", $Namespace).id
} else {
$QueryString = "Id eq '$Id' and IsLatestVersion"
$QueryUrl = 'https://community.chocolatey.org/api/v2/Packages()?$filter={0}' -f [uri]::EscapeUriString($queryString)
[xml]$Nuspec = Invoke-WebRequest $QueryUrl -UseBasicParsing # Not actually a nuspec
$this.Id = $Nuspec.feed.entry.title.'#text'
$this.Dependencies = $Nuspec.feed.entry.properties.Dependencies.ForEach{ $_.Split(":")[0] }
}
}
[string] DisplayTree([int]$Depth = 0, [switch]$Last) {
$LastItemChar, $MoreItemsChar = [char]0x2514, [char]0x251C
$BranchType = if ($Last) {
$LastItemChar
} else {
$MoreItemsChar
}
$StringOutput = if ($Depth) {
(" " * 4 * ($Depth - 1)) + $BranchType + "$([char]0x2500)$([char]0x2500) " + $this.Id + "`n"
} else {
$this.Id + "`n"
}
foreach ($Dependency in $this.Dependencies) {
$StringOutput += $Dependency.DisplayTree($Depth + 1, $(($this.Dependencies.Count - 1) -eq $this.Dependencies.IndexOf($Dependency)))
}
# Fill in gaps between wrapped nested rows
# $StringOutput = $StringOutput.Split("`n")
# foreach ($Line in $StringOutput) {
# if (($WrapIndex = $Line.IndexOf($MoreItemsChar)) -ge 0) {
# foreach ($LineNumber in ($StringOutput.IndexOf($Line)+1)..$StringOutput.Count) {
# Write-Verbose "Checking line '$LineNumber': '$($StringOutput[$LineNumber])'" -Verbose
# if (($StringOutput[$LineNumber].ToCharArray())[$WrapIndex] -in @($LastItemChar, $MoreItemsChar)) {
# Write-Verbose "Found closing char on '$_'" -Verbose
# break
# }
# $StringOutput[$LineNumber] = -join([char[]]($StringOutput[$LineNumber])[0..($WrapIndex-1)] + '|' + ([char[]]$StringOutput[$LineNumber])[($WrapIndex+1)..$StringOutput[$LineNumber].Length])
# }
# }
# }
return $StringOutput -join "`n"
}
[string[]] OutDependencies() {
[string[]]$Packages = foreach ($Dependency in $this.Dependencies) {
$Dependency.OutDependencies()
}
return $Packages + $this.Id | Select-Object -Unique
}
}
function Get-ChocolateyDependencies {
<#
.Synopsis
Shows a tree of Chocolatey packages and their dependencies
#>
[CmdletBinding()]
param(
# If specified, only tests a specific package
[ArgumentCompleter({
param($1, $2, $WordToMatch, $4, $5)
Get-ChildItem $env:ChocolateyInstall\lib | ? BaseName -Like "$WordToMatch*" | % BaseName
if ($WordToMatch) {
Invoke-RestMethod -UseBasicParsing -Uri "https://community.chocolatey.org/json/JsonApi?invoke&action=GetSuggestions&SearchTerm=$WordToMatch" | ForEach-Object {
$_.PackageId
}
}
}
)]
[string]$Id,
# If passed, doesn't list packages that are dependencies on the top layer
[switch]$SkipTopLevelDependencies
)
process {
if ($Id) {
([PackageDependencyChain]$Id).DisplayTree(0, $false)
} else {
$Packages = [PackageDependencyChain[]](Get-ChildItem $env:ChocolateyInstall\lib | % BaseName)
if ($SkipTopLevelDependencies) {
$Packages.Where{ $_.Id -notin $_.Dependencies.Id }.DisplayTree(0, $false) # May need to tune this for nesting
} else {
$Packages.DisplayTree(0, $false)
}
}
}
}
if ($MyInvocation.InvocationName -ne ".") {
Get-ChocolateyDependencies
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment