Invoke-Build with `Set-StrictMode -Version [123]` Results in Exception
uw-dc opened this issue · comments
Running
Invoke-Build -Task . -File build.ps1
Results in:
InvokeBuild\5.8.5\Invoke-Build.ps1 : The variable '$_' cannot be retrieved because it has not been set.
When the build file contains:
Set-StrictMode -Version 1
I think the problem is here:
Line 63 in b4e12f0
At this point, there is no session state for Invoke-Build so inevitably, the value of $_
is $null
If I use an alternative variable name for the session state, e.g...
$ROFL = if ($ROFL = $PSCmdlet.SessionState.PSVariable.Get('*')) {if ($ROFL.Description -eq 'IB') {$ROFL.Value}}
New-Variable * -Description IB ([PSCustomObject]@{
All = [System.Collections.Specialized.OrderedDictionary]([System.StringComparer]::OrdinalIgnoreCase)
Tasks = [System.Collections.Generic.List[object]]@()
Errors = [System.Collections.Generic.List[object]]@()
Warnings = [System.Collections.Generic.List[object]]@()
Redefined = @()
Doubles = @()
Started = [DateTime]::Now
Elapsed = $null
Error = 'Invalid arguments.'
Task = $null
File = $BuildFile = $PSBoundParameters['File']
Safe = $PSBoundParameters['Safe']
Summary = $PSBoundParameters['Summary']
CD = $OriginalLocation = *Path
DP = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
SP = @{}
P = $ROFL
A = 1
B = 0
Q = 0
H = @{}
EnterBuild = $null
ExitBuild = $null
EnterTask = $null
ExitTask = $null
EnterJob = $null
ExitJob = $null
Header = if ($ROFL) {$ROFL.Header} else {{Write-Build 11 "Task $($args[0])"}}
Footer = if ($ROFL) {$ROFL.Footer} else {{Write-Build 11 "Done $($args[0]) $($Task.Elapsed)"}}
Data = @{}
XBuild = $null
XCheck = $null
})
... the exception is not thrown.
Simple script to demonstrate:
function test {
[cmdletbinding()]
param ()
$_ = if ($_ = $PSCmdlet.SessionState.PSVariable.Get('*')) {if ($_.Description -eq 'TEST') {$_.Value}}
$someVar = $_
return "BrokenWithStrictMode"
}
Set-StrictMode -Version 3
test
"Fixed" script:
function test {
[cmdletbinding()]
param ()
$SomeVar = if ($SomeVar = $PSCmdlet.SessionState.PSVariable.Get('*')) {if ($SomeVar.Description -eq 'TEST') {$SomeVar.Value}}
return "WORKS"
}
Set-StrictMode -Version 3
test
I cannot reproduce the problem. Your script works and prints "BrokenWithStrictMode"
as expected.
Some info about the environment I'm testing in
$PSVersionTable
Name Value
---- -----
PSVersion 5.1.19041.1320
PSEdition Desktop
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...}
BuildVersion 10.0.19041.1320
CLRVersion 4.0.30319.42000
WSManStackVersion 3.0
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
I've raised a PR: #191
Yours to do with as you wish, especially given my low level of familiarity with this project. Must admit I'm curious about assigning to $PSItem; is there a reason for using it like this?
Thanks and Happy New Year.
Happy New Year!
I would like to understand what is going on your side.
When you get this error, what is the full error text, including the location?
Could you please also dump the whole $error
and see some hints to a different location of the error?
I'm curious about assigning to $PSItem; is there a reason for using it like this?
(1) Yes. We avoid noise variables in IB as much as possible. Some would find this not important, we find this important.
(2) Is there a reason not to use it? It's just a variable.
My PS version is 5.1.19041.610, a bit lower.
What is you Windows version?
PS C:\Users\user\projects\corp-dsc-build> C:\Users\user\projects\corp-dsc-build\output\RequiredModules\InvokeBuild\5.8.5\Invoke-Build.ps1 : The variable '$_' cannot be retrieved because it has not been set.
At C:\Users\user\projects\corp-dsc-build\build.ps1:145 char:9
+ Invoke-Build @PSBoundParameters -Task $Tasks -File $MyInvocat ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Invoke-Build.ps1], Exception
+ FullyQualifiedErrorId : Invoke-Build.ps1
(1) Yes. We avoid noise variables in IB as much as possible. Some would find this not important, we find this important.
Fair enough
(2) Is there a reason not to use it? It's just a variable.
PSScriptAnalyser advises against it:
https://docs.microsoft.com/en-us/powershell/utility-modules/psscriptanalyzer/rules/AvoidAssignmentToAutomaticVariable?view=ps-modules
It was only curiosity that I queried it. I'm not suggesting it's changed.
Edit: Formatting
Thank you for the report, I appreciate it a lot. Do not get me wrong, we should first understand what's happening and why.
PSScriptAnalyser advises against it:
And there is another long discussion among PS community on why this does not apply to $_
.
TL;DR (1) It is automatic only in a particular context; (2) It is useful for assigning in scripts in many cases.
This is an excerpt from the build.ps1
script this is running from.
It's something I've inherited and I'm in the throes of cleaning it up.
I've had to omit parts, I'm sorry I'm limited on how helpful I can be.
I can take a freshly built Windows Desktop, and test with a simple build script and then add Set-StrictMode -Version (1|2|3|Latest)
and see what happens, but that won't be today.
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingCmdletAliases', 'task')]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'PSDependTarget')]
[CmdletBinding()]
param
(
[Parameter(Position = 0)]
[ValidateSet('?', 'Clean', 'ModuleBuilder', 'TestModules', 'TestQA', 'PublishDSCResourceModules')]
[string[]]$Tasks = '.',
)
BEGIN {
Set-StrictMode -Version 3
$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
if ($MyInvocation.ScriptName -notLike '*Invoke-Build.ps1')
{
// Do Stuff
Write-Host -ForegroundColor Green '[pre-build] Starting Build Init'
Push-Location $PSScriptRoot -StackName BuildModule
}
if ($MyInvocation.ScriptName -notLike '*Invoke-Build.ps1')
{
Invoke-Build @PSBoundParameters -Task $Tasks -File $MyInvocation.MyCommand.Path
Pop-Location -StackName BuildModule
return
}
}
PROCESS {
if ($MyInvocation.ScriptName -notLike '*Invoke-Build.ps1')
{
# Only run the process block through InvokeBuild (Look at the Begin block at the bottom of this script)
return
}
task ModuleBuilder -Before TestQA {
$DSCConfigurationModules = (Get-ChildItem -Path "$PSScriptRoot\*Config" -Directory | Where-Object { $_.Name -ne 'tpDSCBuildConfig' })
$DSCConfigurationModules = ($DSCConfigurationModules | Select-Object -Unique)
# add list of utility related modules that are also part of the project.
$utilityFunctions = @(
'DscLcmSetup'
'DscUtility'
'tpDSCBuildConfig'
)
forEach ($utilityFunction in $utilityFunctions)
{
$ModulePath = (Join-Path -Path $PSScriptRoot -ChildPath $utilityFunction)
Write-Build -Color Yellow -Text "Processing DSC Utility Module '$utilityFunction'."
PublishDscConfigurationModule -ModulePath $ModulePath -ModuleType 'Utility'
}
forEach ($dscConfigurationModule in $DSCConfigurationModules)
{
Write-Build -Color Yellow -Text "Processing DSC Configuration Module '$dscConfigurationModule'."
PublishDscConfigurationModule -ModulePath $dscConfigurationModule
}
# install any dscresource modules which are required by the configuration modules
if ($DSCConfigurationModules.count -gt 0)
{
InstallDscResourceModule -DSCConfigurationModules $DSCConfigurationModules
}
}
}
Thank you for taking the time to understand.
What are these in your case:
When you get this error, what is the full error text, including the location?
Could you please also dump the whole $error and see some hints to a different location of the error?
This is your script "cleaned" which works and prints "Happy to run"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingCmdletAliases', 'task')]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'PSDependTarget')]
[CmdletBinding()]
param
(
[Parameter(Position = 0)]
[ValidateSet('?', 'Clean', 'ModuleBuilder', 'TestModules', 'TestQA', 'PublishDSCResourceModules')]
[string[]]$Tasks = '.'
)
BEGIN {
Set-StrictMode -Version 3
$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
if ($MyInvocation.ScriptName -notLike '*Invoke-Build.ps1')
{
# Do Stuff
Write-Host -ForegroundColor Green '[pre-build] Starting Build Init'
Push-Location $PSScriptRoot -StackName BuildModule
}
if ($MyInvocation.ScriptName -notLike '*Invoke-Build.ps1')
{
Invoke-Build @PSBoundParameters -Task $Tasks -File $MyInvocation.MyCommand.Path
Pop-Location -StackName BuildModule
return
}
}
PROCESS {
if ($MyInvocation.ScriptName -notLike '*Invoke-Build.ps1')
{
# Only run the process block through InvokeBuild (Look at the Begin block at the bottom of this script)
return
}
task ModuleBuilder {
"Happy to run"
}
}
BTW BEGIN
and PROCESS
are redundant in IB scripts. They only make sense when scripts are designed for pipeline input, not the case for IB build scripts.
ScriptStackTrace:
[DBG]: PS C:\Users\user\projects\corp-dsc-build> $_.ScriptStackTrace
at *Die, C:\Users\user\projects\corp-dsc-build\output\RequiredModules\InvokeBuild\5.8.5\Invoke-Build.ps1: line 31
at <ScriptBlock><DynamicParam><trap>, C:\Users\user\projects\corp-dsc-build\output\RequiredModules\InvokeBuild\5.8.5\Invoke-Build.ps1: line 43
at <ScriptBlock><DynamicParam>, C:\Users\user\projects\corp-dsc-build\output\RequiredModules\InvokeBuild\5.8.5\Invoke-Build.ps1: line 61
at <ScriptBlock><Begin>, C:\Users\user\projects\corp-dsc-build\build.ps1: line 247
InvocationInfo:
Line : Invoke-Build -Task $Tasks -File $MyInvocation.MyCommand.Path
PositionMessage : At C:\Users\user\projects\corp-dsc-build\build.ps1:247 char:13
+ Invoke-Build -Task $Tasks -File $MyInv ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
PSScriptRoot : C:\Users\user\projects\corp-dsc-build
PSCommandPath : C:\Users\user\projects\corp-dsc-build\build.ps1
InvocationName : Invoke-Build
PipelineLength : 0
PipelinePosition : 0
ExpectingInput : False
CommandOrigin : Internal
DisplayScriptPosition :
ParameterValues : {}
according to the stach, it fails here:
at <ScriptBlock><DynamicParam>, ...\InvokeBuild\5.8.5\Invoke-Build.ps1: line 61
line 61 is:
DP = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
How is it related to the issue?
What is the full error message, with the location included?
BTW BEGIN and PROCESS are redundant in IB scripts. They only make sense when scripts are designed for pipeline input, not the case for IB build scripts.
I'm guessing the original author's intent was being able to "run the build" without calling Invoke-Build
for some reason.
FYI it looks like to be based on this:
https://github.com/gaelcolas/Sampler/blob/main/Sampler/Templates/Build/build.ps1
What is the full error message with location info included?
Is how it's reported:
C:\Users\user\projects\corp-dsc-build\output\RequiredModules\InvokeBuild\5.8.5\Invoke-Build.ps1 : Cannot retrieve the dynamic parameters for the cmdlet. System error.
At C:\Users\user\projects\corp-dsc-build\build.ps1:247 char:13
+ Invoke-Build @PSBoundParameters -Task $Tasks -File $MyInv ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Invoke-Build.ps1], ParameterBindingException
+ FullyQualifiedErrorId : GetDynamicParametersException,Invoke-Build.ps1
That gives the location in which the exception was returned to the calling script, hence including the ScriptStackTrace
if there's something more you think I can tease out, please feel to advise!
Dump this after error, it often shows all and actual culprit errors:
ps> $Error
NB So far from what I can see, nothing suggests it's the issue you described originally.
I agree entirely that the exception is thrown here:
Line 61: DP = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
I can see with the debugger that this is the last line executed before the The variable '$_' cannot be retrieved because it has not been set.
exception is thrown.
I would agree that doesn't not make a great deal of sense right now.
Adding either:
Set-StrictMode -Off
or amending IB as per the PR stopped it from occurring. Turn strict mode back on or revert the change from the PR and it re-occurs.
To further confuse the situation,
I've been running this simultaneously from VS Code and from the PowerShell CLI.
I've seen the problem in both environments, reproduced in the exact same way.
I've killed and restarted VSCode. And started a new PowerShell CLI session.
That put me in a situation where I can reproduce with VSCode, but not with the CLI.
I killed and restarted the PS Terminal within VS Code, in order to get a clean "dump" of $Error and now I can't reproduce.
I now can't reproduce it with the test function I wrote, even though I tested that and reproduced the same exception in a clean Powershell CLI session.
Tricky... I'll keep thinking.
Another tip: Push-Location
and Pop-Location
are redundant because IB takes care of restoring the current location.
All in all, try to remove bit by bit from the script to get the minimal reproducible variant (even not stable).
And Happy New Year once again! And happy building, too :)
Before winding up in the situation where I could not re-produce the problem, I did put the call to Invoke-Build
into a try...catch and wrote $error to a file:
error.txt
At C:\Users\user\projects\corp-dsc-build\output\RequiredModules\InvokeBuild\5.8.5\Invoke-Build.ps1:63 char:6
+ P = $_
^^ Not line 61 !!!
I'm happy to close this and re-raise if and when I manage to more reliably reproduce the problem.
This is maddening. Not sure I trust/believe anything any more.
Another tip: Push-Location and Pop-Location are redundant because IB takes care of restoring the current location.
Thank you.
I'm planning to rip those out. This was over 1000 lines of code at the start :/
^^ Not line 61 !!!
Good, finally. But how is it possible? This should not be possible per the code we have in IB.
Do you have anything else running, say, in parallel and potentially killing $_
somewhere in the middle?
I'm hoping as a carry on re-factoring this build script I'll break it again in the same way.
Fankly I'm baffled right now.
Reproduced
But no idea what gave rise to it.
I'm not re-opening the issue until I have more concrete information/evidence as to what is happening.
The variable '$_' cannot be retrieved because it has not been set.
C:\Users\user\projects\corp-dsc-build\output\RequiredModules\InvokeBuild\5.8.5\Invoke-Build.ps1 : The variable '$_' cannot be retrieved because it has not been set.
At C:\Users\user\projects\corp-dsc-build\build.ps1:192 char:5
+ Invoke-Build -Task $Tasks -File $MyInvocation.MyCommand.Path
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Invoke-Build.ps1], Exception
+ FullyQualifiedErrorId : Invoke-Build.ps1
The variable '$_' cannot be retrieved because it has not been set.
At C:\Users\user\projects\corp-dsc-build\output\RequiredModules\InvokeBuild\5.8.5\Invoke-Build.ps1:63 char:6
+ P = $_
+ ~~
+ CategoryInfo : InvalidOperation: (_:String) [], RuntimeException
+ FullyQualifiedErrorId : VariableIsUndefined
@uw-dc I am going to make the change similar to your suggested. I still cannot reproduce. But I believe you, i.e. in some environments use of $_
may introduce potential strict mode issues.
"Fixed" in v5.8.8