Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 68 additions & 5 deletions go/cmd/bundler/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import (
"encoding/json"
"flag"
"fmt"
"go/build"
"go/parser"
"go/token"
"io"
"net/http"
"os"
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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_") {
Comment thread
qmuntal marked this conversation as resolved.
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
Expand Down
56 changes: 56 additions & 0 deletions go/cmd/bundler/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading