cmd_build.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. // Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
  2. //
  3. // This Source Code Form is subject to the terms of the MIT License.
  4. // If a copy of the MIT was not distributed with this file,
  5. // You can obtain one at https://github.com/gogf/gf.
  6. package cmd
  7. import (
  8. "context"
  9. "encoding/json"
  10. "fmt"
  11. "os"
  12. "regexp"
  13. "runtime"
  14. "strings"
  15. "github.com/gogf/gf/v2/encoding/gbase64"
  16. "github.com/gogf/gf/v2/frame/g"
  17. "github.com/gogf/gf/v2/os/gcmd"
  18. "github.com/gogf/gf/v2/os/genv"
  19. "github.com/gogf/gf/v2/os/gfile"
  20. "github.com/gogf/gf/v2/os/gproc"
  21. "github.com/gogf/gf/v2/os/gtime"
  22. "github.com/gogf/gf/v2/text/gregex"
  23. "github.com/gogf/gf/v2/text/gstr"
  24. "github.com/gogf/gf/v2/util/gtag"
  25. "github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
  26. )
  27. var (
  28. Build = cBuild{
  29. nodeNameInConfigFile: "gfcli.build",
  30. packedGoFileName: "internal/packed/build_pack_data.go",
  31. }
  32. )
  33. type cBuild struct {
  34. g.Meta `name:"build" brief:"{cBuildBrief}" dc:"{cBuildDc}" eg:"{cBuildEg}" ad:"{cBuildAd}"`
  35. nodeNameInConfigFile string // nodeNameInConfigFile is the node name for compiler configurations in configuration file.
  36. packedGoFileName string // packedGoFileName specifies the file name for packing common folders into one single go file.
  37. }
  38. const (
  39. cBuildBrief = `cross-building go project for lots of platforms`
  40. cBuildEg = `
  41. gf build main.go
  42. gf build main.go --pack public,template
  43. gf build main.go --cgo
  44. gf build main.go -m none
  45. gf build main.go -n my-app -a all -s all
  46. gf build main.go -n my-app -a amd64,386 -s linux -p .
  47. gf build main.go -n my-app -v 1.0 -a amd64,386 -s linux,windows,darwin -p ./docker/bin
  48. `
  49. cBuildDc = `
  50. The "build" command is most commonly used command, which is designed as a powerful wrapper for
  51. "go build" command for convenience cross-compiling usage.
  52. It provides much more features for building binary:
  53. 1. Cross-Compiling for many platforms and architectures.
  54. 2. Configuration file support for compiling.
  55. 3. Build-In Variables.
  56. `
  57. cBuildAd = `
  58. PLATFORMS
  59. darwin amd64,arm64
  60. freebsd 386,amd64,arm
  61. linux 386,amd64,arm,arm64,ppc64,ppc64le,mips,mipsle,mips64,mips64le
  62. netbsd 386,amd64,arm
  63. openbsd 386,amd64,arm
  64. windows 386,amd64
  65. `
  66. // https://golang.google.cn/doc/install/source
  67. cBuildPlatforms = `
  68. darwin amd64
  69. darwin arm64
  70. ios amd64
  71. ios arm64
  72. freebsd 386
  73. freebsd amd64
  74. freebsd arm
  75. linux 386
  76. linux amd64
  77. linux arm
  78. linux arm64
  79. linux ppc64
  80. linux ppc64le
  81. linux mips
  82. linux mipsle
  83. linux mips64
  84. linux mips64le
  85. netbsd 386
  86. netbsd amd64
  87. netbsd arm
  88. openbsd 386
  89. openbsd amd64
  90. openbsd arm
  91. windows 386
  92. windows amd64
  93. android arm
  94. dragonfly amd64
  95. plan9 386
  96. plan9 amd64
  97. solaris amd64
  98. `
  99. )
  100. func init() {
  101. gtag.Sets(g.MapStrStr{
  102. `cBuildBrief`: cBuildBrief,
  103. `cBuildDc`: cBuildDc,
  104. `cBuildEg`: cBuildEg,
  105. `cBuildAd`: cBuildAd,
  106. })
  107. }
  108. type cBuildInput struct {
  109. g.Meta `name:"build" config:"gfcli.build"`
  110. File string `name:"FILE" arg:"true" brief:"building file path"`
  111. Name string `short:"n" name:"name" brief:"output binary name"`
  112. Version string `short:"v" name:"version" brief:"output binary version"`
  113. Arch string `short:"a" name:"arch" brief:"output binary architecture, multiple arch separated with ','"`
  114. System string `short:"s" name:"system" brief:"output binary system, multiple os separated with ','"`
  115. Output string `short:"o" name:"output" brief:"output binary path, used when building single binary file"`
  116. Path string `short:"p" name:"path" brief:"output binary directory path, default is './temp'" d:"./temp"`
  117. Extra string `short:"e" name:"extra" brief:"extra custom \"go build\" options"`
  118. Mod string `short:"m" name:"mod" brief:"like \"-mod\" option of \"go build\", use \"-m none\" to disable go module"`
  119. Cgo bool `short:"c" name:"cgo" brief:"enable or disable cgo feature, it's disabled in default" orphan:"true"`
  120. VarMap g.Map `short:"r" name:"varMap" brief:"custom built embedded variable into binary"`
  121. PackSrc string `short:"ps" name:"packSrc" brief:"pack one or more folders into one go file before building"`
  122. 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"`
  123. 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"`
  124. DumpENV bool `short:"de" name:"dumpEnv" brief:"dump current go build environment before building binary" orphan:"true"`
  125. }
  126. type cBuildOutput struct{}
  127. func (c cBuild) Index(ctx context.Context, in cBuildInput) (out *cBuildOutput, err error) {
  128. // print used go env
  129. if in.DumpENV {
  130. _, _ = Env.Index(ctx, cEnvInput{})
  131. }
  132. mlog.SetHeaderPrint(true)
  133. mlog.Debugf(`build command input: %+v`, in)
  134. // Necessary check.
  135. if gproc.SearchBinary("go") == "" {
  136. mlog.Fatalf(`command "go" not found in your environment, please install golang first to proceed this command`)
  137. }
  138. var (
  139. parser = gcmd.ParserFromCtx(ctx)
  140. file = parser.GetArg(2).String()
  141. )
  142. if len(file) < 1 {
  143. // Check and use the main.go file.
  144. if gfile.Exists("main.go") {
  145. file = "main.go"
  146. } else {
  147. mlog.Fatal("build file path cannot be empty")
  148. }
  149. }
  150. if in.Name == "" {
  151. in.Name = gfile.Name(file)
  152. }
  153. if len(in.Name) < 1 || in.Name == "*" {
  154. mlog.Fatal("name cannot be empty")
  155. }
  156. if in.Mod != "" && in.Mod != "none" {
  157. mlog.Debugf(`mod is %s`, in.Mod)
  158. if in.Extra == "" {
  159. in.Extra = fmt.Sprintf(`-mod=%s`, in.Mod)
  160. } else {
  161. in.Extra = fmt.Sprintf(`-mod=%s %s`, in.Mod, in.Extra)
  162. }
  163. }
  164. if in.Extra != "" {
  165. in.Extra += " "
  166. }
  167. var (
  168. customSystems = gstr.SplitAndTrim(in.System, ",")
  169. customArches = gstr.SplitAndTrim(in.Arch, ",")
  170. )
  171. if len(in.Version) > 0 {
  172. in.Path += "/" + in.Version
  173. }
  174. // System and arch checks.
  175. var (
  176. spaceRegex = regexp.MustCompile(`\s+`)
  177. platformMap = make(map[string]map[string]bool)
  178. )
  179. for _, line := range strings.Split(strings.TrimSpace(cBuildPlatforms), "\n") {
  180. line = gstr.Trim(line)
  181. line = spaceRegex.ReplaceAllString(line, " ")
  182. var (
  183. array = strings.Split(line, " ")
  184. system = strings.TrimSpace(array[0])
  185. arch = strings.TrimSpace(array[1])
  186. )
  187. if platformMap[system] == nil {
  188. platformMap[system] = make(map[string]bool)
  189. }
  190. platformMap[system][arch] = true
  191. }
  192. // Auto packing.
  193. if in.PackSrc != "" {
  194. if in.PackDst == "" {
  195. mlog.Fatal(`parameter "packDst" should not be empty when "packSrc" is used`)
  196. }
  197. if gfile.Exists(in.PackDst) && !gfile.IsFile(in.PackDst) {
  198. mlog.Fatalf(`parameter "packDst" path "%s" should be type of file not directory`, in.PackDst)
  199. }
  200. if !gfile.Exists(in.PackDst) {
  201. // Remove the go file that is automatically packed resource.
  202. defer func() {
  203. _ = gfile.Remove(in.PackDst)
  204. mlog.Printf(`remove the automatically generated resource go file: %s`, in.PackDst)
  205. }()
  206. }
  207. // remove black space in separator.
  208. in.PackSrc, _ = gregex.ReplaceString(`,\s+`, `,`, in.PackSrc)
  209. packCmd := fmt.Sprintf(`gf pack %s %s --keepPath=true`, in.PackSrc, in.PackDst)
  210. mlog.Print(packCmd)
  211. gproc.MustShellRun(ctx, packCmd)
  212. }
  213. // Injected information by building flags.
  214. ldFlags := fmt.Sprintf(
  215. `-X 'github.com/gogf/gf/v2/os/gbuild.builtInVarStr=%v'`,
  216. c.getBuildInVarStr(ctx, in),
  217. )
  218. // start building
  219. mlog.Print("start building...")
  220. if in.Cgo {
  221. genv.MustSet("CGO_ENABLED", "1")
  222. } else {
  223. genv.MustSet("CGO_ENABLED", "0")
  224. }
  225. var (
  226. cmd = ""
  227. ext = ""
  228. )
  229. for system, item := range platformMap {
  230. cmd = ""
  231. ext = ""
  232. if len(customSystems) > 0 && customSystems[0] != "all" && !gstr.InArray(customSystems, system) {
  233. continue
  234. }
  235. for arch := range item {
  236. if len(customArches) > 0 && customArches[0] != "all" && !gstr.InArray(customArches, arch) {
  237. continue
  238. }
  239. if len(customSystems) == 0 && len(customArches) == 0 {
  240. if runtime.GOOS == "windows" {
  241. ext = ".exe"
  242. }
  243. // Single binary building, output the binary to current working folder.
  244. output := ""
  245. if len(in.Output) > 0 {
  246. output = "-o " + in.Output + ext
  247. } else {
  248. output = "-o " + in.Name + ext
  249. }
  250. cmd = fmt.Sprintf(`go build %s -ldflags "%s" %s %s`, output, ldFlags, in.Extra, file)
  251. } else {
  252. // Cross-building, output the compiled binary to specified path.
  253. if system == "windows" {
  254. ext = ".exe"
  255. }
  256. genv.MustSet("GOOS", system)
  257. genv.MustSet("GOARCH", arch)
  258. cmd = fmt.Sprintf(
  259. `go build -o %s/%s/%s%s -ldflags "%s" %s%s`,
  260. in.Path, system+"_"+arch, in.Name, ext, ldFlags, in.Extra, file,
  261. )
  262. }
  263. mlog.Debug(cmd)
  264. // It's not necessary printing the complete command string.
  265. cmdShow, _ := gregex.ReplaceString(`\s+(-ldflags ".+?")\s+`, " ", cmd)
  266. mlog.Print(cmdShow)
  267. if result, err := gproc.ShellExec(ctx, cmd); err != nil {
  268. mlog.Printf(
  269. "failed to build, os:%s, arch:%s, error:\n%s\n\n%s\n",
  270. system, arch, gstr.Trim(result),
  271. `you may use command option "--debug" to enable debug info and check the details`,
  272. )
  273. if in.ExitWhenError {
  274. os.Exit(1)
  275. }
  276. } else {
  277. mlog.Debug(gstr.Trim(result))
  278. }
  279. // single binary building.
  280. if len(customSystems) == 0 && len(customArches) == 0 {
  281. goto buildDone
  282. }
  283. }
  284. }
  285. buildDone:
  286. mlog.Print("done!")
  287. return
  288. }
  289. // getBuildInVarStr retrieves and returns the custom build-in variables in configuration
  290. // file as json.
  291. func (c cBuild) getBuildInVarStr(ctx context.Context, in cBuildInput) string {
  292. buildInVarMap := in.VarMap
  293. if buildInVarMap == nil {
  294. buildInVarMap = make(g.Map)
  295. }
  296. buildInVarMap["builtGit"] = c.getGitCommit(ctx)
  297. buildInVarMap["builtTime"] = gtime.Now().String()
  298. b, err := json.Marshal(buildInVarMap)
  299. if err != nil {
  300. mlog.Fatal(err)
  301. }
  302. return gbase64.EncodeToString(b)
  303. }
  304. // getGitCommit retrieves and returns the latest git commit hash string if present.
  305. func (c cBuild) getGitCommit(ctx context.Context) string {
  306. if gproc.SearchBinary("git") == "" {
  307. return ""
  308. }
  309. var (
  310. cmd = `git log -1 --format="%cd %H" --date=format:"%Y-%m-%d %H:%M:%S"`
  311. s, _ = gproc.ShellExec(ctx, cmd)
  312. )
  313. mlog.Debug(cmd)
  314. if s != "" {
  315. if !gstr.Contains(s, "fatal") {
  316. return gstr.Trim(s)
  317. }
  318. }
  319. return ""
  320. }