diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 71fec803b4..db0efc7d8f 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -7,6 +7,7 @@ ### Dependency updates ### CLI +* Use OS aware runner instead of bash for run-local command ([#2996](https://github.com/databricks/cli/pull/2996)) ### Bundles * Fix "bundle summary -o json" to render null values properly ([#2990](https://github.com/databricks/cli/pull/2990)) diff --git a/acceptance/cmd/workspace/apps/run-local/app/requirements.txt b/acceptance/cmd/workspace/apps/run-local/app/requirements.txt new file mode 100644 index 0000000000..aa4b1291cf --- /dev/null +++ b/acceptance/cmd/workspace/apps/run-local/app/requirements.txt @@ -0,0 +1 @@ +Flask==3.1.1 diff --git a/cmd/workspace/apps/run_local.go b/cmd/workspace/apps/run_local.go index cfc4ac8fb5..adc200ef3c 100644 --- a/cmd/workspace/apps/run_local.go +++ b/cmd/workspace/apps/run_local.go @@ -53,7 +53,7 @@ func setupWorkspaceAndConfig(cmd *cobra.Command, entryPoint string, appPort int) func setupApp(cmd *cobra.Command, config *apps.Config, spec *apps.AppSpec, customEnv []string, prepareEnvironment bool) (apps.App, []string, error) { ctx := cmd.Context() cfg := cmdctx.ConfigUsed(ctx) - app := apps.NewApp(config, spec) + app := apps.NewApp(ctx, config, spec) env := auth.ProcessEnv(cfg) if cfg.Profile != "" { env = append(env, "DATABRICKS_CONFIG_PROFILE="+cfg.Profile) diff --git a/libs/apps/apps.go b/libs/apps/apps.go index 7eeaf7f00c..420db61c52 100644 --- a/libs/apps/apps.go +++ b/libs/apps/apps.go @@ -1,12 +1,14 @@ package apps +import "context" + type App interface { PrepareEnvironment() error GetCommand(bool) ([]string, error) } -func NewApp(config *Config, spec *AppSpec) App { +func NewApp(ctx context.Context, config *Config, spec *AppSpec) App { // We only support python apps for now, but later we can add more types // based on AppSpec - return NewPythonApp(config, spec) + return NewPythonApp(ctx, config, spec) } diff --git a/libs/apps/python.go b/libs/apps/python.go index 964b23e6b4..9360505909 100644 --- a/libs/apps/python.go +++ b/libs/apps/python.go @@ -1,13 +1,15 @@ package apps import ( + "context" "errors" "fmt" "os" - "os/exec" "path/filepath" "strconv" "strings" + + "github.com/databricks/cli/libs/exec" ) const DEBUG_PORT = "5678" @@ -37,16 +39,17 @@ var defaultLibraries = []string{ } type PythonApp struct { + ctx context.Context config *Config spec *AppSpec uvArgs []string } -func NewPythonApp(config *Config, spec *AppSpec) *PythonApp { +func NewPythonApp(ctx context.Context, config *Config, spec *AppSpec) *PythonApp { if config.DebugPort == "" { config.DebugPort = DEBUG_PORT } - return &PythonApp{config: config, spec: spec} + return &PythonApp{ctx: ctx, config: config, spec: spec} } // PrepareEnvironment creates a Python virtual environment using uv and installs required dependencies. @@ -68,7 +71,9 @@ func (p *PythonApp) PrepareEnvironment() error { // Install requirements if they exist if _, err := os.Stat(filepath.Join(p.config.AppPath, "requirements.txt")); err == nil { - reqArgs := []string{"uv", "pip", "install", "-r", filepath.Join(p.config.AppPath, "requirements.txt")} + // We also execute command with CWD set at p.config.AppPath + // so we can just path local path to requirements.txt here + reqArgs := []string{"uv", "pip", "install", "-r", "requirements.txt"} if err := p.runCommand(reqArgs); err != nil { return err } @@ -133,11 +138,19 @@ func (p *PythonApp) enableDebugging() { } } -// runCommand executes the given command as a bash command and returns any error. +// runCommand executes the given command and returns any error. func (p *PythonApp) runCommand(args []string) error { - cmd := exec.Command("bash", "-c", strings.Join(args, " ")) - cmd.Dir = p.spec.config.AppPath - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() + e, err := exec.NewCommandExecutor(p.config.AppPath) + if err != nil { + return err + } + e.WithInheritOutput() + + // Safe to join args with spaces here since args are passed directly inside PrepareEnvironment() and GetCommand() + // and don't contain user input. + cmd, err := e.StartCommand(p.ctx, strings.Join(args, " ")) + if err != nil { + return err + } + return cmd.Wait() } diff --git a/libs/apps/python_test.go b/libs/apps/python_test.go index e2f9709ce9..dcbdc38aea 100644 --- a/libs/apps/python_test.go +++ b/libs/apps/python_test.go @@ -1,6 +1,7 @@ package apps import ( + "context" "path/filepath" "testing" @@ -88,7 +89,7 @@ func TestPythonAppGetCommand(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { config, spec := tt.setup() - app := NewPythonApp(config, spec) + app := NewPythonApp(context.Background(), config, spec) cmd, err := app.GetCommand(tt.debug) if !tt.wantErr { diff --git a/libs/exec/exec.go b/libs/exec/exec.go index 466117e601..f3e777a423 100644 --- a/libs/exec/exec.go +++ b/libs/exec/exec.go @@ -61,21 +61,28 @@ func (c *command) Stderr() io.ReadCloser { } type Executor struct { - shell shell - dir string + shell shell + dir string + inheritOutput bool } +// NewCommandExecutor creates an Executor with default output behavior (no inheritance) func NewCommandExecutor(dir string) (*Executor, error) { shell, err := findShell() if err != nil { return nil, err } return &Executor{ - shell: shell, - dir: dir, + shell: shell, + dir: dir, + inheritOutput: false, }, nil } +func (e *Executor) WithInheritOutput() { + e.inheritOutput = true +} + func NewCommandExecutorWithExecutable(dir string, execType ExecutableType) (*Executor, error) { f, ok := finders[execType] if !ok { @@ -107,20 +114,24 @@ func (e *Executor) StartCommand(ctx context.Context, command string) (Command, e if err != nil { return nil, err } - return e.start(ctx, cmd, ec) + return e.start(cmd, ec) } -func (e *Executor) start(ctx context.Context, cmd *osexec.Cmd, ec *execContext) (Command, error) { +func (e *Executor) start(cmd *osexec.Cmd, ec *execContext) (Command, error) { + if e.inheritOutput { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + return &command{cmd, ec, nil, nil}, cmd.Start() + } stdout, err := cmd.StdoutPipe() if err != nil { return nil, err } - stderr, err := cmd.StderrPipe() if err != nil { return nil, err } - return &command{cmd, ec, stdout, stderr}, cmd.Start() } diff --git a/libs/exec/exec_test.go b/libs/exec/exec_test.go index f245f9dd1e..49ab2eba66 100644 --- a/libs/exec/exec_test.go +++ b/libs/exec/exec_test.go @@ -123,7 +123,7 @@ func TestExecutorCleanupsTempFiles(t *testing.T) { cmd, ec, err := executor.prepareCommand(context.Background(), "echo 'Hello'") assert.NoError(t, err) - command, err := executor.start(context.Background(), cmd, ec) + command, err := executor.start(cmd, ec) assert.NoError(t, err) fileName := ec.args[1]