databricks-cli/python/runner.go

148 lines
3.4 KiB
Go

package python
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"runtime"
"strings"
)
func PyInline(ctx context.Context, inlinePy string) (string, error) {
return Py(ctx, "-c", TrimLeadingWhitespace(inlinePy))
}
func Py(ctx context.Context, script string, args ...string) (string, error) {
py, err := DetectExecutable(ctx)
if err != nil {
return "", err
}
out, err := execAndPassErr(ctx, py, append([]string{script}, args...)...)
if err != nil {
// current error message chain is longer:
// failed to call {pyExec} __non_existing__.py: {pyExec}: can't open
// ... file '{pwd}/__non_existing__.py': [Errno 2] No such file or directory"
// probably we'll need to make it shorter:
// can't open file '$PWD/__non_existing__.py': [Errno 2] No such file or directory
return "", err
}
return trimmedS(out), nil
}
func createVirtualEnv(ctx context.Context) error {
_, err := Py(context.Background(), "-m", "venv", ".venv")
return err
}
// python3 -m build -w
// https://packaging.python.org/en/latest/tutorials/packaging-projects/
func detectVirtualEnv() (string, error) {
wd, err := os.Getwd()
if err != nil {
return "", err
}
wdf, err := os.Open(wd)
if err != nil {
return "", err
}
files, err := wdf.ReadDir(0)
if err != nil {
return "", err
}
for _, v := range files {
if !v.IsDir() {
continue
}
candidate := fmt.Sprintf("%s/%s", wd, v.Name())
_, err = os.Stat(fmt.Sprintf("%s/pyvenv.cfg", candidate))
if errors.Is(err, os.ErrNotExist) {
continue
}
if err != nil {
return "", err
}
return candidate, nil
}
return "", nil
}
var pyExec string
func DetectExecutable(ctx context.Context) (string, error) {
if pyExec != "" {
return pyExec, nil
}
detector := "which"
if runtime.GOOS == "windows" {
detector = "where.exe"
}
out, err := execAndPassErr(ctx, detector, "python3")
if err != nil {
return "", err
}
pyExec = getFirstMatch(string(out))
return pyExec, nil
}
func execAndPassErr(ctx context.Context, name string, args ...string) ([]byte, error) {
// TODO: move out to a separate package, once we have Maven integration
out, err := exec.CommandContext(ctx, name, args...).Output()
return out, nicerErr(err)
}
func getFirstMatch(out string) string {
res := strings.Split(out, "\n")
return strings.Trim(res[0], "\n\r")
}
func nicerErr(err error) error {
if err == nil {
return nil
}
if ee, ok := err.(*exec.ExitError); ok {
errMsg := trimmedS(ee.Stderr)
if errMsg == "" {
errMsg = err.Error()
}
return errors.New(errMsg)
}
return err
}
func trimmedS(bytes []byte) string {
return strings.Trim(string(bytes), "\n\r")
}
// TrimLeadingWhitespace removes leading whitespace
// function copied from Databricks Terraform provider
func TrimLeadingWhitespace(commandStr string) (newCommand string) {
lines := strings.Split(strings.ReplaceAll(commandStr, "\t", " "), "\n")
leadingWhitespace := 1<<31 - 1
for _, line := range lines {
for pos, char := range line {
if char == ' ' || char == '\t' {
continue
}
// first non-whitespace character
if pos < leadingWhitespace {
leadingWhitespace = pos
}
// is not needed further
break
}
}
for i := 0; i < len(lines); i++ {
if lines[i] == "" || strings.Trim(lines[i], " \t") == "" {
continue
}
if len(lines[i]) < leadingWhitespace {
newCommand += lines[i] + "\n" // or not..
} else {
newCommand += lines[i][leadingWhitespace:] + "\n"
}
}
return
}