// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved. // // This Source Code Form is subject to the terms of the MIT License. // If a copy of the MIT was not distributed with this file, // You can obtain one at https://github.com/gogf/gf. package cmd import ( "context" "encoding/json" "fmt" "os" "regexp" "runtime" "strings" "github.com/gogf/gf/v2/encoding/gbase64" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gbuild" "github.com/gogf/gf/v2/os/gcmd" "github.com/gogf/gf/v2/os/genv" "github.com/gogf/gf/v2/os/gfile" "github.com/gogf/gf/v2/os/gproc" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/text/gregex" "github.com/gogf/gf/v2/text/gstr" "github.com/gogf/gf/v2/util/gtag" "hotgo/internal/library/hggen/internal/utility/mlog" ) var Build = cBuild{ nodeNameInConfigFile: "gfcli.build", packedGoFileName: "internal/packed/build_pack_data.go", } type cBuild struct { g.Meta `name:"build" brief:"{cBuildBrief}" dc:"{cBuildDc}" eg:"{cBuildEg}" ad:"{cBuildAd}"` nodeNameInConfigFile string // nodeNameInConfigFile is the node name for compiler configurations in configuration file. packedGoFileName string // packedGoFileName specifies the file name for packing common folders into one single go file. } const ( cBuildDefaultFile = "main.go" cBuildBrief = `cross-building go project for lots of platforms` cBuildEg = ` gf build main.go gf build main.go --ps public,template gf build main.go --cgo gf build main.go -m none gf build main.go -n my-app -a all -s all gf build main.go -n my-app -a amd64,386 -s linux -p . gf build main.go -n my-app -v 1.0 -a amd64,386 -s linux,windows,darwin -p ./docker/bin ` cBuildDc = ` The "build" command is most commonly used command, which is designed as a powerful wrapper for "go build" command for convenience cross-compiling usage. It provides much more features for building binary: 1. Cross-Compiling for many platforms and architectures. 2. Configuration file support for compiling. 3. Build-In Variables. ` cBuildAd = ` PLATFORMS aix ppc64 android 386,amd64,arm,arm64 darwin amd64,arm64 dragonfly amd64 freebsd 386,amd64,arm illumos amd64 ios arm64 js wasm linux 386,amd64,arm,arm64,loong64,mips,mipsle,mips64,mips64le,ppc64,ppc64le,riscv64,s390x netbsd 386,amd64,arm openbsd 386,amd64,arm,arm64 plan9 386,amd64,arm solaris amd64 wasip1 wasm windows 386,amd64,arm,arm64 ` // https://golang.google.cn/doc/install/source cBuildPlatforms = ` aix ppc64 android 386 android amd64 android arm android arm64 darwin amd64 darwin arm64 dragonfly amd64 freebsd 386 freebsd amd64 freebsd arm illumos amd64 ios arm64 js wasm linux 386 linux amd64 linux arm linux arm64 linux loong64 linux mips linux mipsle linux mips64 linux mips64le linux ppc64 linux ppc64le linux riscv64 linux s390x netbsd 386 netbsd amd64 netbsd arm openbsd 386 openbsd amd64 openbsd arm openbsd arm64 plan9 386 plan9 amd64 plan9 arm solaris amd64 wasip1 wasm windows 386 windows amd64 windows arm windows arm64 ` ) func init() { gtag.Sets(g.MapStrStr{ `cBuildBrief`: cBuildBrief, `cBuildDc`: cBuildDc, `cBuildEg`: cBuildEg, `cBuildAd`: cBuildAd, }) } type cBuildInput struct { g.Meta `name:"build" config:"gfcli.build"` File string `name:"FILE" arg:"true" brief:"building file path"` Name string `short:"n" name:"name" brief:"output binary name"` Version string `short:"v" name:"version" brief:"output binary version"` Arch string `short:"a" name:"arch" brief:"output binary architecture, multiple arch separated with ','"` System string `short:"s" name:"system" brief:"output binary system, multiple os separated with ','"` Output string `short:"o" name:"output" brief:"output binary path, used when building single binary file"` Path string `short:"p" name:"path" brief:"output binary directory path, default is '.'" d:"."` Extra string `short:"e" name:"extra" brief:"extra custom \"go build\" options"` Mod string `short:"m" name:"mod" brief:"like \"-mod\" option of \"go build\", use \"-m none\" to disable go module"` Cgo bool `short:"c" name:"cgo" brief:"enable or disable cgo feature, it's disabled in default" orphan:"true"` VarMap g.Map `short:"r" name:"varMap" brief:"custom built embedded variable into binary"` PackSrc string `short:"ps" name:"packSrc" brief:"pack one or more folders into one go file before building"` PackDst string `short:"pd" name:"packDst" brief:"temporary go file path for pack, this go file will be automatically removed after built" d:"internal/packed/build_pack_data.go"` ExitWhenError bool `short:"ew" name:"exitWhenError" brief:"exit building when any error occurs, specially for multiple arch and system buildings. default is false" orphan:"true"` DumpENV bool `short:"de" name:"dumpEnv" brief:"dump current go build environment before building binary" orphan:"true"` } type cBuildOutput struct{} func (c cBuild) Index(ctx context.Context, in cBuildInput) (out *cBuildOutput, err error) { mlog.SetHeaderPrint(true) mlog.Debugf(`build command input: %+v`, in) // Necessary check. if gproc.SearchBinary("go") == "" { mlog.Fatalf(`command "go" not found in your environment, please install golang first to proceed this command`) } var ( parser = gcmd.ParserFromCtx(ctx) file = in.File ) if file == "" { file = parser.GetArg(2).String() // Check and use the main.go file. if gfile.Exists(cBuildDefaultFile) { file = cBuildDefaultFile } else { mlog.Fatal("build file path is empty or main.go not found in current working directory") } } if in.Name == "" { in.Name = gfile.Name(file) } if len(in.Name) < 1 || in.Name == "*" { mlog.Fatal("name cannot be empty") } if in.Mod != "" && in.Mod != "none" { mlog.Debugf(`mod is %s`, in.Mod) if in.Extra == "" { in.Extra = fmt.Sprintf(`-mod=%s`, in.Mod) } else { in.Extra = fmt.Sprintf(`-mod=%s %s`, in.Mod, in.Extra) } } if in.Extra != "" { in.Extra += " " } var ( customSystems = gstr.SplitAndTrim(in.System, ",") customArches = gstr.SplitAndTrim(in.Arch, ",") ) if len(in.Version) > 0 { in.Path += "/" + in.Version } // System and arch checks. var ( spaceRegex = regexp.MustCompile(`\s+`) platformMap = make(map[string]map[string]bool) ) for _, line := range strings.Split(strings.TrimSpace(cBuildPlatforms), "\n") { line = gstr.Trim(line) line = spaceRegex.ReplaceAllString(line, " ") var ( array = strings.Split(line, " ") system = strings.TrimSpace(array[0]) arch = strings.TrimSpace(array[1]) ) if platformMap[system] == nil { platformMap[system] = make(map[string]bool) } platformMap[system][arch] = true } // Auto packing. if in.PackSrc != "" { if in.PackDst == "" { mlog.Fatal(`parameter "packDst" should not be empty when "packSrc" is used`) } if gfile.Exists(in.PackDst) && !gfile.IsFile(in.PackDst) { mlog.Fatalf(`parameter "packDst" path "%s" should be type of file not directory`, in.PackDst) } if !gfile.Exists(in.PackDst) { // Remove the go file that is automatically packed resource. defer func() { _ = gfile.RemoveFile(in.PackDst) mlog.Printf(`remove the automatically generated resource go file: %s`, in.PackDst) }() } // remove black space in separator. in.PackSrc, _ = gregex.ReplaceString(`,\s+`, `,`, in.PackSrc) packCmd := fmt.Sprintf(`gf pack %s %s --keepPath=true`, in.PackSrc, in.PackDst) mlog.Print(packCmd) gproc.MustShellRun(ctx, packCmd) } // Injected information by building flags. ldFlags := fmt.Sprintf( `-X 'github.com/gogf/gf/v2/os/gbuild.builtInVarStr=%v'`, c.getBuildInVarStr(ctx, in), ) // start building mlog.Print("start building...") if in.Cgo { genv.MustSet("CGO_ENABLED", "1") } else { genv.MustSet("CGO_ENABLED", "0") } // print used go env if in.DumpENV { _, _ = Env.Index(ctx, cEnvInput{}) } for system, item := range platformMap { if len(customSystems) > 0 && customSystems[0] != "all" && !gstr.InArray(customSystems, system) { continue } for arch := range item { if len(customArches) > 0 && customArches[0] != "all" && !gstr.InArray(customArches, arch) { continue } if len(customSystems) == 0 && len(customArches) == 0 { // Single binary building, output the binary to current working folder. // For example: // `gf build` // `gf build -o main.exe` c.doBinaryBuild( ctx, file, in.Output, in.Path, runtime.GOOS, runtime.GOARCH, in.Name, ldFlags, in.Extra, in.ExitWhenError, true, ) } else { c.doBinaryBuild( ctx, file, in.Output, in.Path, system, arch, in.Name, ldFlags, in.Extra, in.ExitWhenError, false, ) } // single binary building. if len(customSystems) == 0 && len(customArches) == 0 { goto buildDone } } } buildDone: mlog.Print("done!") return } func (c cBuild) doBinaryBuild( ctx context.Context, filePath string, outputPath, dirPath string, system, arch, name, ldFlags, extra string, exitWhenError bool, singleBuild bool, ) { var ( cmd string ext string ) // Cross-building, output the compiled binary to specified path. if system == "windows" { ext = ".exe" } genv.MustSet("GOOS", system) genv.MustSet("GOARCH", arch) if outputPath != "" { outputPath = "-o " + outputPath } else { if dirPath == "" { dirPath = "." } else { dirPath = gstr.TrimRight(dirPath, "/") } if singleBuild { outputPath = fmt.Sprintf( "-o %s/%s%s", dirPath, name, ext, ) } else { outputPath = fmt.Sprintf( "-o %s/%s/%s%s", dirPath, system+"_"+arch, name, ext, ) } } cmd = fmt.Sprintf( `go build %s -ldflags "%s" %s%s`, outputPath, ldFlags, extra, filePath, ) mlog.Debug(fmt.Sprintf("build for GOOS=%s GOARCH=%s", system, arch)) mlog.Debug(cmd) // It's not necessary printing the complete command string, filtering ldFlags. cmdShow, _ := gregex.ReplaceString(`\s+(-ldflags ".+?")\s+`, " ", cmd) mlog.Print(cmdShow) if result, err := gproc.ShellExec(ctx, cmd); err != nil { mlog.Printf( "failed to build, os:%s, arch:%s, error:\n%s\n\n%s\n", system, arch, gstr.Trim(result), `you may use command option "--debug" to enable debug info and check the details`, ) if exitWhenError { os.Exit(1) } } else { mlog.Debug(gstr.Trim(result)) } } // getBuildInVarStr retrieves and returns the custom build-in variables in configuration // file as json. func (c cBuild) getBuildInVarStr(ctx context.Context, in cBuildInput) string { buildInVarMap := in.VarMap if buildInVarMap == nil { buildInVarMap = make(g.Map) } buildInVarMap[gbuild.BuiltGit] = c.getGitCommit(ctx) buildInVarMap[gbuild.BuiltTime] = gtime.Now().String() buildInVarMap[gbuild.BuiltVersion] = in.Version b, err := json.Marshal(buildInVarMap) if err != nil { mlog.Fatal(err) } return gbase64.EncodeToString(b) } // getGitCommit retrieves and returns the latest git commit hash string if present. func (c cBuild) getGitCommit(ctx context.Context) string { if gproc.SearchBinary("git") == "" { return "" } var ( cmd = `git log -1 --format="%cd %H" --date=format:"%Y-%m-%d %H:%M:%S"` s, _ = gproc.ShellExec(ctx, cmd) ) mlog.Debug(cmd) if s != "" { if !gstr.Contains(s, "fatal") { return gstr.Trim(s) } } return "" }