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**; the `PSGalleryModule` and `PSGalleryNuget` **DependencyScripts** resolve a **VersionRange** to a concrete version to install, while `PSResourceGet` passes the range to `Install-PSResource` and lets it resolve

## Example dialogue

Expand Down
64 changes: 43 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,20 @@ 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 is kept only to detect exact-vs-range here; the resolution below
# re-derives the range per candidate via Test-VersionInRange.
$versionRange = $null
if ($Version -and $Version -ne 'latest') {
$Params.add('RequiredVersion', $Version)
$versionRange = ConvertFrom-VersionRange -Version $Version
if (-not $versionRange) {
Write-Error "Could not parse version [$Version] for [$Name]; expected an exact version or a valid NuGet range."
return
}
if ($versionRange.IsExact) {
$params.Add('RequiredVersion', $versionRange.Exact)
}
}
Comment thread
HeyItsGilbert marked this conversation as resolved.

if ($Credential) {
Expand Down Expand Up @@ -230,7 +250,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 +273,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 +294,26 @@ 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) }

$candidates = Find-Module @resolveParams -AllVersions | ForEach-Object { $_.Version.ToString() }
$resolvedVersion = Resolve-VersionInRange -Candidate $candidates -Required $Version

if (-not $resolvedVersion) {
$repositoryLabel = if ($Repository) { $Repository } else { 'the default repositories' }
Write-Error "No version of [$Name] in [$repositoryLabel] 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
50 changes: 27 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,27 @@ 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 (-not $range) {
Write-Error "Could not parse version [$Version] for [$Name]; expected an exact version or a valid NuGet range."
return
}
if (-not $range.IsExact) {
$candidates = (Find-NugetPackage -Name $Name -PackageSourceUrl $Source -Credential $Credential).Version
$resolvedVersion = Resolve-VersionInRange -Candidate $candidates -Required $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 thread
HeyItsGilbert marked this conversation as resolved.

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

Expand All @@ -200,7 +204,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 +213,6 @@ if ($PSDependAction -contains 'Install') {

# Conditional import
$importVs = if ($Version -and $Version -notlike 'latest') {
$Version
$installVersion
}
Import-PSDependModule -Name $ModulePath -Action $PSDependAction -Version $importVs
55 changes: 31 additions & 24 deletions PSDepend/PSDependScripts/PSResourceGet.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
Relevant Dependency metadata:
Name: The name of the module to install
Version: Used to identify existing installs and as -Version for installation.
Supports NuGet range syntax (e.g. '[1.0.0, ]'). 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. Ranges are passed through to Install-PSResource, which
resolves them. Defaults to 'latest'.
Target: Used as -Scope for Install-PSResource (CurrentUser or AllUsers).
If this is a filesystem path, Save-PSResource is used instead.
Defaults to 'CurrentUser'.
Expand Down Expand Up @@ -117,6 +120,17 @@
}

# Install the latest version of PowerCLI, allowing prerelease versions.

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

# Install the highest BuildHelpers version that is >= 2.0.0 and < 3.0.0
# (NuGet range syntax). The range is passed to Install-PSResource -Version.
#>

[CmdletBinding()]
Expand Down Expand Up @@ -156,6 +170,18 @@ if (-not $Version) {
$Version = 'latest'
}

# PSResourceGet understands NuGet ranges natively, so a range is passed straight
# through to Install-PSResource. Parsing here fails fast on malformed input and
# detects ranges for import resolution, keeping range detection in one place.
$versionRange = $null
if ($Version -and $Version -ne 'latest') {
$versionRange = ConvertFrom-VersionRange -Version $Version
if (-not $versionRange) {
Write-Error "Could not parse version [$Version] for [$Name]; expected an exact version or a valid NuGet range."
return
}
}

# Target doubles as Scope: AllUsers/CurrentUser = install scope; any other value = filesystem path
if (-not $Dependency.Target) {
$Scope = 'CurrentUser'
Expand Down Expand Up @@ -256,10 +282,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 +299,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 Expand Up @@ -329,7 +336,7 @@ if ($PSDependAction -contains 'Install') {

# Conditional import — params['Version'] may be a NuGet range; resolve to a concrete installed version
$importVs = $params['Version']
if ($importVs -and $importVs -match '[\[\](,]') {
if ($versionRange -and -not $versionRange.IsExact) {
$importVs = Get-Module -ListAvailable -Name $ModuleName -ErrorAction SilentlyContinue |
Measure-Object -Property Version -Maximum |
Select-Object -ExpandProperty Maximum
Expand Down
Loading
Loading