When runner on Linux and using virtualbox for win,powershell.Absolute is wrong.
Summary
When host OS is linux, executor=virtualbox, VM geust OS is win, and use powershell,
the powershell.go:Absolute() handle the Windows style absolute path is wrong.
reference to this link for configuration
I use gitlab-runner on windows, using virtualbox for guest win.
set runners.builds_dir in config.toml, like so:
config.toml
[[runners]]
executor = "virtualbox"
builds_dir = "C:\\Users\\ci\\builds"
shell = "pwsh"
.gitlab-ci.yml
echo $CI_PROJECT_DIR
is work fine, the job log:
$ echo $CI_PROJECT_DIR
C:/Users/ci/builds/aozima/test_ci
Steps to reproduce
I want migrate the runner to linux.
Environment description
The environment
host system:
$ cat /etc/issue
Ubuntu 22.04.2 LTS
gitlab-runner vision:
$ gitlab-runner -v
Version: 16.1.0
Git revision: b72e108d
Git branch: 16-1-stable
GO version: go1.19.9
Built: 2023-06-21T21:52:30+0000
OS/Arch: linux/amd64
executor:
$ virtualbox --help
Oracle VM VirtualBox VM Selector v7.0.8
VM geust OS:
win10
At first, I didn't set runners.builds_dir in config.toml.
Get CI_PROJECT_DIR not absolute
$ echo $CI_PROJECT_DIR
builds/aozima/test_ci
Next, I set runners.builds_dir in config.toml, like before on win:
config.toml
[[runners]]
executor = "virtualbox"
builds_dir = "C:\\Users\\ci\\builds"
shell = "pwsh"
but failed to run, when I set variables CI_DEBUG_TRACE=true, the job log:
DEBUG: 69+ >>>> New-Item -ItemType directory -Force -Path "C:/Users/ci/builds/aozima/test_ci.tmp" | out-null
DEBUG: 70+ >>>> [System.IO.File]::WriteAllText("$CurrentDirectory/C:/Users/ci/builds/aozima/test_ci.tmp/CI_SERVER_TLS_CA_FILE", "-----BEGIN CERTIF*******==`n-----END CERTIFICATE-----")
ParentContainsErrorRecordException:
Line |
70 | [System.IO.File]::WriteAllText("$CurrentDirectory/C:/Users/ci/builds/ …
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Exception calling "WriteAllText" with "2" argument(s): "文件名、目录名或卷标语法不正确。 : 'C:\Users\ci\C:\Users\ci\builds\aozima\test_ci.tmp\CI_SERVER_TLS_CA_FILE'"
Cleaning up project directory and file based variables
00:02
DEBUG: 3+ >>>> $ErrorActionPreference = "Stop"
DEBUG: 4+ >>>> $CurrentDirectory = (Resolve-Path ./).Path
DEBUG: 5+ if( >>>> (Get-Command -Name Remove-Item2 -Module NTFSSecurity -ErrorAction SilentlyContinue) -and (Test-Path "$CurrentDirectory/C:/Users/ci/builds/aozima/test_ci.tmp/CI_SERVER_TLS_CA_FILE" -PathType Leaf) ) {
DEBUG: 7+ } elseif( >>>> Test-Path "$CurrentDirectory/C:/Users/ci/builds/aozima/test_ci.tmp/CI_SERVER_TLS_CA_FILE") {
DEBUG: 10+ >>>> }
ERROR: Job failed: Process exited with status 1
root case:
WriteAllText("$CurrentDirectory/C:/Users
Read the code, I think the bug happened at shells/powershell.go:Absolute(),
so I add some log for test
func (p *PsWriter) Absolute(dir string) string {
logrus.Infoln("shells/powershell.go Absolute: dir=[", dir, "].")
if p.resolvePaths {
logrus.Infoln("shells/powershell.go Absolute: [if p.resolvePaths == true]")
return dir
}
if filepath.IsAbs(dir) {
logrus.Infoln("shells/powershell.go Absolute: [if filepath.IsAbs(dir) == true]")
return dir
}
logrus.Infoln("shells/powershell.go Absolute: p.Linef=[", string(os.PathSeparator), "].")
logrus.Infoln("shells/powershell.go Absolute: p.Join=[", p.Join("$CurrentDirectory", dir), "].")
p.Linef("$CurrentDirectory = (Resolve-Path .%s).Path", string(os.PathSeparator))
return p.Join("$CurrentDirectory", dir)
}
get the runner logs:
shells/powershell.go Absolute: dir=[ C:/Users/ci/builds/aozima/test_ci.tmp/CI_SERVER_TLS_CA_FILE ].
shells/powershell.go Absolute: p.Linef=[ / ].
shells/powershell.go Absolute: p.Join=[ $CurrentDirectory/C:/Users/ci/builds/aozima/test_ci.tmp/CI_SERVER_TLS_CA_FILE ].
oops, filepath.IsAbs("C:/Users...") return false.
I wrote a examples of test
main.go
package main
import (
"fmt"
"runtime"
"path/filepath"
)
func main() {
fmt.Println("Hello World")
path := "C:/Users/ci/builds/aozima/test_ci.tmp/CI_SERVER_TLS_CA_FILE"
println(runtime.GOOS)
println(filepath.IsAbs(path)) // windows=true, linux=false
}
Test on windows and linux, get the diff result...
>.\test.exe
windows
true
$ ./test.elf
linux
false
Possible fixes
I read the go source code, the IsAbs implement for different platforms,
https://cs.opensource.google/go/go/+/refs/tags/go1.20.5:src/path/filepath/path_windows.go
My host OS is linux, but the guest OS is windows, problem happened... Because powershell only(confirm?) use for windows, so I patch for powershell.go
diff --git a/shells/powershell.go b/shells/powershell.go
index 12675d7d9..8645afebd 100644
--- a/shells/powershell.go
+++ b/shells/powershell.go
@@ -11,6 +11,7 @@ import (
"runtime"
"strings"
+ "github.com/sirupsen/logrus"
"gitlab.com/gitlab-org/gitlab-runner/common"
"gitlab.com/gitlab-org/gitlab-runner/helpers"
"gitlab.com/gitlab-org/gitlab-runner/helpers/featureflags"
@@ -446,15 +447,105 @@ func (p *PsWriter) EmptyLine() {
p.Line(`echo ""`)
}
+func isSlash(c uint8) bool {
+ return c == '\\' || c == '/'
+}
+
+func toUpper(c byte) byte {
+ if 'a' <= c && c <= 'z' {
+ return c - ('a' - 'A')
+ }
+ return c
+}
+
+// cutPath slices path around the first path separator.
+func cutPath(path string) (before, after string, found bool) {
+ for i := range path {
+ if isSlash(path[i]) {
+ return path[:i], path[i+1:], true
+ }
+ }
+ return path, "", false
+}
+
+// volumeNameLen returns length of the leading volume name on Windows.
+// It returns 0 elsewhere.
+//
+// See: https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
+func volumeNameLen(path string) int {
+ if len(path) < 2 {
+ return 0
+ }
+ // with drive letter
+ c := path[0]
+ if path[1] == ':' && ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') {
+ return 2
+ }
+ // UNC and DOS device paths start with two slashes.
+ if !isSlash(path[0]) || !isSlash(path[1]) {
+ return 0
+ }
+ rest := path[2:]
+ p1, rest, _ := cutPath(rest)
+ p2, rest, ok := cutPath(rest)
+ if !ok {
+ return len(path)
+ }
+ if p1 != "." && p1 != "?" {
+ // This is a UNC path: \\${HOST}\${SHARE}\
+ return len(path) - len(rest) - 1
+ }
+ // This is a DOS device path.
+ if len(p2) == 3 && toUpper(p2[0]) == 'U' && toUpper(p2[1]) == 'N' && toUpper(p2[2]) == 'C' {
+ // This is a DOS device path that links to a UNC: \\.\UNC\${HOST}\${SHARE}\
+ _, rest, _ = cutPath(rest) // host
+ _, rest, ok = cutPath(rest) // share
+ if !ok {
+ return len(path)
+ }
+ }
+ return len(path) - len(rest) - 1
+}
+
+// IsAbs reports whether the path is absolute.
+func IsAbs(path string) (b bool) {
+ l := volumeNameLen(path)
+ if l == 0 {
+ return false
+ }
+ // If the volume name starts with a double slash, this is an absolute path.
+ if isSlash(path[0]) && isSlash(path[1]) {
+ return true
+ }
+ path = path[l:]
+ if path == "" {
+ return false
+ }
+ return isSlash(path[0])
+}
+
func (p *PsWriter) Absolute(dir string) string {
+ logrus.Infoln("shells/powershell.go Absolute: dir=[", dir, "].")
if p.resolvePaths {
+ logrus.Infoln("shells/powershell.go Absolute: [if p.resolvePaths == true]")
return dir
}
if filepath.IsAbs(dir) {
+ logrus.Infoln("shells/powershell.go Absolute: [if filepath.IsAbs(dir) == true]")
return dir
}
+ if runtime.GOOS != "windows" {
+ if IsAbs(dir) {
+ logrus.Infoln("shells/powershell.go Absolute: [not windows, but path IsAbs]")
+ return dir
+ }
+ }
+
+ logrus.Infoln("shells/powershell.go Absolute: p.Linef=[", string(os.PathSeparator), "].")
+ logrus.Infoln("shells/powershell.go Absolute: p.Join=[", p.Join("$CurrentDirectory", dir), "].")
+
p.Linef("$CurrentDirectory = (Resolve-Path .%s).Path", string(os.PathSeparator))
return p.Join("$CurrentDirectory", dir)
}
It't works!! the new log
shells/powershell.go Absolute: dir=[ C:/Users/ci/builds/aozima/test_ci.tmp/CI_SERVER_TLS_CA_FILE ].
shells/powershell.go Absolute: [if filepath.IsAbs(dir) == true]
shells/powershell.go Absolute: dir=[ C:/Users/ci/builds/aozima/test_ci.tmp ].
Is here has better solution?