diff --git a/go/cmd/bundler/main.go b/go/cmd/bundler/main.go index fb11ff8699..e7bbd7a64e 100644 --- a/go/cmd/bundler/main.go +++ b/go/cmd/bundler/main.go @@ -19,6 +19,9 @@ import ( "encoding/json" "flag" "fmt" + "go/build" + "go/parser" + "go/token" "io" "net/http" "os" @@ -33,10 +36,11 @@ import ( const ( // Keep these URLs centralized so reviewers can verify all outbound calls in one place. - sdkModule = "github.com/github/copilot-sdk/go" - packageLockURLFmt = "https://raw.githubusercontent.com/github/copilot-sdk/%s/nodejs/package-lock.json" - tarballURLFmt = "https://registry.npmjs.org/@github/copilot-%s/-/copilot-%s-%s.tgz" - licenseTarballFmt = "https://registry.npmjs.org/@github/copilot/-/copilot-%s.tgz" + sdkModule = "github.com/github/copilot-sdk/go" + packageLockURLFmt = "https://raw.githubusercontent.com/github/copilot-sdk/%s/nodejs/package-lock.json" + tarballURLFmt = "https://registry.npmjs.org/@github/copilot-%s/-/copilot-%s-%s.tgz" + licenseTarballFmt = "https://registry.npmjs.org/@github/copilot/-/copilot-%s.tgz" + defaultPackageName = "main" ) // Platform info: npm package suffix, binary name @@ -89,6 +93,11 @@ func main() { return } + pkgName, err := detectPackageName(*output, goos, goarch) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to detect package name: %v; using package %s\n", err, pkgName) + } + fmt.Printf("Building bundle for %s (CLI version %s)\n", *platform, version) bundle, err := buildBundle(info, version, outputPath, goos) @@ -137,7 +146,7 @@ func main() { muslBundle.wrapperHash, muslBundle.assetsArtifactPath, muslBundle.assetsHash, - "main", + pkgName, ); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) @@ -193,6 +202,60 @@ func validPlatforms() []string { return result } +// detectPackageName reads package clauses from files that match the target +// platform and build constraints. It returns defaultPackageName with an error +// when detection fails. +func detectPackageName(dir, goos, goarch string) (string, error) { + if dir == "" { + dir = "." + } + + entries, err := os.ReadDir(dir) + if err != nil { + return defaultPackageName, fmt.Errorf("failed to read package directory %q: %w", dir, err) + } + + buildContext := build.Default + buildContext.GOOS = goos + buildContext.GOARCH = goarch + + packageName := "" + for _, entry := range entries { + name := entry.Name() + if entry.IsDir() || !strings.HasSuffix(name, ".go") || + strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") || + strings.HasSuffix(name, "_test.go") || strings.HasPrefix(name, "zcopilot_") { + continue + } + matches, err := buildContext.MatchFile(dir, name) + if err != nil { + return defaultPackageName, fmt.Errorf("failed to evaluate build constraints in %q: %w", filepath.Join(dir, name), err) + } + if !matches { + continue + } + + path := filepath.Join(dir, name) + file, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.PackageClauseOnly) + if err != nil { + return defaultPackageName, fmt.Errorf("failed to parse package clause in %q: %w", path, err) + } + + if packageName == "" { + packageName = file.Name.Name + continue + } + if packageName != file.Name.Name { + return defaultPackageName, fmt.Errorf("multiple packages %q and %q found in %q", packageName, file.Name.Name, dir) + } + } + + if packageName == "" { + return defaultPackageName, fmt.Errorf("no Go package found in %q", dir) + } + return packageName, nil +} + // detectCLIVersion detects the CLI version by: // 1. Running "go list -m" to get the copilot-sdk version from the user's go.mod // 2. Fetching the package-lock.json from the SDK repo at that version diff --git a/go/cmd/bundler/main_test.go b/go/cmd/bundler/main_test.go index 0a7f1c3876..f41afe43f5 100644 --- a/go/cmd/bundler/main_test.go +++ b/go/cmd/bundler/main_test.go @@ -101,6 +101,62 @@ func readTarGz(t *testing.T, path string) map[string]string { } } +func TestDetectPackageName(t *testing.T) { + dir := t.TempDir() + files := map[string]string{ + "app_linux.go": "package application\n", + "app_test.go": "package application_test\n", + "app_windows.go": "package windowsapplication\n", + "tagged.go": "//go:build windows\n\npackage windowsapplication\n", + "zcopilot_linux_amd64.go": "package main\n", + "_ignored.go": "package ignored\n", + "zcopilot_inprocess_linux.go": "package main\n", + } + for name, content := range files { + if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0644); err != nil { + t.Fatal(err) + } + } + + for _, test := range []struct { + goos string + want string + }{ + {goos: "linux", want: "application"}, + {goos: "windows", want: "windowsapplication"}, + } { + t.Run(test.goos, func(t *testing.T) { + got, err := detectPackageName(dir, test.goos, "amd64") + if err != nil { + t.Fatal(err) + } + if got != test.want { + t.Fatalf("detectPackageName() = %q, want %q", got, test.want) + } + }) + } +} + +func TestDetectPackageNameFallsBackForMultiplePackages(t *testing.T) { + dir := t.TempDir() + for name, content := range map[string]string{ + "one.go": "package one\n", + "two.go": "package two\n", + } { + if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0644); err != nil { + t.Fatal(err) + } + } + + got, err := detectPackageName(dir, "linux", "amd64") + if err == nil { + t.Fatal("detectPackageName() succeeded for a directory containing multiple packages") + } + if got != defaultPackageName { + t.Fatalf("detectPackageName() = %q, want fallback %q", got, defaultPackageName) + } +} + func TestGenerateGoFileEmbedsRuntimeWrapperPair(t *testing.T) { dir := t.TempDir() binaryPath := filepath.Join(dir, "copilot.zst")