diff --git a/pkg/runtime/golang/golang.go b/pkg/runtime/golang/golang.go index 6059faef34..141edb7fc9 100644 --- a/pkg/runtime/golang/golang.go +++ b/pkg/runtime/golang/golang.go @@ -3,6 +3,7 @@ package golang import ( "context" "encoding/json" + "fmt" "io" "log/slog" "os" @@ -130,6 +131,20 @@ func (r *Runtime) Run(ctx context.Context, input *runtime.RunInput) (runtime.Wor }, nil } +func (r *Runtime) ValidateHandler(input *runtime.BuildInput) error { + if input.Handler != "" { + if info, err := os.Stat(input.Handler); err != nil || !info.IsDir() { + return fmt.Errorf("handler not found: %v", input.Handler) + } + } + + _, err := fs.FindUp(input.Handler, "go.mod") + if err != nil { + return fmt.Errorf("handler not found: could not find go.mod for handler %v", input.Handler) + } + return nil +} + func (r *Runtime) ShouldRebuild(functionID string, file string) bool { if !strings.HasSuffix(file, ".go") { return false diff --git a/pkg/runtime/node/build.go b/pkg/runtime/node/build.go index c3bc6fbf19..abfed46426 100644 --- a/pkg/runtime/node/build.go +++ b/pkg/runtime/node/build.go @@ -41,7 +41,7 @@ func (r *Runtime) Build(ctx context.Context, input *runtime.BuildInput) (*runtim file, ok := r.getFile(input) if !ok { - return nil, fmt.Errorf("Handler not found: %v", input.Handler) + return nil, fmt.Errorf("handler not found: %v", input.Handler) } isESM := true diff --git a/pkg/runtime/node/node.go b/pkg/runtime/node/node.go index f35f237b25..86b2528afc 100644 --- a/pkg/runtime/node/node.go +++ b/pkg/runtime/node/node.go @@ -3,6 +3,7 @@ package node import ( "context" "encoding/json" + "fmt" "io" "log/slog" "os" @@ -242,15 +243,12 @@ func (r *Runtime) Match(runtime string) bool { return strings.HasPrefix(runtime, "node") } -func (r *Runtime) getFile(input *runtime.BuildInput) (string, bool) { - dir := filepath.Dir(input.Handler) - fileSplit := strings.Split(filepath.Base(input.Handler), ".") +func findHandlerFile(rootDir string, handler string) (string, bool) { + dir := filepath.Dir(handler) + fileSplit := strings.Split(filepath.Base(handler), ".") base := strings.Join(fileSplit[:len(fileSplit)-1], ".") for _, ext := range NODE_EXTENSIONS { - file := filepath.Join(dir, base+ext) - if !filepath.IsAbs(file) { - file = filepath.Join(path.ResolveRootDir(input.CfgPath), file) - } + file := filepath.Join(rootDir, dir, base+ext) if _, err := os.Stat(file); err == nil { return file, true } @@ -258,6 +256,22 @@ func (r *Runtime) getFile(input *runtime.BuildInput) (string, bool) { return "", false } +func (r *Runtime) getFile(input *runtime.BuildInput) (string, bool) { + return findHandlerFile(path.ResolveRootDir(input.CfgPath), input.Handler) +} + +func (r *Runtime) ValidateHandler(input *runtime.BuildInput) error { + rootDir := path.ResolveRootDir(input.CfgPath) + if input.Bundle != "" { + rootDir = input.Bundle + } + _, ok := findHandlerFile(rootDir, input.Handler) + if !ok { + return fmt.Errorf("handler not found: %v", input.Handler) + } + return nil +} + func (r *Runtime) ShouldRebuild(functionID string, file string) bool { result, ok := r.results.Load(functionID) if !ok { diff --git a/pkg/runtime/python/python.go b/pkg/runtime/python/python.go index eb882e7919..dba5bb6a56 100644 --- a/pkg/runtime/python/python.go +++ b/pkg/runtime/python/python.go @@ -175,6 +175,18 @@ func (r *PythonRuntime) Run(ctx context.Context, input *runtime.RunInput) (runti } +func (r *PythonRuntime) ValidateHandler(input *runtime.BuildInput) error { + rootDir := path.ResolveRootDir(input.CfgPath) + if input.Bundle != "" { + rootDir = input.Bundle + } + _, err := findHandlerFile(rootDir, input.Handler) + if err != nil { + return fmt.Errorf("handler not found: %v", input.Handler) + } + return nil +} + func (r *PythonRuntime) ShouldRebuild(functionID string, file string) bool { // Assume that the build is always stale. We could do a better job here but bc of how the build // process actually works its not a slowdown as the real slow part is starting the python interpreter @@ -382,25 +394,25 @@ func (r *PythonRuntime) CreateBuildAsset(ctx context.Context, input *runtime.Bui } } -func (r *PythonRuntime) getFile(input *runtime.BuildInput) (string, error) { - slog.Info("looking for python handler file", "handler", input.Handler) - - dir := filepath.Dir(input.Handler) - base := strings.TrimSuffix(filepath.Base(input.Handler), filepath.Ext(input.Handler)) - rootDir := path.ResolveRootDir(input.CfgPath) +func findHandlerFile(rootDir string, handler string) (string, error) { + dir := filepath.Dir(handler) + base := strings.TrimSuffix(filepath.Base(handler), filepath.Ext(handler)) - // Look for .py file pythonFile := filepath.Join(rootDir, dir, base+".py") if _, err := os.Stat(pythonFile); err == nil { return pythonFile, nil } - // No Python file found for the handler return "", fmt.Errorf("could not find Python file '%s.py' in directory '%s'", base, filepath.Join(rootDir, dir)) } +func (r *PythonRuntime) getFile(input *runtime.BuildInput) (string, error) { + slog.Info("looking for python handler file", "handler", input.Handler) + return findHandlerFile(path.ResolveRootDir(input.CfgPath), input.Handler) +} + func copyFile(src, dst string) error { srcFile, err := os.Open(src) if err != nil { diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index b8436aa6cd..08232e730d 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -20,6 +20,7 @@ type Runtime interface { Build(ctx context.Context, input *BuildInput) (*BuildOutput, error) Run(ctx context.Context, input *RunInput) (Worker, error) ShouldRebuild(functionID string, path string) bool + ValidateHandler(input *BuildInput) error } type Worker interface { @@ -100,6 +101,13 @@ func (c *Collection) Build(ctx context.Context, input *BuildInput) (*BuildOutput if input.Bundle != "" { out = input.Bundle + runtime, ok := c.Runtime(input.Runtime) + if !ok { + return nil, fmt.Errorf("runtime not found: %v", input.Runtime) + } + if err := runtime.ValidateHandler(input); err != nil { + return nil, err + } result = &BuildOutput{ Handler: input.Handler, Errors: []string{}, diff --git a/pkg/runtime/runtime_test.go b/pkg/runtime/runtime_test.go index 69dc5f169d..60719b5089 100644 --- a/pkg/runtime/runtime_test.go +++ b/pkg/runtime/runtime_test.go @@ -26,6 +26,9 @@ func (m *mockRuntime) Run(ctx context.Context, input *runtime.RunInput) (runtime func (m *mockRuntime) ShouldRebuild(functionID string, path string) bool { return false } +func (m *mockRuntime) ValidateHandler(input *runtime.BuildInput) error { + return nil +} func TestBuildInputOut(t *testing.T) { cfgPath := filepath.Join("/project", "sst.config.ts") diff --git a/pkg/runtime/rust/rust.go b/pkg/runtime/rust/rust.go index 367cd649a2..37082c5b71 100644 --- a/pkg/runtime/rust/rust.go +++ b/pkg/runtime/rust/rust.go @@ -81,6 +81,10 @@ func (r *Runtime) Build(ctx context.Context, input *runtime.BuildInput) (*runtim var properties Properties json.Unmarshal(input.Properties, &properties) + if err := r.ValidateHandler(input); err != nil { + return nil, err + } + // split handler into path and function name parts := strings.Split(input.Handler, ".") handler := strings.Join(parts[:len(parts)-1], ".") @@ -194,6 +198,26 @@ func (r *Runtime) Run(ctx context.Context, input *runtime.RunInput) (runtime.Wor }, nil } +func (r *Runtime) ValidateHandler(input *runtime.BuildInput) error { + parts := strings.Split(input.Handler, ".") + handler := strings.Join(parts[:len(parts)-1], ".") + + if handler != "" { + if info, err := os.Stat(handler); err != nil || !info.IsDir() { + return fmt.Errorf("handler not found: %v", input.Handler) + } + } + + _, err := fs.FindUp(handler, "cargo.toml") + if err != nil { + _, err = fs.FindUp(handler, "Cargo.toml") + if err != nil { + return fmt.Errorf("handler not found: could not find Cargo.toml for handler %v", input.Handler) + } + } + return nil +} + func (r *Runtime) ShouldRebuild(functionID string, file string) bool { // copied from go if !strings.HasSuffix(file, ".rs") { diff --git a/pkg/runtime/worker/worker.go b/pkg/runtime/worker/worker.go index 52d39d4726..1d84c6add2 100644 --- a/pkg/runtime/worker/worker.go +++ b/pkg/runtime/worker/worker.go @@ -179,11 +179,11 @@ func (w *Runtime) Match(runtime string) bool { return runtime == "worker" } -func (w *Runtime) getFile(input *runtime.BuildInput) (string, bool) { - dir := filepath.Dir(input.Handler) - base := strings.Split(filepath.Base(input.Handler), ".")[0] +func findHandlerFile(rootDir string, handler string) (string, bool) { + dir := filepath.Dir(handler) + base := strings.Split(filepath.Base(handler), ".")[0] for _, ext := range node.NODE_EXTENSIONS { - file := filepath.Join(path.ResolveRootDir(input.CfgPath), dir, base+ext) + file := filepath.Join(rootDir, dir, base+ext) if _, err := os.Stat(file); err == nil { return file, true } @@ -191,6 +191,22 @@ func (w *Runtime) getFile(input *runtime.BuildInput) (string, bool) { return "", false } +func (w *Runtime) getFile(input *runtime.BuildInput) (string, bool) { + return findHandlerFile(path.ResolveRootDir(input.CfgPath), input.Handler) +} + +func (r *Runtime) ValidateHandler(input *runtime.BuildInput) error { + rootDir := path.ResolveRootDir(input.CfgPath) + if input.Bundle != "" { + rootDir = input.Bundle + } + _, ok := findHandlerFile(rootDir, input.Handler) + if !ok { + return fmt.Errorf("handler not found: %v", input.Handler) + } + return nil +} + func (r *Runtime) ShouldRebuild(functionID string, file string) bool { r.lock.RLock() result, ok := r.results[functionID] diff --git a/pkg/server/runtime/runtime.go b/pkg/server/runtime/runtime.go index 0e91a3212c..ba95d7bd7b 100644 --- a/pkg/server/runtime/runtime.go +++ b/pkg/server/runtime/runtime.go @@ -2,6 +2,7 @@ package runtime import ( "context" + "fmt" "net/rpc" "github.com/sst/sst/v3/pkg/bus" @@ -24,6 +25,14 @@ func (r *Runtime) Build(input *runtime.BuildInput, output *runtime.BuildOutput) } func (r *Runtime) AddTarget(input *runtime.BuildInput, output *bool) error { + input.CfgPath = r.project.PathConfig() + rt, ok := r.project.Runtime.Runtime(input.Runtime) + if !ok { + return fmt.Errorf("runtime not found: %v", input.Runtime) + } + if err := rt.ValidateHandler(input); err != nil { + return err + } bus.Publish(input) *output = true return nil