shells/powershell.go (617 lines of code) (raw):
package shells
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"gitlab.com/gitlab-org/gitlab-runner/common"
"gitlab.com/gitlab-org/gitlab-runner/helpers"
"gitlab.com/gitlab-org/gitlab-runner/helpers/featureflags"
"golang.org/x/text/encoding/unicode"
)
const (
dockerWindowsExecutor = "docker-windows"
SNPwsh = "pwsh"
SNPowershell = "powershell"
// When the shell is set to 'powershell', the UTF8 BOM character is prepended to the initialization script, which causes unmarshalling to fail.
// To prevent this, we add the 'echo ""' command.
// We also introduce the variable '$script_path' to extract the script name without extension from '$PSCommandPath'.
pwshJSONInitializationScript = `$script_path= %s -command "(Get-Item $PSCommandPath).BaseName"
$start_json= '{"script": "' + $script_path + '"}'
echo ""
echo "$start_json"
`
// Before executing a script, powershell parses it.
// A `ParserError` can then be thrown if a parsing error is found.
// Those errors are not catched by the powershell_trap_script thus causing the job to hang
// To avoid this problem, the PwshValidationScript is used to validate the given script and eventually to cause
// the job to fail if a `ParserError` is thrown
// As $Path already refers to the script being executed, the script name will be extracted from there in this context
pwshJSONTerminationScript = `
param (
[Parameter(Mandatory=$true,Position=1)]
[string]$Path
)
%[1]s -File $Path; $command_exit_code = $LASTEXITCODE
$script_path= %[1]s -command "(Get-Item $Path).BaseName"
$out_json= '{"command_exit_code": ' + $command_exit_code + ', "script": "' + $script_path + '"}'
echo ""
echo "$out_json"
Exit 0
`
// This script expected the PID of the process which must be terminated with its children
// It has been designed this way to handle both Kubernetes and Shell executor
// For Kubernetes executor, the PID is retrieved through a command
// For Shell executor, the process ID as it is already known
powershellStageProcessesKillerScript = `
function List-Children ($ProcessId) {
$children = Get-CIMInstance Win32_Process | Where-Object { $_.ParentProcessId -eq $ProcessId }
foreach ($child in $children) {
List-Children $child.ProcessId
If($child.ProcessId) { Stop-Process -Id $child.ProcessId; }
}
};
$processId=%s; List-Children $processId
`
)
type powershellChangeUserError struct {
shell string
executor string
}
func (p *powershellChangeUserError) Error() string {
return fmt.Sprintf("%s doesn't support changing user with the %s executor", p.shell, p.executor)
}
type PowerShell struct {
AbstractShell
Shell string
EOL string
}
type PsWriter struct {
bytes.Buffer
TemporaryPath string
indent int
Shell string
EOL string
PassFile bool
resolvePaths bool
useJSONInitializationTermination bool
}
func NewPsWriter(b *PowerShell, info common.ShellScriptInfo) *PsWriter {
return &PsWriter{
Shell: b.Shell,
EOL: b.EOL,
PassFile: b.passAsFile(info),
TemporaryPath: info.Build.TmpProjectDir(),
resolvePaths: info.Build.IsFeatureFlagOn(featureflags.UsePowershellPathResolver),
// useJSONInitializationTermination is only used for kubernetes executor when
// the feature flag FF_USE_LEGACY_KUBERNETES_EXECUTION_STRATEGY is set to false
useJSONInitializationTermination: info.Build.Runner.Executor == common.ExecutorKubernetes &&
!info.Build.IsFeatureFlagOn(featureflags.UseLegacyKubernetesExecutionStrategy),
}
}
func stdinCmdArgs(shell string, preCmds ...string) []string {
if shell == SNPwsh {
return pwshStdinCmdArgs(shell, preCmds...)
}
return powershellStdinCmdArgs(shell, preCmds...)
}
func pwshStdinCmdArgs(shell string, preCmds ...string) []string {
// The stdin script we pass is always UTF-8 encoded, however, depending on
// how powershell is configured, it may not be expecting UTF-8.
//
// To get around this issue, we pass an initialization script which sets
// the correct input and output encoding.
//
// The initialization script then calls '<shell> -Command -', so that our
// main script is executed by it being passed to stdin like usual.
//
// The initilization script itself is encoded so that it can be passed with
// -EncodeCommand, to avoid potential issues of passing script as an
// argument. Confusingly, -EncodeCommand expects our initialization script
// to be base64-encoded utf16.
//
// Note: the encoded script, depending on powershell configurations, can be
// limited to a certain length. The minimum maximum length is 8190. This
// encoded initialization script should be kept small.
var sb strings.Builder
for _, preCmd := range preCmds {
sb.WriteString(preCmd + "\r\n")
}
sb.WriteString("$OutputEncoding = [console]::InputEncoding = [console]::OutputEncoding = New-Object System.Text.UTF8Encoding\r\n")
sb.WriteString(shell + " -NoProfile -NonInteractive -Command -\r\n")
sb.WriteString("if(!$?) { Exit &{if($LASTEXITCODE) {$LASTEXITCODE} else {1}} }")
encoded, _ := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder().String(sb.String())
return append(
defaultPowershellFlags,
"-EncodedCommand",
base64.StdEncoding.EncodeToString([]byte(encoded)),
)
}
var defaultPowershellFlags = []string{
"-NoProfile",
"-NoLogo",
"-InputFormat",
"text",
"-OutputFormat",
"text",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
}
// Avoid using -EncodedCommand due to the powershell progress stream leaking to
// to the output: https://github.com/PowerShell/PowerShell/issues/5912.
func powershellStdinCmdArgs(shell string, preCmds ...string) []string {
script := "-"
if len(preCmds) > 0 {
script = ""
for _, preCmd := range preCmds {
script += preCmd + "; "
}
script += shell + " -NoProfile -Command -"
}
return append(
defaultPowershellFlags,
"-Command",
script,
)
}
func fileCmdArgs() []string {
return []string{"-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File"}
}
func PwshJSONTerminationScript(shell string) string {
return fmt.Sprintf(pwshJSONTerminationScript, shell)
}
func PowershellStageProcessesKillerScript(processId string) string {
return fmt.Sprintf(powershellStageProcessesKillerScript, processId)
}
func PowershellDockerCmd(shell string, preCmds ...string) []string {
return append([]string{shell}, stdinCmdArgs(shell, preCmds...)...)
}
func psReplaceSpecialChars(text string) string {
// taken from https://ss64.com/ps/syntax-esc.html
text = strings.ReplaceAll(text, "`", "``")
text = strings.ReplaceAll(text, "\a", "`a")
text = strings.ReplaceAll(text, "\b", "`b")
text = strings.ReplaceAll(text, "\f", "`f")
text = strings.ReplaceAll(text, "\r", "`r")
text = strings.ReplaceAll(text, "\n", "`n")
text = strings.ReplaceAll(text, "\t", "`t")
text = strings.ReplaceAll(text, "\v", "`v")
text = strings.ReplaceAll(text, "#", "`#")
text = strings.ReplaceAll(text, "'", "`'")
text = strings.ReplaceAll(text, "\"", "`\"")
return text
}
func psSingleQuote(text string) string {
return singleQuote(text)
}
// github.com/PowerShell/PowerShell/blob/v7.3.1/src/System.Management.Automation/engine/parser/CharTraits.cs#L276-L282
func psDoubleQuote(text string) string {
text = psReplaceSpecialChars(text)
text = strings.ReplaceAll(text, "“", "`“")
text = strings.ReplaceAll(text, "”", "`”")
text = strings.ReplaceAll(text, "„", "`„")
return doubleQuote(text)
}
func psQuoteVariable(text string) string {
text = psDoubleQuote(text)
text = strings.ReplaceAll(text, "$", "`$")
text = strings.ReplaceAll(text, "``e", "`e")
return text
}
func (p *PsWriter) GetTemporaryPath() string {
return p.TemporaryPath
}
func (p *PsWriter) Line(text string) {
p.WriteString(strings.Repeat(" ", p.indent) + text + p.EOL)
}
func (p *PsWriter) Linef(format string, arguments ...interface{}) {
p.Line(fmt.Sprintf(format, arguments...))
}
func (p *PsWriter) CheckForErrors() {
p.checkErrorLevel()
}
func (p *PsWriter) Indent() {
p.indent++
}
func (p *PsWriter) Unindent() {
p.indent--
}
func (p *PsWriter) checkErrorLevel() {
p.Line("if(!$?) { Exit &{if($LASTEXITCODE) {$LASTEXITCODE} else {1}} }")
p.Line("")
}
func (p *PsWriter) Command(command string, arguments ...string) {
p.Line(p.buildCommand(psSingleQuote, command, arguments...))
p.checkErrorLevel()
}
// SetupGitCredHelper is the powershell implementation of setting up the runner's default cred helper, which pulls out
// the job token from the environment.
// For different editions/versions of powershell, we need to pass args differently, however we only have the version
// information at runtime.
func (p *PsWriter) SetupGitCredHelper(confFile, section, user string) {
helperSection := psSingleQuote(section + ".helper")
userSection := psSingleQuote(section + ".username")
confFile = p.resolvePath(confFile)
clearCmd := fmt.Sprintf(`git config -f %s --replace-all %s`, confFile, helperSection)
setCmd := fmt.Sprintf(`git config -f %s --add %s`, confFile, helperSection)
userCmd := fmt.Sprintf(`git config -f %s %s %s`, confFile, userSection, psSingleQuote(user))
// Specialities re. "special arguments" to external commands (empty args, args with `"`, ...):
// - Starting from 7.3 we can pass on args without any special handling
// - Before 7.3 we need to handle "special arguments" explicitly, ie. ensure we escape special stuff
// - For 7.2.x it depends on the experimental feature PSNativeCommandArgumentPassing:
// - if it's not enabled, we have to handle "special arguments" explicitly
// - if it's enabled, we need to accept that; we can control exact behavior but we have to use it
// We run all that in a script block, so that setting PSNativeCommandArgumentPassing only effects this (and child)
// scopes.
p.Line(strings.Join([]string{
`& {`,
`$psVer = [Version]$PSVersionTable.PSVersion`,
`$needsSpecialArgQuoting = ($psVer -lt [Version]"7.3")`,
`if ( ($psVer -ge [Version]"7.2") -and ($psVer -lt [Version]"7.3") -and ((Get-ExperimentalFeature -Name PSNativeCommandArgumentPassing).Enabled) ) {`,
` $PSNativeCommandArgumentPassing = 'Standard'`,
` $needsSpecialArgQuoting = $False`,
`}`,
`if ($needsSpecialArgQuoting) {`,
clearCmd + " " + `'""'`,
setCmd + " " + psSingleQuote(strings.ReplaceAll(credHelperCommand, `"`, `\"`)),
`} else {`,
clearCmd + " " + `''`,
setCmd + " " + psSingleQuote(credHelperCommand),
`}`,
userCmd,
`}`,
}, p.EOL))
p.CheckForErrors()
}
func (p *PsWriter) CommandArgExpand(command string, arguments ...string) {
p.Line(p.buildCommand(psDoubleQuote, command, arguments...))
p.checkErrorLevel()
}
func (p *PsWriter) SectionStart(id, command string, options []string) {
p.Noticef("$ %s", command)
}
func (p *PsWriter) SectionEnd(id string) {}
func (p *PsWriter) buildCommand(quoter stringQuoter, command string, arguments ...string) string {
list := []string{
psDoubleQuote(command),
}
for _, argument := range arguments {
list = append(list, quoter(argument))
}
return "& " + strings.Join(list, " ")
}
func (p *PsWriter) resolvePath(path string) string {
if p.resolvePaths {
return fmt.Sprintf(
"$ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath(%s)", psDoubleQuote(path),
)
}
return psDoubleQuote(p.fromSlash(path))
}
func (p *PsWriter) TmpFile(name string) string {
if p.resolvePaths {
return p.Join(p.TemporaryPath, name)
}
return p.cleanPath(p.Join(p.TemporaryPath, name))
}
func (p *PsWriter) cleanPath(name string) string {
if p.resolvePaths {
return name
}
return p.fromSlash(p.Absolute(name))
}
func (p *PsWriter) fromSlash(path string) string {
if p.resolvePaths {
return path
}
if p.Shell == SNPwsh {
// pwsh wants OS slash style, not necessarily backslashes
return filepath.FromSlash(path)
}
return helpers.ToBackslash(path)
}
func (p *PsWriter) EnvVariableKey(name string) string {
return fmt.Sprintf("$%s", name)
}
func (p *PsWriter) isTmpFile(path string) bool {
return strings.HasPrefix(path, p.TemporaryPath)
}
func (p *PsWriter) Variable(variable common.JobVariable) {
if variable.File {
variableFile := p.TmpFile(variable.Key)
p.MkDir(p.TemporaryPath)
p.Linef(
"[System.IO.File]::WriteAllText(%s, %s)",
p.resolvePath(variableFile),
psQuoteVariable(variable.Value),
)
p.Linef("$%s=%s", variable.Key, p.resolvePath(variableFile))
} else {
if p.isTmpFile(variable.Value) {
variable.Value = p.cleanPath(variable.Value)
}
p.Linef("$%s=%s", variable.Key, psQuoteVariable(variable.Value))
}
p.Linef("$env:%s=$%s", variable.Key, variable.Key)
}
func (p *PsWriter) DotEnvVariables(baseFilename string, variables map[string]string) string {
p.MkDir(p.TemporaryPath)
dotEnvFile := p.TmpFile(baseFilename)
p.Linef("[System.IO.File]::WriteAllText(%s, @\"\n%s\n\"@)", p.resolvePath(dotEnvFile), helpers.DotEnvEscape(variables))
return dotEnvFile
}
func (p *PsWriter) SourceEnv(pathname string) {
p.MkDir(p.TemporaryPath)
pathname = p.resolvePath(pathname)
p.Linef("if(!(Test-Path %s)) { New-Item -ItemType file -Force %s | out-null }", pathname, pathname)
p.Linef("Try { Get-Content %s | ForEach { $k, $v = $_.split('='); Set-Content env:\\$k $v} } Catch {", pathname)
p.Indent()
p.Warningf("Unable to read env file: %s", pathname)
p.Unindent()
p.Line("}")
}
func (p *PsWriter) IfDirectory(path string) {
p.Linef("if(Test-Path %s -PathType Container) {", p.resolvePath(path))
p.Indent()
}
func (p *PsWriter) IfFile(path string) {
p.Linef("if(Test-Path %s -PathType Leaf) {", p.resolvePath(path))
p.Indent()
}
func (p *PsWriter) IfCmd(cmd string, arguments ...string) {
p.ifInTryCatch(p.buildCommand(psSingleQuote, cmd, arguments...) + " 2>$null")
}
func (p *PsWriter) IfCmdWithOutput(cmd string, arguments ...string) {
p.ifInTryCatch(p.buildCommand(psSingleQuote, cmd, arguments...))
}
func (p *PsWriter) IfGitVersionIsAtLeast(version string) {
p.Printf("Powershell does not support Git version detection")
p.Linef("if($false) {")
p.Indent()
}
func (p *PsWriter) ifInTryCatch(cmd string) {
p.Line("Set-Variable -Name cmdErr -Value $false")
p.Line("Try {")
p.Indent()
p.Line(cmd)
p.Line("if(!$?) { throw &{if($LASTEXITCODE) {$LASTEXITCODE} else {1}} }")
p.Unindent()
p.Line("} Catch {")
p.Indent()
p.Line("Set-Variable -Name cmdErr -Value $true")
p.Unindent()
p.Line("}")
p.Line("if(!$cmdErr) {")
p.Indent()
}
func (p *PsWriter) Else() {
p.Unindent()
p.Line("} else {")
p.Indent()
}
func (p *PsWriter) EndIf() {
p.Unindent()
p.Line("}")
}
func (p *PsWriter) Cd(path string) {
p.Line("cd " + p.resolvePath(path))
p.checkErrorLevel()
}
func (p *PsWriter) MkDir(path string) {
p.Linef("New-Item -ItemType directory -Force -Path %s | out-null", p.resolvePath(path))
}
func (p *PsWriter) MkTmpDir(name string) string {
dirPath := p.Join(p.TemporaryPath, name)
p.MkDir(dirPath)
return dirPath
}
func (p *PsWriter) RmDir(path string) {
path = p.resolvePath(path)
p.Linef(
"if( (Get-Command -Name Remove-Item2 -Module NTFSSecurity -ErrorAction SilentlyContinue) "+
"-and (Test-Path %s -PathType Container) ) {",
path,
)
p.Indent()
p.Line("Remove-Item2 -Force -Recurse " + path)
p.Unindent()
p.Linef("} elseif(Test-Path %s) {", path)
p.Indent()
p.Line("Remove-Item -Force -Recurse " + path)
p.Unindent()
p.Line("}")
p.Line("")
}
func (p *PsWriter) RmFile(path string) {
path = p.resolvePath(path)
p.Line(
"if( (Get-Command -Name Remove-Item2 -Module NTFSSecurity -ErrorAction SilentlyContinue) " +
"-and (Test-Path " + path + " -PathType Leaf) ) {")
p.Indent()
p.Line("Remove-Item2 -Force " + path)
p.Unindent()
p.Linef("} elseif(Test-Path %s) {", path)
p.Indent()
p.Line("Remove-Item -Force " + path)
p.Unindent()
p.Line("}")
p.Line("")
}
func (p *PsWriter) RmFilesRecursive(path string, name string) {
resolvedPath := p.resolvePath(path)
p.IfDirectory(path)
p.Linef(
// `Remove-Item -Recurse` has a known issue (see Example 4 in
// https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.management/remove-item)
"Get-ChildItem -Path %s -Filter %s -Recurse | ?{ -not $_.PSIsContainer } | ForEach-Object { Remove-Item -Force $_.FullName }",
resolvedPath, psQuoteVariable(name),
)
p.EndIf()
}
func (p *PsWriter) RmDirsRecursive(path string, name string) {
resolvedPath := p.resolvePath(path)
p.IfDirectory(path)
p.Linef(
// `Remove-Item -Recurse` has a known issue (see Example 4 in
// https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.management/remove-item)
"Get-ChildItem -Path %s -Filter %s -Recurse | ?{ $_.PSIsContainer } | ForEach-Object { Remove-Item -Recurse -Force $_.FullName }",
resolvedPath, psQuoteVariable(name),
)
p.EndIf()
}
func (p *PsWriter) Printf(format string, arguments ...interface{}) {
coloredText := helpers.ANSI_RESET + fmt.Sprintf(format, arguments...)
p.Line("echo " + psQuoteVariable(coloredText))
}
func (p *PsWriter) Noticef(format string, arguments ...interface{}) {
coloredText := helpers.ANSI_BOLD_GREEN + fmt.Sprintf(format, arguments...) + helpers.ANSI_RESET
p.Line("echo " + psQuoteVariable(coloredText))
}
func (p *PsWriter) Warningf(format string, arguments ...interface{}) {
coloredText := helpers.ANSI_YELLOW + fmt.Sprintf(format, arguments...) + helpers.ANSI_RESET
p.Line("echo " + psQuoteVariable(coloredText))
}
func (p *PsWriter) Errorf(format string, arguments ...interface{}) {
coloredText := helpers.ANSI_BOLD_RED + fmt.Sprintf(format, arguments...) + helpers.ANSI_RESET
p.Line("echo " + psQuoteVariable(coloredText))
}
func (p *PsWriter) EmptyLine() {
p.Line(`echo ""`)
}
func (p *PsWriter) Absolute(dir string) string {
if p.resolvePaths {
return dir
}
if filepath.IsAbs(dir) {
return dir
}
p.Linef("$CurrentDirectory = (Resolve-Path .%s).Path", string(os.PathSeparator))
return p.Join("$CurrentDirectory", dir)
}
func (p *PsWriter) Join(elem ...string) string {
if p.resolvePaths {
// We rely on the resolve function and always use forward slashes
// when joining paths.
return path.Join(elem...)
}
return filepath.Join(elem...)
}
func (p *PsWriter) Finish(trace bool) string {
var buf strings.Builder
if p.Shell == SNPwsh {
p.finishPwsh(&buf, trace)
} else {
p.finishPowerShell(&buf, trace)
}
return buf.String()
}
func (p *PsWriter) finishPwsh(buf *strings.Builder, trace bool) {
if p.EOL == "\n" {
buf.WriteString("#!/usr/bin/env " + SNPwsh + p.EOL)
}
// All pwsh scripts can and should be wrapped in a script block. Regardless whether they are passed
// as files or through stdin, this way the whole script will be executed as a block,
// this was suggested at https://github.com/PowerShell/PowerShell/issues/15331#issuecomment-1016942586.
// This also fixes things like https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/2715 and
// allows us to bypass file permissions when changing the current user.
buf.WriteString("& {" + p.EOL + p.EOL)
if p.useJSONInitializationTermination {
buf.WriteString(fmt.Sprintf(pwshJSONInitializationScript, p.Shell) + p.EOL + p.EOL)
}
if trace {
buf.WriteString("Set-PSDebug -Trace 2" + p.EOL)
}
buf.WriteString(`$ErrorActionPreference = "Stop"` + p.EOL)
buf.WriteString(p.String() + p.EOL)
buf.WriteString("}" + p.EOL + p.EOL)
}
func (p *PsWriter) finishPowerShell(buf *strings.Builder, trace bool) {
if p.PassFile {
// write UTF-8 BOM (Powershell Core doesn't use a BOM as mentioned in
// https://gitlab.com/gitlab-org/gitlab-runner/-/issues/3896#note_157830131)
buf.WriteString("\xef\xbb\xbf")
} else {
buf.WriteString("& {" + p.EOL + p.EOL)
}
if p.useJSONInitializationTermination {
buf.WriteString(fmt.Sprintf(pwshJSONInitializationScript, p.Shell) + p.EOL + p.EOL)
}
if trace {
buf.WriteString("Set-PSDebug -Trace 2" + p.EOL)
}
buf.WriteString(p.String() + p.EOL)
if !p.PassFile {
buf.WriteString("}" + p.EOL + p.EOL)
}
}
func (b *PowerShell) GetName() string {
return b.Shell
}
func (b *PowerShell) GetEntrypointCommand(_ common.ShellScriptInfo, probeFile string) []string {
preCmds := []string{}
if probeFile != "" {
preCmds = append(preCmds, fmt.Sprintf("Out-File -Force -FilePath '%s'", probeFile))
}
return PowershellDockerCmd(b.Shell, preCmds...)
}
func (b *PowerShell) GetConfiguration(info common.ShellScriptInfo) (*common.ShellConfiguration, error) {
script := &common.ShellConfiguration{
Command: b.Shell,
PassFile: b.passAsFile(info),
Extension: "ps1",
DockerCommand: PowershellDockerCmd(b.Shell),
}
if info.User != "" {
if script.PassFile {
return nil, &powershellChangeUserError{
shell: b.Shell,
executor: info.Build.Runner.Executor,
}
}
script.Command = "su"
if runtime.GOOS == OSLinux {
script.Arguments = append(script.Arguments, "-s", "/usr/bin/"+b.Shell)
}
script.Arguments = append(
script.Arguments,
info.User,
"-c",
b.Shell+" "+strings.Join(stdinCmdArgs(b.Shell), " "),
)
} else {
script.Arguments = b.scriptArgs(script)
}
script.CmdLine = strings.Join(append([]string{script.Command}, script.Arguments...), " ")
return script, nil
}
func (b *PowerShell) scriptArgs(script *common.ShellConfiguration) []string {
if script.PassFile {
return fileCmdArgs()
}
return stdinCmdArgs(b.Shell)
}
func (b *PowerShell) passAsFile(info common.ShellScriptInfo) bool {
// if DisablePowershellStdin is false, powershell is passed via stdin
if !info.Build.IsFeatureFlagOn(featureflags.DisablePowershellStdin) {
return false
}
// we only support powershell script by a file for shell & custom executors
switch info.Build.Runner.Executor {
case "shell", "custom":
return true
}
return false
}
func (b *PowerShell) GenerateScript(
ctx context.Context,
buildStage common.BuildStage,
info common.ShellScriptInfo,
) (string, error) {
w := NewPsWriter(b, info)
return b.generateScript(ctx, w, buildStage, info)
}
func (b *PowerShell) generateScript(
ctx context.Context,
w ShellWriter,
buildStage common.BuildStage,
info common.ShellScriptInfo,
) (string, error) {
b.ensurePrepareStageHostnameMessage(w, buildStage, info)
err := b.writeScript(ctx, w, buildStage, info)
if err != nil {
return "", err
}
script := w.Finish(info.Build.IsDebugTraceEnabled())
return script, nil
}
func (b *PowerShell) GenerateSaveScript(info common.ShellScriptInfo, scriptPath, script string) (string, error) {
w := NewPsWriter(b, info)
return b.generateSaveScript(w, info, scriptPath, script), nil
}
func (b *PowerShell) generateSaveScript(w *PsWriter, info common.ShellScriptInfo, scriptPath, script string) string {
var buf strings.Builder
w.Line(fmt.Sprintf(`$in =%s`, psQuoteVariable(base64.StdEncoding.EncodeToString([]byte(script)))))
w.Line("$customEncoding = New-Object System.Text.UTF8Encoding $True")
w.Line(fmt.Sprintf("$sw = [System.IO.StreamWriter]::new(\"%s\", $customEncoding)", scriptPath))
w.Line("$sw.Write([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($in)))")
w.Line("$sw.Flush()")
w.Line("$sw.Close()")
buf.WriteString("& {" + w.EOL + w.EOL)
if info.Build.IsDebugTraceEnabled() {
buf.WriteString("Set-PSDebug -Trace 2" + w.EOL)
}
buf.WriteString(w.String())
buf.WriteString(w.EOL + w.EOL + "}" + w.EOL + w.EOL)
return buf.String()
}
func (b *PowerShell) ensurePrepareStageHostnameMessage(
w ShellWriter,
buildStage common.BuildStage,
info common.ShellScriptInfo,
) {
if buildStage == common.BuildStagePrepare {
if info.Build.Hostname != "" {
w.Line(
fmt.Sprintf(
`echo "Running on $([Environment]::MachineName) via %s..."`,
psQuoteVariable(info.Build.Hostname),
),
)
} else {
w.Line(`echo "Running on $([Environment]::MachineName)..."`)
}
}
}
func (b *PowerShell) IsDefault() bool {
return runtime.GOOS == OSWindows
}
func init() {
eol := "\r\n"
if runtime.GOOS != OSWindows {
eol = "\n"
}
common.RegisterShell(WrapShell(&PowerShell{Shell: SNPwsh, EOL: eol}))
common.RegisterShell(WrapShell(&PowerShell{Shell: SNPowershell, EOL: "\r\n"}))
}