Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Version ranges in the `Version` field using NuGet range syntax (e.g.
`'[2.2.3,3.0)'`, `'[2.0,)'`, `'(,3.0)'`) for the `PSGalleryModule`,
`PSResourceGet`, and `PSGalleryNuget` dependency types. A bare version
(e.g. `'3.2.1'`) still means that exact version; a range installs the
highest available version that satisfies it (#65, #91).
- `FileDownload` is now supported on all platforms (`windows`, `core`,
`macos`, `linux`); there was no Windows-only code blocking this (#98).
- `FileDownload` relative `Target` paths are now rooted against `$PWD`
Expand Down
5 changes: 5 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ _Avoid_: dependency (to avoid confusion with the Dependency concept), requiremen
A label on a Dependency that controls inclusion when `Invoke-PSDepend` is called with `-Tags`.
_Avoid_: filter, category, label

**VersionRange**:
A constraint on which versions of a Dependency satisfy it, expressed in NuGet range syntax (e.g. `[2.2.3,3.0)`, `[2.0,)`) inside the Version field. A bare version (`3.2.1`) is not a range — it means exactly that version.
_Avoid_: version spec, version constraint, MinimumVersion/MaximumVersion

## Relationships

- A **DependencyFile** contains one or more **Dependencies** and at most one **PSDependOptions** block
Expand All @@ -48,6 +52,7 @@ _Avoid_: filter, category, label
- A **Dependency** may carry zero or more **Tags**
- A **DependencyScript** receives a **Dependency** and a set of **PSDependAction** flags on each invocation
- **Target** is a field on a **Dependency** interpreted differently by each **DependencyScript**
- A **Dependency**'s Version field carries either an exact version or a **VersionRange**; each gallery **DependencyScript** resolves a **VersionRange** to a concrete version to install

## Example dialogue

Expand Down
63 changes: 42 additions & 21 deletions PSDepend/PSDependScripts/PSGalleryModule.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
Relevant Dependency metadata:
Name: The name for this module
Version: Used to identify existing installs meeting this criteria, and as RequiredVersion for installation. Defaults to 'latest'
Also accepts a NuGet version range (e.g. '[2.2.3,3.0)', '[2.0,)', '(,3.0)'). A bare version (e.g. '3.2.1') still means that exact version. When a range is given, the highest available version that satisfies it is installed.
Target: Used as 'Scope' for Install-Module. If this is a path, we use Save-Module with this path. On reruns, PSDepend checks existing modules first and skips reinstalling when the requested version is already present. Defaults to 'AllUsers'
AddToPath: If target is used as a path, prepend that path to ENV:PSModulePath
Credential: The username and password used to authenticate against the private repository
Expand Down Expand Up @@ -90,6 +91,13 @@
}
}
# Install the latest version of PowerCLI, allowing for prerelease

.EXAMPLE
@{
BuildHelpers = '[2.0.0,3.0.0)'
}

# Install the highest BuildHelpers version that is >= 2.0.0 and < 3.0.0 (NuGet range syntax)
#>
[CmdletBinding()]
param(
Expand Down Expand Up @@ -192,8 +200,14 @@ if ($Repository) {
$params.Add('Repository', $Repository)
}

# Exact versions map straight to RequiredVersion. Ranges have no Install-Module
# parameter, so they are resolved to a concrete version just before install.
$versionRange = $null
if ($Version -and $Version -ne 'latest') {
$Params.add('RequiredVersion', $Version)
$versionRange = ConvertFrom-VersionRange -Version $Version
if ($versionRange -and $versionRange.IsExact) {
$Params.add('RequiredVersion', $versionRange.Exact)
}
}
Comment on lines +203 to 211

if ($Credential) {
Expand Down Expand Up @@ -230,7 +244,7 @@ if ($Existing) {
Write-Verbose "Found existing module [$Name]"

if ($Version -and $Version -ne 'latest') {
$matchedInstall = $Existing | Where-Object { Test-VersionEquality $Version $_.Version.ToString() } | Select-Object -First 1
$matchedInstall = $Existing | Where-Object { Test-VersionInRange -Version $_.Version.ToString() -Required $Version } | Select-Object -First 1
if ($matchedInstall) {
Write-Verbose "You have the requested version [$Version] of [$Name]"
Import-PSDependModule -Name $ModuleName -Action $PSDependAction -Version $matchedInstall.Version
Expand All @@ -253,25 +267,7 @@ if ($Existing) {
}

$GalleryVersion = Find-Module @FindModuleParams | Measure-Object -Property Version -Maximum | Select-Object -ExpandProperty Maximum
[System.Version]$parsedExistingVersion = $null
[System.Version]$parsedGalleryVersion = $null
[System.Management.Automation.SemanticVersion]$parsedExistingSemanticVersion = $null
[System.Management.Automation.SemanticVersion]$parsedGallerySemanticVersion = $null
$isGalleryVersionLessEquals = if (
[System.Management.Automation.SemanticVersion]::TryParse([string]$ExistingVersion, [ref]$parsedExistingSemanticVersion) -and
[System.Management.Automation.SemanticVersion]::TryParse([string]$GalleryVersion, [ref]$parsedGallerySemanticVersion)
) {
$parsedGallerySemanticVersion -le $parsedExistingSemanticVersion
}
elseif (
[System.Version]::TryParse([string]$ExistingVersion, [ref]$parsedExistingVersion) -and
[System.Version]::TryParse([string]$GalleryVersion, [ref]$parsedGalleryVersion)
) {
$parsedGalleryVersion -le $parsedExistingVersion
}
else {
$false
}
$isGalleryVersionLessEquals = (Compare-Version -ReferenceVersion ([string]$GalleryVersion) -DifferenceVersion ([string]$ExistingVersion)) -le 0

# latest, and we have latest
if ( $Version -and ($Version -eq 'latest' -or $Version -eq '') -and $isGalleryVersionLessEquals) {
Expand All @@ -292,6 +288,31 @@ if ( $PSDependAction -contains 'Test' -and $PSDependAction.count -eq 1) {
return $False
}

# Resolve a version range to the highest available version that satisfies it,
# then install that exact version (Install-Module has no native range parameter).
if ($versionRange -and -not $versionRange.IsExact -and $PSDependAction -contains 'Install') {
$resolveParams = @{ Name = $Name }
if ($Repository) { $resolveParams.Add('Repository', $Repository) }
if ($Credential) { $resolveParams.Add('Credential', $Credential) }
if ($AllowPrerelease) { $resolveParams.Add('AllowPrerelease', $AllowPrerelease) }

$resolvedVersion = $null
foreach ($candidate in (Find-Module @resolveParams -AllVersions)) {
$candidateVersion = $candidate.Version.ToString()
if ((Test-VersionInRange -Version $candidateVersion -Required $Version) -and
($null -eq $resolvedVersion -or (Compare-Version -ReferenceVersion $candidateVersion -DifferenceVersion $resolvedVersion) -gt 0)) {
$resolvedVersion = $candidateVersion
}
}

if (-not $resolvedVersion) {
Write-Error "No version of [$Name] in repository [$Repository] satisfies range [$Version]"
return
}
Write-Verbose "Resolved range [$Version] to version [$resolvedVersion] for [$Name]"
$params['RequiredVersion'] = $resolvedVersion
}

if ($PSDependAction -contains 'Install') {
if ('AllUsers', 'CurrentUser' -contains $Scope) {
Write-Verbose "Installing [$Name] with scope [$Scope]"
Expand Down
51 changes: 28 additions & 23 deletions PSDepend/PSDependScripts/PSGalleryNuget.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
Relevant Dependency metadata:
Name: The name for this module
Version: Used to identify existing installs meeting this criteria, and as RequiredVersion for installation. Defaults to 'latest'
Also accepts a NuGet version range (e.g. '[2.2.3,3.0)', '[2.0,)', '(,3.0)'). A bare version (e.g. '0.1.19') still means that exact version. When a range is given, the highest available version that satisfies it is installed.
Source: Source Uri for Nuget. Defaults to https://www.powershellgallery.com/api/v2/
Target: Required path to save this module. No Default
Example: To install PSDeploy to C:\temp\PSDeploy, I would specify C:\temp
Expand Down Expand Up @@ -123,9 +124,9 @@ if (Test-Path $ModulePath) {
$ExistingVersion = $ManifestData.ModuleVersion
$GetGalleryVersion = { (Find-NugetPackage -Name $Name -PackageSourceUrl $Source -Credential $Credential -IsLatest).Version }

# Version string, and equal to current
# Version string (exact or range), and the installed version satisfies it
if ($Version -and $Version -ne 'latest') {
if (Test-VersionEquality $Version $ExistingVersion) {
if (Test-VersionInRange -Version $ExistingVersion -Required $Version) {
Write-Verbose "You have the requested version [$Version] of [$Name]"
# Conditional import
Import-PSDependModule -Name $ModulePath -Action $PSDependAction -Version $ExistingVersion
Expand All @@ -139,25 +140,7 @@ if (Test-Path $ModulePath) {
# latest, and we have latest
if ($Version -and ($Version -eq 'latest' -or $Version -like '')) {
$GalleryVersion = & $GetGalleryVersion
[System.Version]$parsedExistingVersion = $null
[System.Version]$parsedGalleryVersion = $null
[System.Management.Automation.SemanticVersion]$parsedExistingSemanticVersion = $null
[System.Management.Automation.SemanticVersion]$parsedGallerySemanticVersion = $null
$isGalleryVersionLessEquals = if (
[System.Management.Automation.SemanticVersion]::TryParse([string]$ExistingVersion, [ref]$parsedExistingSemanticVersion) -and
[System.Management.Automation.SemanticVersion]::TryParse([string]$GalleryVersion, [ref]$parsedGallerySemanticVersion)
) {
$parsedGallerySemanticVersion -le $parsedExistingSemanticVersion
}
elseif (
[System.Version]::TryParse([string]$ExistingVersion, [ref]$parsedExistingVersion) -and
[System.Version]::TryParse([string]$GalleryVersion, [ref]$parsedGalleryVersion)
) {
$parsedGalleryVersion -le $parsedExistingVersion
}
else {
$false
}
$isGalleryVersionLessEquals = (Compare-Version -ReferenceVersion ([string]$GalleryVersion) -DifferenceVersion ([string]$ExistingVersion)) -le 0

if ($isGalleryVersionLessEquals) {
Write-Verbose "You have the latest version of [$Name], with installed version [$ExistingVersion] and PSGallery version [$GalleryVersion]"
Expand Down Expand Up @@ -190,6 +173,28 @@ if ( $PSDependAction -contains 'Test' -and $PSDependAction.count -eq 1) {
return $False
}

# Resolve a version range to the highest available version that satisfies it;
# nuget.exe -version takes an exact version, not a range.
$installVersion = $Version
if ($Version -and $Version -notlike 'latest') {
$range = ConvertFrom-VersionRange -Version $Version
if ($range -and -not $range.IsExact) {
$resolvedVersion = $null
foreach ($candidate in (Find-NugetPackage -Name $Name -PackageSourceUrl $Source -Credential $Credential)) {
if ((Test-VersionInRange -Version $candidate.Version -Required $Version) -and
($null -eq $resolvedVersion -or (Compare-Version -ReferenceVersion $candidate.Version -DifferenceVersion $resolvedVersion) -gt 0)) {
$resolvedVersion = $candidate.Version
}
}
if (-not $resolvedVersion) {
Write-Error "No version of [$Name] at source [$Source] satisfies range [$Version]"
return
}
Write-Verbose "Resolved range [$Version] to version [$resolvedVersion] for [$Name]"
$installVersion = $resolvedVersion
}
}
Comment on lines +176 to +196

if ($PSDependAction -contains 'Install') {
$TargetExists = Test-Path $Target -PathType Container

Expand All @@ -200,7 +205,7 @@ if ($PSDependAction -contains 'Install') {
$Null = New-Item -ItemType Directory -Path $Target -Force -ErrorAction SilentlyContinue
}
if ($Version -and $Version -notlike 'latest') {
$NugetParams += '-version', $Version
$NugetParams += '-version', $installVersion
}
$NugetParams = 'install', $Name + $NugetParams

Expand All @@ -209,6 +214,6 @@ if ($PSDependAction -contains 'Install') {

# Conditional import
$importVs = if ($Version -and $Version -notlike 'latest') {
$Version
$installVersion
}
Import-PSDependModule -Name $ModulePath -Action $PSDependAction -Version $importVs
25 changes: 3 additions & 22 deletions PSDepend/PSDependScripts/PSResourceGet.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -256,10 +256,10 @@ if ($Existing) {
$FindModuleParams.Add('Prerelease', $true)
}

# Version string, and that version is already installed (may not be the maximum)
# Version string (exact or range), and a satisfying version is already installed
$matchedExisting = if ($Version -and $Version -ne 'latest') {
$Existing | Where-Object {
Test-VersionEquality -ReferenceVersion $_.Version -DifferenceVersion $Version
Test-VersionInRange -Version $_.Version -Required $Version
} | Select-Object -First 1
}
if ($matchedExisting) {
Expand All @@ -273,26 +273,7 @@ if ($Existing) {
}

$GalleryVersion = Find-PSResource @FindModuleParams | Measure-Object -Property Version -Maximum | Select-Object -ExpandProperty Maximum
# Compare using SemanticVersion first (PSResourceGet uses SemVer); fall back to System.Version
[System.Version]$parsedVersion = $null
[System.Version]$parsedGalleryVersion = $null
[System.Management.Automation.SemanticVersion]$parsedSemanticVersion = $null
[System.Management.Automation.SemanticVersion]$parsedTempSemanticVersion = $null
$existingIsUpToDate = if (
[System.Management.Automation.SemanticVersion]::TryParse([string]$ExistingVersion, [ref]$parsedSemanticVersion) -and
[System.Management.Automation.SemanticVersion]::TryParse([string]$GalleryVersion, [ref]$parsedTempSemanticVersion)
) {
$parsedTempSemanticVersion -le $parsedSemanticVersion
}
elseif (
[System.Version]::TryParse([string]$ExistingVersion, [ref]$parsedVersion) -and
[System.Version]::TryParse([string]$GalleryVersion, [ref]$parsedGalleryVersion)
) {
$parsedGalleryVersion -le $parsedVersion
}
else {
$false
}
$existingIsUpToDate = (Compare-Version -ReferenceVersion ([string]$GalleryVersion) -DifferenceVersion ([string]$ExistingVersion)) -le 0

# latest, and we have latest
if ($Version -and ($Version -eq 'latest' -or $Version -eq '') -and $existingIsUpToDate) {
Expand Down
77 changes: 77 additions & 0 deletions PSDepend/Private/Compare-Version.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
function Compare-Version {
<#
.SYNOPSIS
Order two version strings, returning -1, 0, or 1.

.DESCRIPTION
Coerce both version strings to a common comparable type and compare them via
[IComparable]. SemanticVersion is tried first so pre-release ordering is
honoured (e.g. 1.0.0-alpha sorts below 1.0.0); System.Version is the
fallback so four-part versions (1.2.3.4) still compare. Missing System.Version
components are normalised to 0 so 1.2.3 and 1.2.3.0 compare equal. If neither

Check warning on line 11 in PSDepend/Private/Compare-Version.ps1

View workflow job for this annotation

GitHub Actions / Continuous Integration / Run Linters

Unknown word (normalised) Suggestions: (normalized, normalize, normalizer, normalcies, normalizes)
type can parse both inputs, fall back to an ordinal string comparison.

Both operands must coerce to the same type - a SemanticVersion cannot be
compared to a System.Version - so each branch requires both inputs to parse.

.PARAMETER ReferenceVersion
The version on the left of the comparison.

.PARAMETER DifferenceVersion
The version on the right of the comparison.

.EXAMPLE
Compare-Version -ReferenceVersion '1.2.0' -DifferenceVersion '1.2.3'

Returns -1 (1.2.0 is less than 1.2.3).

.EXAMPLE
Compare-Version -ReferenceVersion '1.0.0' -DifferenceVersion '1.0.0-beta'

Returns 1 (a release sorts above its pre-release).
#>
[CmdletBinding()]
[OutputType([int])]
param(
[string]$ReferenceVersion,
[string]$DifferenceVersion
)

# SemanticVersion first: it orders pre-release labels correctly.
[System.Management.Automation.SemanticVersion]$refSemVer = $null
[System.Management.Automation.SemanticVersion]$diffSemVer = $null
if (
[System.Management.Automation.SemanticVersion]::TryParse($ReferenceVersion, [ref]$refSemVer) -and
[System.Management.Automation.SemanticVersion]::TryParse($DifferenceVersion, [ref]$diffSemVer)
) {
return $refSemVer.CompareTo($diffSemVer)
}

# System.Version fallback handles four-part versions SemVer rejects.
# Normalise absent components (-1) to 0 so 1.2.3 equals 1.2.3.0.

Check warning on line 51 in PSDepend/Private/Compare-Version.ps1

View workflow job for this annotation

GitHub Actions / Continuous Integration / Run Linters

Unknown word (Normalise) Suggestions: (normalize, normalcies, normalizes, normale, Normale)
[System.Version]$refVer = $null
[System.Version]$diffVer = $null
if (
[System.Version]::TryParse($ReferenceVersion, [ref]$refVer) -and
[System.Version]::TryParse($DifferenceVersion, [ref]$diffVer)
) {
$refNormalised = [System.Version]::new(

Check warning on line 58 in PSDepend/Private/Compare-Version.ps1

View workflow job for this annotation

GitHub Actions / Continuous Integration / Run Linters

Unknown word (Normalised) Suggestions: (normalized, normalize, normalizer, normalcies, normalizes)
[Math]::Max($refVer.Major, 0),
[Math]::Max($refVer.Minor, 0),
[Math]::Max($refVer.Build, 0),
[Math]::Max($refVer.Revision, 0)
)
$diffNormalised = [System.Version]::new(

Check warning on line 64 in PSDepend/Private/Compare-Version.ps1

View workflow job for this annotation

GitHub Actions / Continuous Integration / Run Linters

Unknown word (Normalised) Suggestions: (normalized, normalize, normalizer, normalcies, normalizes)
[Math]::Max($diffVer.Major, 0),
[Math]::Max($diffVer.Minor, 0),
[Math]::Max($diffVer.Build, 0),
[Math]::Max($diffVer.Revision, 0)
)
return $refNormalised.CompareTo($diffNormalised)

Check warning on line 70 in PSDepend/Private/Compare-Version.ps1

View workflow job for this annotation

GitHub Actions / Continuous Integration / Run Linters

Unknown word (Normalised) Suggestions: (normalized, normalize, normalizer, normalcies, normalizes)

Check warning on line 70 in PSDepend/Private/Compare-Version.ps1

View workflow job for this annotation

GitHub Actions / Continuous Integration / Run Linters

Unknown word (Normalised) Suggestions: (normalized, normalize, normalizer, normalcies, normalizes)
}

# Neither type parses both: ordinal string comparison, clamped to -1/0/1.
return [Math]::Sign(
[string]::Compare($ReferenceVersion, $DifferenceVersion, [System.StringComparison]::OrdinalIgnoreCase)
)
Comment on lines +73 to +76
}
Loading
Loading