//go:build ignore package main import ( "bufio" "fmt" "os" "os/exec" "path/filepath" "strings" "time" ) type Platform struct { OS string Arch string Name string Ext string } var platforms = []Platform{ {"windows", "amd64", "Windows x86-64", ".exe"}, {"linux", "amd64", "Linux x86-64", ""}, {"linux", "arm64", "Linux ARM64", ""}, {"linux", "arm", "Linux ARMv7l", ""}, } const ( BuildTime = "2026-07-29" Binary = "frpc-console" ) // ============================================================ // 版本管理核心 // ============================================================ // readVersion 读取仓库根目录的 VERSION.ini 文件 // Preview 格式: "2.4" // LTS 格式: "2.5 20260729" func readVersion() (semver string, ltsDate string) { data, err := os.ReadFile("./VERSION.ini") if err != nil { return "0.0.0", "" } // 去掉注释行(以 ; 或 # 开头) lines := strings.Split(string(data), "\n") var content string for _, line := range lines { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") { continue } content = line break } parts := strings.Fields(content) if len(parts) < 1 { return "0.0.0", "" } semver = parts[0] if len(parts) > 1 { ltsDate = parts[1] } return semver, ltsDate } // getVersionType 判断版本类型 // 规则: X.0 或 X.5 → LTS,其他 → Preview func getVersionType(semver string) string { parts := strings.Split(semver, ".") if len(parts) < 2 { return "preview" } minor := parts[1] if minor == "0" || minor == "5" { return "lts" } return "preview" } // getFullVersion 生成完整版本字符串 // LTS (X.0/X.5): {semver}-lts-{ltsDate} (必须有日期) // Preview (其他): {semver}-preview-{buildTimestamp} func getFullVersion() string { semver, ltsDate := readVersion() vType := getVersionType(semver) switch vType { case "lts": if ltsDate == "" { ltsDate = time.Now().Format("20060102") } return semver + "-lts-" + ltsDate default: timestamp := time.Now().Format("20060102-150405") return semver + "-preview-" + timestamp } } // getImageTag 生成镜像标签 // LTS: {semver}-lts-{ltsDate} // Preview: preview-{buildTimestamp} func getImageTag() string { semver, ltsDate := readVersion() vType := getVersionType(semver) switch vType { case "lts": if ltsDate == "" { ltsDate = time.Now().Format("20060102") } return semver + "-lts-" + ltsDate default: return "preview-" + time.Now().Format("20060102-150405") } } func main() { if len(os.Args) > 1 { target := os.Args[1] switch target { case "all": buildAll() return case "clean": clean() return case "list": listPlatforms() return case "version": fmt.Println(getFullVersion()) return case "image-tag": fmt.Println(getImageTag()) return } for _, p := range platforms { if target == p.OS+"/"+p.Arch { build(p, false) return } } fmt.Println("❌ 不支持的平台:", target) fmt.Println(" 可用: windows/amd64, linux/amd64, linux/arm64, linux/arm") fmt.Println(" 或: all, list, clean, version, image-tag") return } interactiveMenu() } // ============================================================ // 构建函数 // ============================================================ func listPlatforms() { fmt.Println("可用平台:") for i, p := range platforms { fmt.Printf(" %d. %s (%s/%s)\n", i+1, p.Name, p.OS, p.Arch) } } func interactiveMenu() { fmt.Println("========================================") fmt.Println(" frpc-console 多平台构建工具") fmt.Println("========================================") fmt.Println() fmt.Println(" 当前版本:", getFullVersion()) fmt.Println(" 镜像标签:", getImageTag()) fmt.Println() for i, p := range platforms { fmt.Printf(" %d. %s (%s/%s)\n", i+1, p.Name, p.OS, p.Arch) } fmt.Println(" a. 全部构建") fmt.Println(" c. 清理构建产物") fmt.Println(" v. 显示版本信息") fmt.Println(" q. 退出") fmt.Println() reader := bufio.NewReader(os.Stdin) fmt.Print("请选择: ") input, _ := reader.ReadString('\n') input = strings.TrimSpace(strings.ToLower(input)) switch input { case "q": fmt.Println("已退出") return case "c": clean() return case "a": buildAll() return case "v": fmt.Println("版本:", getFullVersion()) fmt.Println("镜像标签:", getImageTag()) return default: var idx int if n, err := fmt.Sscanf(input, "%d", &idx); n == 1 && err == nil && idx >= 1 && idx <= len(platforms) { build(platforms[idx-1], false) return } fmt.Println("❌ 无效选择") interactiveMenu() } } func buildAll() { for _, p := range platforms { build(p, true) } fmt.Println("\n✅ 全部构建完成!产物位于 build/ 目录") } func build(p Platform, silent bool) { if !silent { fmt.Printf("🔨 构建 %s (%s/%s)...\n", p.Name, p.OS, p.Arch) } outDir := "./build" if err := os.MkdirAll(outDir, 0755); err != nil { fmt.Printf("❌ 创建目录失败: %v\n", err) return } fullVersion := getFullVersion() imageTag := getImageTag() outName := Binary + "-" + p.OS + "-" + p.Arch if p.Arch == "arm" { outName += "v7" } outName += p.Ext outPath := filepath.Join(outDir, outName) cmd := exec.Command("go", "build", "-ldflags="+ "-s -w "+ "-X main.buildVersion="+fullVersion+" "+ "-X main.buildImageTag="+imageTag+" "+ "-X main.buildTime="+BuildTime, "-o", outPath, ".", ) cmd.Env = append(os.Environ(), "GOOS="+p.OS, "GOARCH="+p.Arch, "CGO_ENABLED=0", ) if p.Arch == "arm" { cmd.Env = append(cmd.Env, "GOARM=7") } cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("❌ 构建失败: %v\n", err) return } if !silent { fmt.Printf("✅ 构建完成: %s\n", outPath) fmt.Printf(" 版本: %s\n", fullVersion) if info, err := os.Stat(outPath); err == nil { size := float64(info.Size()) / 1024 / 1024 fmt.Printf(" 📦 %.2f MB\n", size) } } if !silent { packageBinary(outPath, p) } } func packageBinary(binPath string, p Platform) { base := strings.TrimSuffix(filepath.Base(binPath), p.Ext) dir := filepath.Dir(binPath) if p.Ext == ".exe" { zipName := filepath.Join(dir, base+".zip") cmd := exec.Command("zip", "-j", zipName, binPath) if err := cmd.Run(); err == nil { fmt.Printf(" 📦 已打包: %s\n", zipName) } } else { tarName := filepath.Join(dir, base+".tar.gz") cmd := exec.Command("tar", "-czf", tarName, "-C", dir, filepath.Base(binPath)) if err := cmd.Run(); err == nil { fmt.Printf(" 📦 已打包: %s\n", tarName) } } } func clean() { fmt.Println("🧹 清理构建产物...") if err := os.RemoveAll("./build"); err != nil { fmt.Printf("❌ 清理失败: %v\n", err) return } fmt.Println("✅ 清理完成") }