nightroman / Invoke-Build

Build Automation in PowerShell

Home Page:https://github.com/nightroman/Invoke-Build/wiki

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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:

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.

$_ is assigned in here:

$_ = if ($_ = $PSCmdlet.SessionState.PSVariable.Get('*')) {if ($_.Description -eq 'IB') {$_.Value}}

In this expression $_ is always assigned first, then read.

IB is tested with the Latest strict mode:

Set-StrictMode -Version Latest

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

winver

(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