main.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. package main
  2. import (
  3. "bufio"
  4. "fmt"
  5. "io/ioutil"
  6. "log"
  7. "net/url"
  8. "os"
  9. "os/exec"
  10. "os/user"
  11. "path"
  12. "path/filepath"
  13. "regexp"
  14. "strconv"
  15. "strings"
  16. "time"
  17. "github.com/pkg/errors"
  18. "github.com/urfave/cli"
  19. )
  20. var tplFlag = false
  21. var grpcFlag = false
  22. // 使用app/tool/warden/protoc.sh 生成grpc .pb.go
  23. var protocShRunned = false
  24. var syncLiveDoc = false
  25. func main() {
  26. app := cli.NewApp()
  27. app.Name = "bmgen"
  28. app.Usage = "根据proto文件生成bm框架或者grpc代码: \n" +
  29. "用法1,在项目的任何一个位置运行bmgen,会自动找到proto文件\n" +
  30. "用法2,bmgen proto文件(文件必须在一个项目的api中(项目包含cmd和api目录))\n"
  31. app.Version = "1.0.0"
  32. app.Commands = []cli.Command{
  33. {
  34. Name: "update",
  35. Usage: "更新工具本身",
  36. Action: actionUpdate,
  37. },
  38. {
  39. Name: "clean-live-doc",
  40. Usage: "清除live-doc已经失效的分支的文档",
  41. Action: actionCleanLiveDoc,
  42. },
  43. }
  44. app.Action = actionGenerate
  45. app.Flags = []cli.Flag{
  46. cli.StringFlag{
  47. Name: "interface, i",
  48. Value: "",
  49. Usage: "generate for the interface project `RELATIVE-PATH`, eg. live/live-demo",
  50. },
  51. cli.BoolFlag{
  52. Name: "grpc, g",
  53. Destination: &grpcFlag,
  54. Usage: "废弃,会自动检测是否需要grpc",
  55. },
  56. cli.BoolFlag{
  57. Name: "tpl, t",
  58. Destination: &tplFlag,
  59. Usage: "是否生成service模板代码",
  60. },
  61. cli.BoolFlag{
  62. Name: "live-doc, l",
  63. Destination: &syncLiveDoc,
  64. Usage: "同步该项目的文档到live-doc https://git.bilibili.co/live-dev/live-doc/tree/doc/proto/$branch/$package/$filename.$Service.md",
  65. },
  66. }
  67. err := app.Run(os.Args)
  68. if err != nil {
  69. log.Fatal(err)
  70. }
  71. }
  72. func autoUpdate(ctx *cli.Context) (err error) {
  73. tfile, e := ioutil.ReadFile("/tmp/bmgentime")
  74. if e != nil {
  75. tfile = []byte{'0'}
  76. }
  77. ts, _ := strconv.ParseInt(string(tfile), 0, 64)
  78. current := time.Now().Unix()
  79. if (current - int64(ts)) > 3600*12 { // 12 hours old
  80. ioutil.WriteFile("/tmp/bmgentime", []byte(strconv.FormatInt(current, 10)), 0777)
  81. err = actionUpdate(ctx)
  82. if err != nil {
  83. return
  84. }
  85. }
  86. return
  87. }
  88. func actionCleanLiveDoc(ctx *cli.Context) (err error) {
  89. fmt.Println("暂未实现,敬请期待")
  90. return
  91. }
  92. func installDependencies() (err error) {
  93. // 检查protoc
  94. //
  95. e := runCmd("which protoc")
  96. if e != nil {
  97. var uname string
  98. uname, err = runCmdRet("uname")
  99. if err != nil {
  100. return
  101. }
  102. if uname == "Darwin" {
  103. err = runCmd("brew install protobuf")
  104. if err != nil {
  105. return
  106. }
  107. } else {
  108. fmt.Println("找不到protoc,请于 https://github.com/protocolbuffers/protobuf/releases 下载里面的protoc-$VERSION-$OS.zip 安装, 注意把文件拷贝到正确的未知")
  109. return errors.New("找不到protoc")
  110. }
  111. }
  112. err = runCmd("which protoc-gen-gogofast || go get github.com/gogo/protobuf/protoc-gen-gogofast")
  113. return
  114. }
  115. // actionGenerate invokes protoc to generate files
  116. func actionGenerate(ctx *cli.Context) (err error) {
  117. if err = autoUpdate(ctx); err != nil {
  118. return
  119. }
  120. err = installDependencies()
  121. if err != nil {
  122. return
  123. }
  124. f := ctx.Args().Get(0)
  125. goPath := initGopath()
  126. if !fileExist(goPath) {
  127. return cli.NewExitError(fmt.Sprintf("GOPATH not exist: "+goPath), 1)
  128. }
  129. filesToGenerate := []string{f}
  130. iPath := ctx.String("i")
  131. if iPath != "" {
  132. iPath = goPath + "/src/go-common/app/interface/" + iPath
  133. if !fileExist(iPath) {
  134. return cli.NewExitError(fmt.Sprintf("interface project not found: "+iPath), 1)
  135. }
  136. pbs := filesWithSuffix(iPath+"/api", ".pb", ".proto")
  137. if len(pbs) == 0 {
  138. return cli.NewExitError(fmt.Sprintf("no pbs found in path: "+iPath+"/api"), 1)
  139. }
  140. filesToGenerate = pbs
  141. fmt.Printf(".pb files found %v\n", pbs)
  142. } else {
  143. if f == "" {
  144. // if is is empty, look up project that contains current dir
  145. abs, _ := filepath.Abs(".")
  146. proj := lookupProjPath(abs)
  147. if proj == "" {
  148. return cli.NewExitError("current dir is not in any project : "+abs, 1)
  149. }
  150. if proj != "" {
  151. pbs := filesWithSuffix(proj+"/api", ".pb", ".proto")
  152. if len(pbs) == 0 {
  153. return cli.NewExitError(fmt.Sprintf("no pbs found in path: "+proj+"/api"), 1)
  154. }
  155. filesToGenerate = pbs
  156. fmt.Printf(".pb files found %v\n", pbs)
  157. }
  158. }
  159. }
  160. for _, p := range filesToGenerate {
  161. if !fileExist(p) {
  162. return cli.NewExitError(fmt.Sprintf("file not exist: "+p), 1)
  163. }
  164. generateForFile(p, goPath)
  165. }
  166. if syncLiveDoc {
  167. err = actionSyncLiveDoc(ctx)
  168. }
  169. return
  170. }
  171. func generateForFile(f string, goPath string) {
  172. absPath, _ := filepath.Abs(f)
  173. base := filepath.Base(f)
  174. projPath := lookupProjPath(absPath)
  175. fileFolder := filepath.Dir(absPath)
  176. var relativePath string
  177. if projPath != "" {
  178. relativePath = absPath[len(projPath)+1:]
  179. }
  180. var cmd string
  181. if strings.Index(relativePath, "api/liverpc") == 0 {
  182. // need not generate for liverpc
  183. fmt.Printf("skip for liverpc \n")
  184. return
  185. }
  186. genTpl := 0
  187. if tplFlag {
  188. genTpl = 1
  189. }
  190. if !strings.Contains(relativePath, "api/http") {
  191. //非http 生成grpc和http的代码
  192. isInsideGoCommon := strings.Contains(fileFolder, "go-common")
  193. if !protocShRunned && isInsideGoCommon {
  194. //protoc.sh 只能在大仓库中使用
  195. protocShRunned = true
  196. cmd = fmt.Sprintf(`cd "%s" && "%s/src/go-common/app/tool/warden/protoc.sh"`, fileFolder, goPath)
  197. runCmd(cmd)
  198. }
  199. genGrpcArg := "--gogofast_out=plugins=grpc:."
  200. if isInsideGoCommon {
  201. // go-common中已经用上述命令生成过了
  202. genGrpcArg = ""
  203. }
  204. cmd = fmt.Sprintf(`cd "%s" && protoc --bm_out=tpl=%d:. `+
  205. `%s -I. -I%s/src -I"%s/src/go-common" -I"%s/src/go-common/vendor" "%s"`,
  206. fileFolder, genTpl, genGrpcArg, goPath, goPath, goPath, base)
  207. } else {
  208. // 只生成http的代码
  209. var pbOutArg string
  210. if strings.LastIndex(f, ".pb") == len(f)-3 {
  211. // ends with .pb
  212. log.Printf("\n\033[0;33m======!!!!WARNING!!!!========\n" +
  213. ".pb文件生成代码的功能已经不再维护,请尽快迁移到.proto, 详情:\nhttp://info.bilibili.co/pages/viewpage.action?pageId=11864735#proto文件格式-.pb迁移到.proto\n" +
  214. "======!!!!WARNING!!!!========\033[0m\n")
  215. pbOutArg = "--gogofasterg_out=json_emitdefault=1,suffix=.pbg.go:."
  216. } else {
  217. pbOutArg = "--gogofast_out=."
  218. }
  219. cmd = fmt.Sprintf(`cd "%s" && protoc --bm_out=tpl=%d:. `+
  220. pbOutArg+` -I. -I"%s/src" -I"%s/src/go-common" -I"%s/src/go-common/vendor" "%s"`,
  221. fileFolder, genTpl, goPath, goPath, goPath, base)
  222. }
  223. err := runCmd(cmd)
  224. if err == nil {
  225. fmt.Println("ok")
  226. }
  227. }
  228. // files all files with suffix
  229. func filesWithSuffix(dir string, suffixes ...string) (result []string) {
  230. filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
  231. if !info.IsDir() {
  232. for _, suffix := range suffixes {
  233. if strings.HasSuffix(info.Name(), suffix) {
  234. result = append(result, path)
  235. break
  236. }
  237. }
  238. }
  239. return nil
  240. })
  241. return
  242. }
  243. // 扫描md文档并且同步到live-doc
  244. func actionSyncLiveDoc(ctx *cli.Context) (err error) {
  245. //mdFiles := filesWithSuffix(
  246. // if is is empty, look up project that contains current dir
  247. abs, _ := filepath.Abs(".")
  248. proj := lookupProjPath(abs)
  249. if proj == "" {
  250. err = cli.NewExitError("current dir is not in any project : "+abs, 1)
  251. return
  252. }
  253. var branch string
  254. branch, err = runCmdRet("git rev-parse --abbrev-ref HEAD")
  255. if err != nil {
  256. return
  257. }
  258. branch = url.QueryEscape(branch)
  259. err = runCmd("[ -d ~/.brpc ] || mkdir ~/.brpc")
  260. if err != nil {
  261. return
  262. }
  263. var liveDocUrl = "git@git.bilibili.co:live-dev/live-doc.git"
  264. err = runCmd("cd ~/.brpc && [ -d ~/.brpc/live-doc ] || git clone " + liveDocUrl)
  265. if err != nil {
  266. return
  267. }
  268. err = runCmd("cd ~/.brpc/live-doc && git checkout doc && git pull origin doc")
  269. if err != nil {
  270. return
  271. }
  272. mdFiles := filesWithSuffix(proj+"/api", ".md")
  273. var u *user.User
  274. u, err = user.Current()
  275. if err != nil {
  276. return err
  277. }
  278. var liveDocDir = u.HomeDir + "/.brpc/live-doc"
  279. for _, file := range mdFiles {
  280. fileHandle, _ := os.Open(file)
  281. fileScanner := bufio.NewScanner(fileHandle)
  282. for fileScanner.Scan() {
  283. var text = fileScanner.Text()
  284. var reg = regexp.MustCompile(`package=([\w\.]+)`)
  285. matches := reg.FindStringSubmatch(text)
  286. if len(matches) >= 2 {
  287. pkg := matches[1]
  288. relativeDir := strings.Replace(pkg, ".", "/", -1)
  289. var destDir = liveDocDir + "/protodoc/" + branch + "/" + relativeDir
  290. runCmd("mkdir -p " + destDir)
  291. err = runCmd(fmt.Sprintf("cp %s %s", file, destDir))
  292. if err != nil {
  293. return
  294. }
  295. } else {
  296. fmt.Println("package not found", file)
  297. }
  298. break
  299. }
  300. fileHandle.Close()
  301. }
  302. err = runCmd(`cd "` + liveDocDir + `" && git add -A`)
  303. if err != nil {
  304. return
  305. }
  306. var gitStatus string
  307. gitStatus, _ = runCmdRet(`cd "` + liveDocDir + `" && git status --porcelain`)
  308. if gitStatus != "" {
  309. err = runCmd(`cd "` + liveDocDir + `" && git commit -m 'update doc from proto' && git push origin doc`)
  310. if err != nil {
  311. return
  312. }
  313. }
  314. fmt.Printf("文档已经生成至 %s%s\n", "https://git.bilibili.co/live-dev/live-doc/tree/doc/protodoc/", url.QueryEscape(branch))
  315. return
  316. }
  317. // actionUpdate update the tools its self
  318. func actionUpdate(ctx *cli.Context) (err error) {
  319. log.Print("Updating bmgen.....")
  320. goPath := initGopath()
  321. goCommonPath := goPath + "/src/go-common"
  322. if !fileExist(goCommonPath) {
  323. return cli.NewExitError("go-common not exist : "+goCommonPath, 1)
  324. }
  325. cmd := fmt.Sprintf(`go install "go-common/app/tool/bmproto/..."`)
  326. if err = runCmd(cmd); err != nil {
  327. err = cli.NewExitError(err.Error(), 1)
  328. return
  329. }
  330. log.Print("Updated!")
  331. return
  332. }
  333. // runCmd runs the cmd & print output (both stdout & stderr)
  334. func runCmd(cmd string) (err error) {
  335. fmt.Printf("CMD: %s \n", cmd)
  336. out, err := exec.Command("/bin/bash", "-c", cmd).CombinedOutput()
  337. fmt.Print(string(out))
  338. return
  339. }
  340. func runCmdRet(cmd string) (out string, err error) {
  341. fmt.Printf("CMD: %s \n", cmd)
  342. outBytes, err := exec.Command("/bin/bash", "-c", cmd).CombinedOutput()
  343. out = strings.Trim(string(outBytes), "\n\r\t ")
  344. return
  345. }
  346. // lookupProjPath get project path by proto absolute path
  347. // assume that proto is in the project's model directory
  348. func lookupProjPath(protoAbs string) (result string) {
  349. lastIndex := len(protoAbs)
  350. curPath := protoAbs
  351. for lastIndex > 0 {
  352. if fileExist(curPath+"/cmd") && fileExist(curPath+"/api") {
  353. result = curPath
  354. return
  355. }
  356. lastIndex = strings.LastIndex(curPath, string(os.PathSeparator))
  357. curPath = protoAbs[:lastIndex]
  358. }
  359. result = ""
  360. return
  361. }
  362. func fileExist(file string) bool {
  363. _, err := os.Stat(file)
  364. return err == nil
  365. }
  366. func initGopath() string {
  367. root, err := goPath()
  368. if err != nil || root == "" {
  369. log.Printf("can not read GOPATH, use ~/go as default GOPATH")
  370. root = path.Join(os.Getenv("HOME"), "go")
  371. }
  372. return root
  373. }
  374. func goPath() (string, error) {
  375. gopaths := strings.Split(os.Getenv("GOPATH"), ":")
  376. if len(gopaths) == 1 {
  377. return gopaths[0], nil
  378. }
  379. for _, gp := range gopaths {
  380. absgp, err := filepath.Abs(gp)
  381. if err != nil {
  382. return "", err
  383. }
  384. if fileExist(absgp + "/src/go-common") {
  385. return absgp, nil
  386. }
  387. }
  388. return "", fmt.Errorf("can't found current gopath")
  389. }