New-VSCodeTask.ps1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167

<#PSScriptInfo
.VERSION 1.0.0
.AUTHOR Roman Kuzmin
.COPYRIGHT (c) 2011-2016 Roman Kuzmin
.TAGS Invoke, Task, Invoke-Build, VSCode
.GUID b8b2b532-28f6-443a-b0b1-079a66dd4ce3
.LICENSEURI http://www.apache.org/licenses/LICENSE-2.0
.PROJECTURI https://github.com/nightroman/Invoke-Build
#>


<#
.Synopsis
    Makes VSCode tasks from Invoke-Build tasks
 
.Description
    The script makes VSCode tasks from Invoke-Build tasks. It creates tasks.cmd
    and tasks.json in .\.vscode. The existing files are overridden. Change to
    the VSCode workspace directory before invoking the script.
 
    Do not edit tasks.json directly. Edit the build script instead. When you
    add, remove, rename, reorder tasks, change tags or Invoke-Build location
    then regenerate VSCode tasks.
 
    The default task becomes a so called VSCode build task (Ctrl-Shift-B).
    The default task is '.' if it exists, otherwise it is the first task.
 
    In order to invoke another task from VSCode use Ctrl-P and type 'task'.
    Then type a task name or select it from the opened list of all tasks.
 
    Only tasks with certain names are included. They contain alphanumeric
    characters, '_', '.', and '-', with the first character other than '-'.
 
    In order to invoke some tasks in a console host outside VSCode specify the
    tag #ConsoleHost in a comment preceding the task definition. Note that all
    parent tasks in the task trees get this tag automatically.
 
.Parameter BuildFile
        Specifies the build script path, absolute or relative. By default it is
        the standard default script in the current location, i.e. the workspace
        root.
.Parameter InvokeBuild
        Specifies the Invoke-Build.ps1 path, absolute or relative. If it is not
        specified then the script tries to find it in the workspace directory
        recursively. If it is not found then 'Invoke-Build.ps1' is used, i.e.
        the script is expected to be in the path.
 
.Example
    New-VSCodeTask
    This command binds to the default build script in the workspace root and
    Invoke-Build.ps1 either in the workspace root or subdirectory or in the
    path.
 
.Example
    New-VSCodeTask .\Scripts\Build.ps1 .\Tools\Invoke-Build\Invoke-Build.ps1
    This command binds to the relative build and engine script paths. The
    second argument may be omitted, Invoke-Build.ps1 will be discovered
    (it is needed if there are several versions of it for some reason).
#>


[CmdletBinding()]
param(
    [string]$BuildFile,
    [string]$InvokeBuild
)

function Add-Text($Text) {$null = $out.Append($Text)}
function Add-Line($Text) {$null = $out.AppendLine($Text)}

$Comments = @{}
function Test-ConsoleHost($Task) {
    $info = $Task.InvocationInfo
    $file = $info.ScriptName
    if (!($data = $Comments[$file])) {
        $Comments[$file] = $data = @{}
        foreach($_ in [System.Management.Automation.PSParser]::Tokenize((Get-Content -LiteralPath $file), [ref]$null)) {
            if ($_.Type -eq 'Comment') {$data[$_.EndLine] = $_.Content}
        }
    }
    for($n = $info.ScriptLineNumber; --$n -ge 1 -and ($c = $data[$n])) {
        if ($c -match '#ConsoleHost\b') {
            return 1
        }
    }
    foreach($j in $Task.Jobs) {if ($j -is [string]) {
        if (Test-ConsoleHost $all[$j]) {
            return 1
        }
    }}
}

### main

trap {$PSCmdlet.ThrowTerminatingError($_)}
$ErrorActionPreference = 'Stop'

# resolve missing Invoke-Build.ps1
if (!$InvokeBuild) {
    $InvokeBuild2 = @(Get-ChildItem . -Name -Recurse -Include Invoke-Build.ps1)
    $InvokeBuild = if ($InvokeBuild2) {".\$($InvokeBuild2[0])"} else {'Invoke-Build.ps1'}
}

# get all tasks and the default task
$all = & $InvokeBuild ?? -File $BuildFile
$dot = if ($all['.']) {'.'} else {$all.Item(0).Name}

# ensure .vscode
if (!(Test-Path .vscode)) {
    $null = mkdir .vscode
}

### tasks.cmd

$InvokeBuild2 = $InvokeBuild.Replace("'", "''")
$BuildFile2 = if ($BuildFile) {" -File '{0}'" -f $BuildFile.Replace("'", "''")}

$out = @'
@rem Do not edit! This file is generated by New-VSCodeTask.ps1
@echo off
if "%1" == "!" goto start
chcp 65001 > nul
PowerShell.exe -NoProfile -ExecutionPolicy Bypass "& '{0}'{1} %1"
exit
:start
shift
start PowerShell.exe -NoExit -NoProfile -ExecutionPolicy Bypass "& '{0}'{1} %1"
'@
 -f $InvokeBuild2, $BuildFile2

Set-Content .\.vscode\tasks.cmd $out

### tasks.json

$out = New-Object System.Text.StringBuilder
Add-Line '// Do not edit! This file is generated by New-VSCodeTask.ps1'
Add-Line '// Modify the build script instead and regenerate this file.'
Add-Line '{'
Add-Line ' "version": "0.1.0",'
Add-Line ' "command": ".\\.vscode\\tasks.cmd",'
Add-Line ' "suppressTaskName": false,'
Add-Line ' "showOutput": "always",'
Add-Line ' "tasks": ['

foreach($task in $all.Values) {
    $name = $task.Name
    if ($name -match '[^\w\.\-]|^-') {
        continue
    }
    Add-Line ' {'
    if ($name -eq $dot) {
        Add-Line ' "isBuildCommand": true,'
    }
    if (Test-ConsoleHost $task) {
        Add-Line ' "suppressTaskName": true,'
        Add-Line (' "args": ["!", "{0}"],' -f $name)
    }
    Add-Line (' "taskName": "{0}"' -f $name)
    Add-Line ' },'
}

Add-Line ' {'
Add-Line ' "taskName": "?"'
Add-Line ' }'
Add-Line ' ]'
Add-Text '}'

Set-Content .\.vscode\tasks.json $out.ToString() -Encoding UTF8