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"})) }