From 76fbf74444c34d430ae427a6a161c26c6054fae4 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 28 Feb 2026 11:07:23 -0700 Subject: [PATCH] fix(monorel): error if binary path is not a main package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add checkPackageMain(), which uses go/parser with PackageClauseOnly mode (reads only the package-declaration token of each .go file — very fast) to verify the resolved binary directory declares `package main`. Called in groupByModule for every argument after resolving the absolute path, before findModuleRoot. Produces a helpful error, e.g.: monorel: error: .../io/transform/gsheet2csv is package "gsheet2csv", not a main package --- tools/monorel/main.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tools/monorel/main.go b/tools/monorel/main.go index be396a2..b077575 100644 --- a/tools/monorel/main.go +++ b/tools/monorel/main.go @@ -23,6 +23,8 @@ package main import ( "fmt" + "go/parser" + "go/token" "os" "os/exec" "path/filepath" @@ -125,6 +127,35 @@ func findModuleRoot(absDir string) (string, error) { } } +// checkPackageMain returns an error if dir does not contain a Go main package. +// It only parses the package clause of each file (PackageClauseOnly mode is +// fast: it reads just the first few tokens of every .go file). +func checkPackageMain(dir string) error { + fset := token.NewFileSet() + pkgs, err := parser.ParseDir(fset, dir, nil, parser.PackageClauseOnly) + if err != nil { + return fmt.Errorf("parsing Go files in %s: %w", dir, err) + } + if len(pkgs) == 0 { + return fmt.Errorf("no Go source files in %s", dir) + } + if _, ok := pkgs["main"]; ok { + return nil + } + // Collect non-test package names for the error message. + var names []string + for name := range pkgs { + if !strings.HasSuffix(name, "_test") { + names = append(names, name) + } + } + sort.Strings(names) + if len(names) == 0 { + return fmt.Errorf("no non-test Go source files in %s", dir) + } + return fmt.Errorf("%s is package %q, not a main package", dir, strings.Join(names, ", ")) +} + // groupByModule resolves each binary path to an absolute directory, finds its // module root via findModuleRoot, and groups binaries by that root. Groups // are returned in first-occurrence order (preserving the order of args). @@ -143,6 +174,10 @@ func groupByModule(args []string) ([]*moduleGroup, error) { absDir = filepath.Dir(abs) } + if err := checkPackageMain(absDir); err != nil { + return nil, err + } + modRoot, err := findModuleRoot(absDir) if err != nil { return nil, err