-
-
Notifications
You must be signed in to change notification settings - Fork 59
DEV Internal Functions
Use of variables $p
, $c
, $r
, $a
, all removed later.
The variable $p
(parent) is used in:
$p = if ($_ = $PSCmdlet.SessionState.PSVariable.Get('*')) {if ($_.Description -eq 'IB') {$_.Value}}
Originally we used $_
. But -- 190.
So we changed it to $p
, hopefully to work around some weird issues.
Then $p
is used in processing script parameters.
The variables $c
(common parameters), $r
(reserved parameters), $a
(parameter attribute) are
initialized first $c, $r, $a = $null
, so that they can be safely removed later.
Then they are used in dynamic parameters, not all always, so we initialize them.
Get common parameters as Cmdlet.CommonParameters. The issue is reported for PS 7.4 daily. My current version is PS 7.3.3, it has Cmdlet.CommonParameters, so 7.3.3 may be used:
if ($PSVersionTable.PSVersion -ge ([version]'7.3.3')) {
$c = @([System.Management.Automation.Cmdlet]::CommonParameters)
}
Yet for now we continue using hardcoded values with the new ProgressAction
added.
Issues /152 /217, see /Tests/Issues
We now shift script positional parameters positions (+2, avoiding conflicts with Task
and File
).
In PS v2 this does not change parameter positions (and the problem does not exists perhaps).
In later PS parameter positions keep changing +2, looks like PS caches attributes.
But this works because this shift still preserves parameter relative positions.
function *SL($P=$BuildRoot)
$P - new location, literal path
It sets the current location to specified literal path.
This command is frequently called with the omitted argument in order to set the location to the build root. This is done before running user code and resolving user input paths.
function *Path($P)
$P - full or relative path
It converts the specified path to full depending on the current PowerShell location. Without arguments it gets the current location.
function *Die($M, $C=0)
$M - error message, [object] converted to [string] as "$M"
$C - error category, [object] converted to [System.Management.Automation.ErrorCategory]
The following categories are used:
0 NotSpecified
5 InvalidArgument
6 InvalidData
7 InvalidOperation
8 InvalidResult
13 ObjectNotFound
*Die
is used in public functions designed for calls from scripts.
Unlike throw
, it throws an error which points to the caller.
It also sets some error category.
Important: *Die
has no cmdlet binding but its callers, i.e. engine functions, must have.
*Fin
(end, fail internally) is used for the internal failures.
For example, error in *IO
which is called from *Task
, not from a script directly.
Important: *Fin
uses cmdlet binding. Callers have no such restrictions.
Avoid raw throw
It looks like throw
includes the error message in CategoryInfo
and FullyQualifiedErrorId
.
In category CategoryInfo
it may be truncated. In v5 this may be done with ellipses in the middle.
Long and especially multi-line messages result in ugly formatting.
NB:
In the main try-block, use *Fin
because *Die
does not trigger the main catch
, see
PowerShellTraps/ThrowTerminatingError.
function *Run($_)
$_ - command to run
$args - remaining arguments are command arguments
It invokes commands with the current location set to the build root.
Commands are normally some user code called on various build blocks.
If $_
is null the command is ignored.
The parameter $_
is the only extra variable exposed to user code.
As documented, we do not hide $_
.
function *At($I)
$I - anything with the property InvocationInfo
For an object with the property InvocationInfo
(a task or an error record),
the function returns its amended position message like
At <file>:<line> char <char>
+ ...
Why Trim()
In PS v2 InvocationInfo.PositionMessage
has the unwanted leading \n
and the
line separator \n
instead of \r\n
. This is fixed in v3. But for v2 we keep
Trim()
in order to remove the leading \n
.
function *Msg($M, $I)
$M - error message
$I - anything with the property InvocationInfo
It joins an error message and its invocation info position message.
We used here-string to avoid x-plat dependent line ends.
Now we just use \n
because:
(1) The here-string approach is not reliable.
(2) These strings end up in Write-Build
which splits into lines anyway.
function *Check($J, $T, $P=@())
$J - jobs to check
$T - parent task, omitted for root jobs
$P - parent tasks for cycle checks, omitted for root jobs
Plot
for each string job $_
try to find the task by name $_
if it is missing
*Die "Missing task", mind messages depending on $T
if parent array contains the task
*Die "Cyclic reference"
call *Check with the task jobs, the task, parents with this task added
function *Err($T)
$T - failed task
$_ - current error in the parent catch block
It adds the current error $_
info to Result.Errors
.
v4.1.0 It also prints the error info, #106. It is called as soon as an error happens (simple and robust in all scenarios). We used to delay some errors in order to avoid kind of "duplicated information" (complex and not good with some hosts and redirection).
It is mostly called from *Task
(condition, IO, action).
And it is called once in the main catch if no errors exist.
It means tasks are not failed so it is a preprocessing error.
function *My
$_ - error in the parent scope
It returns true if the current error $_
is thrown by the engine itself and
false if the error is external, i.e. thrown by user code.
When called from *Task
:
- True: log an error message with no source, inner source is useless
- False: log a message and the useful external source
When called from the main catch
block:
- True: throw a new error, source will point to the caller
- False: re-throw preserving the external source
function *Job($J)
$J - job object
It validates a job object from -Jobs
, -After
, -Before
and returns the
resolved job (task reference or action script) and the optional job data.
Simple jobs are returned as they are.
References ?<TaskName>
are returned as @("<TaskName>", 1)
.
The command throws "Invalid job" if $J
is invalid.
Valid jobs:
-
[string] "TaskName"
- normal task reference -
[string] "?TaskName"
- safe task reference -
[scriptblock]
- action
# $N - task name
# $J - jobs to be checked
function *Unsafe($N, $J) {
# if jobs contain the name then the task is referenced unsafe, return 1
if ($J -contains $N) {return 1}
# walk task trees where roots are the jobs
foreach($_ in $J) {
# unwrap the job (e.g. ?SomeName), $r is a task name or a block
$r, $null = *Job $_
#! It is tempting to get $safe instead of the above $null and continue if it is true.
#! Do not! Yes, the build is not going to stop but parents of a failed task with unsafe
#! references still should fail. So step into and walk the safe branch anyway.
#! (this is covered by tests, fact 17-11-24)
# $r -ne $N -- skip the input, i.e. already failed task
# $t = ${*}.All[$r] -- get the task object by name, skip null ~ $r is block -- ineffective
# $t.If -- skip the task with the false condition, it is not going to run
# *Unsafe $N $t.Jobs -- test the input name with jobs of the task
if ($r -ne $N -and ($t = ${*}.All[$r]) -and $t.If -and (*Unsafe $N $t.Jobs)) {
return 1
}
}
}
It checks jobs $J
with the task $N
and returns:
- 1: an unsafe reference is found (build should stop)
- nothing: all references are safe (build may continue)
The first time it is called with root tasks $BuildTask
as $J
:
if (*Unsafe ${*j} $BuildTask) {throw}
The code is probably not optimal for builds with failing tasks. But it is compact and this is better for scenarios without failures.
function *Amend($X, $J, $B)
$X -- an extra task to be added to tasks specified by $J
$J | $X.Before | $X.After tasks
$B | 1 -- Before | 0 -- After
It is called on preprocessing for tasks with parameters Before
and After
.
NB: v5.0.0 $J
is [string[]]
(from -After
and -Before
), so *Job
cannot fail.
filter *Help($H)
$_ - input task object
$H - cache used by *Synopsis, a caller just passes @{}
For an input task it gets a task help object with the properties
Name
, Jobs
, and Synopsis
. It is called in the special case:
Invoke-Build ?
Why
$r = 1 | Select-Object Name, Jobs, Synopsis
Because in PS v2.0 New-Object PSCustomObject -Property @{...}
results in
unwanted order: Name
, Synopsis
, Jobs
.
Jobs
is an array, not text (v2.9.7)
- Pros
- With many jobs the column
Synopsis
is not dropped (try "?" with Tests/.build.ps1). - Jobs are easier to process or show differently by external tools.
- With many jobs the column
- Cons
- Only a few jobs are shown by default (
$FormatEnumerationLimit = 4
). On the other hand, with many jobs text would be truncated as well.
- Only a few jobs are shown by default (
function *Root($A)
$A - task dictionary
It is used on Invoke-Build *
in order to filter out tasks with parents and
return the root task names, to be invoked later. It also calls *Check
.
function *IO
It evaluates inputs and outputs of the current incremental and partial incremental task and returns a reason to skip, if all is up-to-date.
It is always called from *Task
, so that it does not have any parameters,
it just uses $Task
, the parent variable of the current task.
The function returns an array of two values:
- `[0]`: result code: `2`: output is up-to-date; `$null`: output is out-of-date
- `[1]`: information message
In the out-of-date case, the function stores processed input and outputs
as $Task.Inputs
and $Task.Outputs
. They are used in *Task
later.
Plot, mind *SL
before *Path
and calling user code:
${*i} = $Task.Inputs ~ invoke if a scriptblock. NB for PS v2 use @(& ..)
*SL and collect ${*p} = full input paths and ${*i} = input converted to FileInfo
if nothing then return (2, 'Skipping empty input.')
NB: *p is array, *i is array or not
if ($Task.Partial) {
*SL is not needed, it is done above and no user code is invoked after
${*o} = $Task.Outputs either invoked (with *SL after this) or cast to array
if *o and *p counts are different -> 'Different input and output'
No user code after this, we can use simple variables.
}
else {
*SL is not needed, it is done above and no user code is invoked after
invoke output (if a scriptblock), set it back to $Task.Outputs, *SL
set $Task.Inputs to *p (full input paths)
}
NB:
Replaced Get-Item -LiteralPath .. -Force -ErrorAction Stop
with [System.IO.FileInfo](*Path ..)
.
The old does not work in paths with [ ]
.
NB:
At some point replaced Test-Path -LiteralPath
with *Path
as well due to paths with [ ]
.
Later on replaced with times compared with 1601-01-01, see below.
NB: checks for Exists
#69
At some point redundant checks for `Exists` on output files were removed.
It is documented that time of a missing file is 1601-01-01.
v3.3.9 restores checks because Mono is different.
function *Task
$args[0] - task name
$args[1] - parent task path or null for a root task
This function is the engine's heart, it invokes tasks.
Obtain the task object by name from the build list. Do not check for null, it should exist. Create this task path from the parent path, it is for the log.
If the task is done then log and return. Before 2.14.4 we used to check for errors before this and print a different message. It's unlikely very useful.
Evaluate If
.
If it is false then return.
If it fails then catch and store the error + decorate the task as invoked, then re-throw.
Q: To fail or not if it is a safe call?
We think failures in If
should be fatal.
Process task jobs in a try
block.
CASE: Job is a task reference
Call the referenced task and go to the next job.
CASE: Job is a script block
Input of incremental tasks can be empty, so check for null. Use case: a task works for some files but it is possible that they do not exist.
# *i[0] is a flag which tells how to deal with *IO
${private:*i} = , [int]($null -ne $Task.Inputs)
# initial values:
0: not incremental task
1: incremental, *IO was not called, call it and print the result info *i[1]
# new values after called *IO
2: incremental, *IO was called, skip incremental job
$null: incremental, *IO was called, do incremental job
Variables $Task
and $Job
*Task
creates the variable $Task
. It is exposed to blocks and actions, it
is used by *Task
and other engine functions. Some blocks are dot-sourced in
*Task
. Thus, user code may change $Task
and break normal processing. That
is why $Task
is made constant.
Similarly, the read only variable $Job
is created for each task action.
It is used by the engine itself and may be used by user scripts (#185).
It may not handle all text anomalies or weird formatting as one may expect. But it works very well with typical cases and reasonable anomalies.
The first line (code after {
, if any) is preserved, so its indent is irrelevant.
Cases:
exec { first line }
exec { first line
...
}
Thus, the common indent is inferred from the next not empty line.
This function is documented together with the public Write-Build
.
- Concepts
- Script Tutorial
- Incremental Tasks
- Partial Incremental Tasks
- How Build Works
- Special Variables
- Build Failures
- Build Analysis
- Parallel Builds
- Persistent Builds
- Portable Build Scripts
- Using for Test Automation
- Debugging Tips
- VSCode Tips
Helpers
- Invoke Task from VSCode
- Generate VSCode Tasks
- Invoke Task from ISE
- Resolve MSBuild
- Show Build Trees
- Show Build Graph
- Argument Completers
- Invoke-Build.template
Appendix