package golang import ( "fmt" "os" "path/filepath" "strings" "git.linuxforward.com/byop/byop-engine/analyzer/templates" ) type Golang struct { // Add fields as needed for Golang analysis } // Name returns the name of the Golang technology stack. func (g *Golang) Name() string { return "Golang" } // Analyze analyzes the codebase and returns a guessed technology stack for Golang. func (g *Golang) Analyze(codebasePath string) (bool, error) { // Verify if the codebasePath is valid and contains Go files if codebasePath == "" { return false, fmt.Errorf("codebase path cannot be empty") } // Check if the codebase exists and contains Go files _, err := os.Stat(codebasePath) if os.IsNotExist(err) { return false, fmt.Errorf("codebase path does not exist: %s", codebasePath) } // Enhanced detection logic inspired by Railway Railpack return g.isGoMod(codebasePath) || g.isGoWorkspace(codebasePath) || g.hasMainGo(codebasePath), nil } // GenerateDockerfile generates a Dockerfile for Golang projects based on analysis func (g *Golang) GenerateDockerfile(codebasePath string) (string, error) { // Analyze the Go project analysis, err := g.analyzeGoProject(codebasePath) if err != nil { return "", fmt.Errorf("failed to analyze Go project: %w", err) } // Check for file existence hasGoMod := g.fileExists(filepath.Join(codebasePath, "go.mod")) hasGoSum := g.fileExists(filepath.Join(codebasePath, "go.sum")) hasVendor := g.dirExists(filepath.Join(codebasePath, "vendor")) // Determine build tools needed buildTools := []string{"git"} // Always need git for go get needsBuildTools := true // Determine runtime dependencies runtimeDeps := []string{} needsRuntimeDeps := false runtimeImage := "alpine:latest" if analysis.RequiresCACerts { runtimeDeps = append(runtimeDeps, "ca-certificates") needsRuntimeDeps = true } if analysis.RequiresTimezone { runtimeDeps = append(runtimeDeps, "tzdata") needsRuntimeDeps = true } if analysis.CGOEnabled { runtimeDeps = append(runtimeDeps, "libc6-compat") needsRuntimeDeps = true buildTools = append(buildTools, "build-base") } // If no runtime deps needed, we can use scratch if !needsRuntimeDeps { runtimeImage = "scratch" } // Create template data templateData := DockerfileTemplateData{ AppName: analysis.AppName, BinaryName: analysis.AppName, GoVersion: g.getGoVersion(codebasePath), Port: analysis.Port, BuildCommand: g.getBuildCommand(analysis, codebasePath), HasGoMod: hasGoMod, HasGoSum: hasGoSum, HasVendor: hasVendor, CGOEnabled: analysis.CGOEnabled, NeedsBuildTools: needsBuildTools, BuildTools: buildTools, NeedsRuntimeDeps: needsRuntimeDeps, RuntimeDeps: runtimeDeps, RuntimeImage: runtimeImage, HealthCheckEndpoint: "/health", // Default health check endpoint } // Create template engine engine, err := templates.NewTemplateEngine() if err != nil { return "", fmt.Errorf("failed to create template engine: %w", err) } // Render template dockerfile, err := engine.Render("golang", templateData) if err != nil { return "", fmt.Errorf("failed to render Golang template: %w", err) } return dockerfile, nil } // DockerfileTemplateData represents the data passed to Golang Dockerfile templates type DockerfileTemplateData struct { AppName string `json:"app_name"` BinaryName string `json:"binary_name"` GoVersion string `json:"go_version"` Port int `json:"port"` BuildCommand string `json:"build_command"` HasGoMod bool `json:"has_go_mod"` HasGoSum bool `json:"has_go_sum"` HasVendor bool `json:"has_vendor"` CGOEnabled bool `json:"cgo_enabled"` NeedsBuildTools bool `json:"needs_build_tools"` BuildTools []string `json:"build_tools"` NeedsRuntimeDeps bool `json:"needs_runtime_deps"` RuntimeDeps []string `json:"runtime_deps"` RuntimeImage string `json:"runtime_image"` HealthCheckEndpoint string `json:"health_check_endpoint,omitempty"` } // GoProjectAnalysis contains analysis results for a Go project type GoProjectAnalysis struct { GoVersion string `json:"go_version"` AppName string `json:"app_name"` MainPackage string `json:"main_package"` Port int `json:"port"` Modules []string `json:"modules"` BuildTags []string `json:"build_tags"` RequiresCACerts bool `json:"requires_ca_certs"` RequiresTimezone bool `json:"requires_timezone"` EnvVars map[string]string `json:"env_vars"` CGOEnabled bool `json:"cgo_enabled"` // New field for CGO } // analyzeGoProject analyzes a Go project to understand its structure and requirements func (g *Golang) analyzeGoProject(codebasePath string) (*GoProjectAnalysis, error) { analysis := &GoProjectAnalysis{ Port: 8080, // Default EnvVars: make(map[string]string), Modules: []string{}, CGOEnabled: os.Getenv("CGO_ENABLED") == "1", // Check CGO_ENABLED env var } // Read go.mod to get module name and Go version goModPath := filepath.Join(codebasePath, "go.mod") if content, err := os.ReadFile(goModPath); err == nil { lines := strings.Split(string(content), "\n") for _, line := range lines { line = strings.TrimSpace(line) if strings.HasPrefix(line, "module ") { parts := strings.Fields(line) if len(parts) > 1 { moduleName := parts[1] // Extract app name from module path pathParts := strings.Split(moduleName, "/") analysis.AppName = pathParts[len(pathParts)-1] } } else if strings.HasPrefix(line, "go ") { parts := strings.Fields(line) if len(parts) > 1 { analysis.GoVersion = parts[1] } } } } // Find main package analysis.MainPackage = g.findMainPackage(codebasePath) // Analyze imports to determine requirements g.analyzeImports(codebasePath, analysis) // Try to detect port from common patterns analysis.Port = g.detectPort(codebasePath) // Set default app name if not found if analysis.AppName == "" { analysis.AppName = filepath.Base(codebasePath) if analysis.AppName == "." || analysis.AppName == "" { analysis.AppName = "goapp" } } return analysis, nil } func (g *Golang) findMainPackage(codebasePath string) string { // Strategy inspired by Railway Railpack - multiple fallback approaches // 1. Check for main.go in root (most common simple case) if g.hasMainGo(codebasePath) && g.hasRootGoFiles(codebasePath) { return "." } // 2. Look for cmd directory structure (standard Go layout) if dirs, err := g.findCmdDirectories(codebasePath); err == nil && len(dirs) > 0 { // Try to find the first directory with a main function for _, dir := range dirs { if g.hasMainFunction(filepath.Join(codebasePath, dir)) { return "./" + dir // Add ./ prefix for local path } } // Fallback to first cmd directory if none have main function return "./" + dirs[0] } // 3. Check if it's a Go workspace with multiple modules if g.isGoWorkspace(codebasePath) { packages := g.getGoWorkspacePackages(codebasePath) for _, pkg := range packages { pkgPath := filepath.Join(codebasePath, pkg) if g.hasMainGo(pkgPath) || g.hasMainFunction(pkgPath) { return "./" + pkg } } } // 4. Look for any directory with a main function as final fallback if entries, err := os.ReadDir(codebasePath); err == nil { for _, entry := range entries { if entry.IsDir() && entry.Name() != ".git" && entry.Name() != "vendor" && entry.Name() != "configs" { dirPath := filepath.Join(codebasePath, entry.Name()) if g.hasMainFunction(dirPath) { return "./" + entry.Name() } } } } // 5. Default fallback return "." } // hasMainFunction checks if a directory contains Go files with a main function func (g *Golang) hasMainFunction(dirPath string) bool { files, err := os.ReadDir(dirPath) if err != nil { return false } for _, file := range files { if strings.HasSuffix(file.Name(), ".go") && !file.IsDir() { content, err := os.ReadFile(filepath.Join(dirPath, file.Name())) if err != nil { continue } contentStr := string(content) // Look for main function or main.go file if strings.Contains(contentStr, "func main(") || file.Name() == "main.go" { return true } } } return false } func (g *Golang) analyzeImports(codebasePath string, analysis *GoProjectAnalysis) { // Walk through Go files and analyze imports filepath.Walk(codebasePath, func(path string, info os.FileInfo, err error) error { if err != nil || !strings.HasSuffix(path, ".go") { return nil } content, err := os.ReadFile(path) if err != nil { return nil } contentStr := string(content) // Check for common patterns that require CA certs if strings.Contains(contentStr, "crypto/tls") || strings.Contains(contentStr, "net/http") || strings.Contains(contentStr, "github.com/") { analysis.RequiresCACerts = true } // Check for timezone requirements if strings.Contains(contentStr, "time.LoadLocation") || strings.Contains(contentStr, "time.ParseInLocation") { analysis.RequiresTimezone = true } // Add common modules if strings.Contains(contentStr, "github.com/gin-gonic/gin") { analysis.Modules = append(analysis.Modules, "gin") } if strings.Contains(contentStr, "github.com/gorilla/mux") { analysis.Modules = append(analysis.Modules, "gorilla-mux") } if strings.Contains(contentStr, "github.com/labstack/echo") { analysis.Modules = append(analysis.Modules, "echo") } return nil }) } func (g *Golang) detectPort(codebasePath string) int { defaultPort := 8080 // Common port detection patterns patterns := []string{ ":8080", ":3000", ":8000", "PORT", "HTTP_PORT", } filepath.Walk(codebasePath, func(path string, info os.FileInfo, err error) error { if err != nil || !strings.HasSuffix(path, ".go") { return nil } content, err := os.ReadFile(path) if err != nil { return nil } contentStr := string(content) for _, pattern := range patterns { if strings.Contains(contentStr, pattern) { // Try to extract actual port number if strings.Contains(pattern, ":") { portStr := strings.TrimPrefix(pattern, ":") if port := g.parsePort(portStr); port > 0 { defaultPort = port return filepath.SkipAll } } } } return nil }) return defaultPort } func (g *Golang) parsePort(portStr string) int { // Simple port parsing - in production you'd want more robust parsing switch portStr { case "8080": return 8080 case "3000": return 3000 case "8000": return 8000 default: return 0 } } func (g *Golang) fileExists(path string) bool { _, err := os.Stat(path) return err == nil } func (g *Golang) dirExists(dirpath string) bool { info, err := os.Stat(dirpath) return err == nil && info.IsDir() } // Helper methods inspired by Railway Railpack func (g *Golang) isGoMod(codebasePath string) bool { _, err := os.Stat(filepath.Join(codebasePath, "go.mod")) return err == nil } func (g *Golang) isGoWorkspace(codebasePath string) bool { _, err := os.Stat(filepath.Join(codebasePath, "go.work")) return err == nil } func (g *Golang) hasMainGo(codebasePath string) bool { _, err := os.Stat(filepath.Join(codebasePath, "main.go")) return err == nil } func (g *Golang) hasRootGoFiles(codebasePath string) bool { files, err := filepath.Glob(filepath.Join(codebasePath, "*.go")) if err != nil { return false } return len(files) > 0 } func (g *Golang) findCmdDirectories(codebasePath string) ([]string, error) { cmdDir := filepath.Join(codebasePath, "cmd") entries, err := os.ReadDir(cmdDir) if err != nil { return nil, err } var dirs []string for _, entry := range entries { if entry.IsDir() { dirs = append(dirs, filepath.Join("cmd", entry.Name())) } } return dirs, nil } func (g *Golang) getGoWorkspacePackages(codebasePath string) []string { var packages []string err := filepath.Walk(codebasePath, func(path string, info os.FileInfo, err error) error { if err != nil { return nil // Continue walking even if there's an error } if info.Name() == "go.mod" { relPath, err := filepath.Rel(codebasePath, filepath.Dir(path)) if err != nil { return nil } // Skip the root go.mod if relPath != "." { packages = append(packages, relPath) } } return nil }) if err != nil { return packages } return packages } // Enhanced build strategy inspired by Railway Railpack func (g *Golang) getBuildCommand(analysis *GoProjectAnalysis, codebasePath string) string { appName := analysis.AppName if appName == "" { appName = "app" } flags := "-w -s" if !analysis.CGOEnabled { flags = "-w -s -extldflags \"-static\"" } baseBuildCmd := fmt.Sprintf("go build -ldflags=\"%s\" -o %s", flags, appName) // Strategy 1: Use explicit main package if detected if analysis.MainPackage != "" && analysis.MainPackage != "." { return fmt.Sprintf("%s %s", baseBuildCmd, analysis.MainPackage) } // Strategy 2: Check for root Go files if g.hasRootGoFiles(codebasePath) && g.isGoMod(codebasePath) { return baseBuildCmd } // Strategy 3: Try cmd directories if dirs, err := g.findCmdDirectories(codebasePath); err == nil && len(dirs) > 0 { return fmt.Sprintf("%s ./%s", baseBuildCmd, dirs[0]) } // Strategy 4: Go workspace if g.isGoWorkspace(codebasePath) { packages := g.getGoWorkspacePackages(codebasePath) for _, pkg := range packages { if g.hasMainGo(filepath.Join(codebasePath, pkg)) { return fmt.Sprintf("%s ./%s", baseBuildCmd, pkg) } } } // Strategy 5: Fallback to main.go if present if g.hasMainGo(codebasePath) { return fmt.Sprintf("%s main.go", baseBuildCmd) } // Default return baseBuildCmd } // Enhanced Go version detection func (g *Golang) getGoVersion(codebasePath string) string { // First check go.mod file if goModContents, err := os.ReadFile(filepath.Join(codebasePath, "go.mod")); err == nil { lines := strings.Split(string(goModContents), "\n") for _, line := range lines { line = strings.TrimSpace(line) if strings.HasPrefix(line, "go ") { parts := strings.Fields(line) if len(parts) > 1 { return parts[1] } } } } // Check environment variable if envVersion := os.Getenv("GO_VERSION"); envVersion != "" { return envVersion } // Default version return "1.23" }