From 922340d8c4a4330f766d4903bdea5fdf6e3d4cf0 Mon Sep 17 00:00:00 2001 From: lyyyuna Date: Mon, 19 Apr 2021 10:18:37 +0800 Subject: [PATCH] parse build args --- cmd/build.go | 27 ++-- cmd/root.go | 3 - doc/how_to_parse_args_and_flags_in_goc.md | 16 ++ doc/return_error_or_fatal.md | 2 + go.mod | 1 + pkg/config/config.go | 45 +++++- pkg/flag/build_flags.go | 82 ++++++++++ pkg/flag/flags.go | 40 +++++ pkg/flag/help.go | 20 +++ pkg/flag/packages.go | 181 ++++++++++++++++++++++ pkg/log/ci_logger.go | 2 + 11 files changed, 398 insertions(+), 21 deletions(-) create mode 100644 doc/how_to_parse_args_and_flags_in_goc.md create mode 100644 doc/return_error_or_fatal.md create mode 100644 pkg/flag/build_flags.go create mode 100644 pkg/flag/flags.go create mode 100644 pkg/flag/help.go create mode 100644 pkg/flag/packages.go diff --git a/cmd/build.go b/cmd/build.go index 413346e..de5e2d6 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -1,31 +1,24 @@ package cmd import ( - "time" + "github.com/qiniu/goc/v2/pkg/flag" - "github.com/qiniu/goc/v2/pkg/log" "github.com/spf13/cobra" ) var buildCmd = &cobra.Command{ Use: "build", - Run: func(cmd *cobra.Command, args []string) { - log.StartWait("doing something") - time.Sleep(time.Second * 3) - log.Infof("building") - time.Sleep(time.Second * 3) - log.Infof("making temp dir") - time.Sleep(time.Second * 3) - log.StopWait() - log.Donef("done") - log.Infof("hello") - log.Errorf("hello") - log.Warnf("hello") - log.Debugf("hello") - log.Fatalf("fail to excute: %v, %v", "ee", "ddd") - }, + Run: build, + + DisableFlagParsing: true, // build 命令需要用原生 go 的方式处理 flags } func init() { rootCmd.AddCommand(buildCmd) } + +func build(cmd *cobra.Command, args []string) { + remainedArgs := flag.BuildCmdArgsParse(cmd, args) + where, buildName := flag.GetPackagesDir(remainedArgs) + +} diff --git a/cmd/root.go b/cmd/root.go index 021df66..187ed04 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -17,7 +17,6 @@ import ( "github.com/qiniu/goc/v2/pkg/config" "github.com/qiniu/goc/v2/pkg/log" "github.com/spf13/cobra" - "github.com/spf13/viper" ) var rootCmd = &cobra.Command{ @@ -40,8 +39,6 @@ Find more information at: func init() { rootCmd.PersistentFlags().BoolVar(&config.GocConfig.Debug, "debug", false, "run goc in debug mode") - - viper.BindPFlag("debug", rootCmd.PersistentFlags().Lookup("debug")) } // Execute the goc tool diff --git a/doc/how_to_parse_args_and_flags_in_goc.md b/doc/how_to_parse_args_and_flags_in_goc.md new file mode 100644 index 0000000..c4e9e8b --- /dev/null +++ b/doc/how_to_parse_args_and_flags_in_goc.md @@ -0,0 +1,16 @@ +# goc 中的参数处理设计 + +## 背景 + +goc build/install/run 有不少人反馈使用起来和 go build/install/run 相比,还是有不少的差异。这种差异导致在日常开发、CI/CD 中替换不便,有些带引号的参数会被改写的面目全非。 + +## 原则 + +goc build/install/run 会尽可能的模仿 go 原生的方式去处理参数。 + +## 主要问题 + +1. goc 使用 cobra 库来组织各个子命令。cobra 对 flag 处理采用的是 posix 风格(两个个短横线),和 go 的 flag 处理差异很大(一个短横线)。 +2. go 命令中 args 和 flags 有着严格先后顺序。而 cobra 库对 flags 和 args 的位置没有要求。 +3. 参数中 `[packages]` 有多种组合情况,会影响到插桩的起始位置。 +4. goc 还有自己参数,且需要和**非** goc build/install/run 的子命令保持一致(两个短横线)。 \ No newline at end of file diff --git a/doc/return_error_or_fatal.md b/doc/return_error_or_fatal.md new file mode 100644 index 0000000..a3bc736 --- /dev/null +++ b/doc/return_error_or_fatal.md @@ -0,0 +1,2 @@ +# 返回 error 还是原地 fatal + diff --git a/go.mod b/go.mod index 822c63b..5cc7a2f 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/mattn/go-colorable v0.1.8 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d github.com/spf13/cobra v1.1.3 + github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.7.1 github.com/ugorji/go v1.1.4 // indirect github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 // indirect diff --git a/pkg/config/config.go b/pkg/config/config.go index d1958ae..b065550 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,7 +1,50 @@ package config type gocConfig struct { - Debug bool + Debug bool + CurPkgDir string + CurModProjectDir string + TmpModProjectDir string + TmpPkgDir string + BinaryName string } var GocConfig gocConfig + +type goConfig struct { + BuildA bool + BuildBuildmode string // -buildmode flag + BuildMod string // -mod flag + BuildModReason string // reason -mod flag is set, if set by default + BuildI bool // -i flag + BuildLinkshared bool // -linkshared flag + BuildMSan bool // -msan flag + BuildN bool // -n flag + BuildO string // -o flag + BuildP int // -p flag + BuildPkgdir string // -pkgdir flag + BuildRace bool // -race flag + BuildToolexec string // -toolexec flag + BuildToolchainName string + BuildToolchainCompiler func() string + BuildToolchainLinker func() string + BuildTrimpath bool // -trimpath flag + BuildV bool // -v flag + BuildWork bool // -work flag + BuildX bool // -x flag + // from buildcontext + Installsuffix string // -installSuffix + BuildTags string // -tags + // from load + BuildAsmflags string + BuildCompiler string + BuildGcflags string + BuildGccgoflags string + BuildLdflags string + + // mod related + ModCacheRW bool + ModFile string +} + +var GoConfig goConfig diff --git a/pkg/flag/build_flags.go b/pkg/flag/build_flags.go new file mode 100644 index 0000000..86abf6f --- /dev/null +++ b/pkg/flag/build_flags.go @@ -0,0 +1,82 @@ +package flag + +import ( + "flag" + + "github.com/qiniu/goc/v2/pkg/log" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +var buildUsage string = `Usage: + goc build [-o output] [build flags] [packages] [goc flags] + +The [goc flags] can be placed in anywhere in the command line. +However, other flags' order are same with the go official command. +` + +// BuildCmdArgsParse parse both go flags and goc flags, it returns all non-flag arguments. +// It will log fatal if error +func BuildCmdArgsParse(cmd *cobra.Command, args []string) []string { + // 首先解析 cobra 定义的 flag + allFlagSets := cmd.Flags() + // 因为 args 里面含有 go 的 flag,所以需要忽略解析 go flag 的错误 + allFlagSets.Init("GOC", pflag.ContinueOnError) + allFlagSets.Parse(args) + + // 重写 help + helpFlag := allFlagSets.Lookup("help") + + if helpFlag.Changed { + printHelp(buildUsage, cmd) + } + // 删除 help flag + args = findAndDelHelpFlag(args) + + // 必须手动调用 + // 由于关闭了 cobra 的 flag parse,root PersistentPreRun 调用时,log.NewLogger 并没有拿到 debug 值 + log.NewLogger() + + // 删除 cobra 定义的 flag + allFlagSets.Visit(func(f *pflag.Flag) { + args = findAndDelGocFlag(args, f.Name) + }) + + // 然后解析 go 的 flag + goFlagSets := flag.NewFlagSet("GO", flag.ContinueOnError) + addBuildFlags(goFlagSets) + addOutputFlags(goFlagSets) + err := goFlagSets.Parse(args) + if err != nil { + log.Fatalf("%v", err) + } + + return goFlagSets.Args() +} + +func findAndDelGocFlag(a []string, x string) []string { + new := make([]string, 0, len(a)) + x = "--" + x + for _, v := range a { + if v == x { + continue + } else { + new = append(new, v) + } + } + + return new +} + +func findAndDelHelpFlag(a []string) []string { + new := make([]string, 0, len(a)) + for _, v := range a { + if v == "--help" || v == "-h" { + continue + } else { + new = append(new, v) + } + } + + return new +} diff --git a/pkg/flag/flags.go b/pkg/flag/flags.go new file mode 100644 index 0000000..6cbfb1a --- /dev/null +++ b/pkg/flag/flags.go @@ -0,0 +1,40 @@ +package flag + +import ( + "flag" + + "github.com/qiniu/goc/v2/pkg/config" +) + +func addBuildFlags(cmdSet *flag.FlagSet) { + cmdSet.BoolVar(&config.GoConfig.BuildA, "a", false, "") + cmdSet.BoolVar(&config.GoConfig.BuildN, "n", false, "") + cmdSet.IntVar(&config.GoConfig.BuildP, "p", 4, "") + cmdSet.BoolVar(&config.GoConfig.BuildV, "v", false, "") + cmdSet.BoolVar(&config.GoConfig.BuildX, "x", false, "") + cmdSet.StringVar(&config.GoConfig.BuildBuildmode, "buildmode", "default", "") + cmdSet.StringVar(&config.GoConfig.BuildMod, "mod", "", "") + cmdSet.StringVar(&config.GoConfig.Installsuffix, "installsuffix", "", "") + + // 类型和 go 原生的不一样,这里纯粹是为了 parse 并传递给 go + cmdSet.StringVar(&config.GoConfig.BuildAsmflags, "asmflags", "", "") + cmdSet.StringVar(&config.GoConfig.BuildCompiler, "compiler", "", "") + cmdSet.StringVar(&config.GoConfig.BuildGcflags, "gcflags", "", "") + cmdSet.StringVar(&config.GoConfig.BuildGccgoflags, "gccgoflags", "", "") + // mod related + cmdSet.BoolVar(&config.GoConfig.ModCacheRW, "modcacherw", false, "") + cmdSet.StringVar(&config.GoConfig.ModFile, "modfile", "", "") + cmdSet.StringVar(&config.GoConfig.BuildLdflags, "ldflags", "", "") + cmdSet.BoolVar(&config.GoConfig.BuildLinkshared, "linkshared", false, "") + cmdSet.StringVar(&config.GoConfig.BuildPkgdir, "pkgdir", "", "") + cmdSet.BoolVar(&config.GoConfig.BuildRace, "race", false, "") + cmdSet.BoolVar(&config.GoConfig.BuildMSan, "msan", false, "") + cmdSet.StringVar(&config.GoConfig.BuildTags, "tags", "", "") + cmdSet.StringVar(&config.GoConfig.BuildToolexec, "toolexec", "", "") + cmdSet.BoolVar(&config.GoConfig.BuildTrimpath, "trimpath", false, "") + cmdSet.BoolVar(&config.GoConfig.BuildWork, "work", false, "") +} + +func addOutputFlags(cmdSet *flag.FlagSet) { + cmdSet.StringVar(&config.GoConfig.BuildO, "o", "", "") +} diff --git a/pkg/flag/help.go b/pkg/flag/help.go new file mode 100644 index 0000000..78fe230 --- /dev/null +++ b/pkg/flag/help.go @@ -0,0 +1,20 @@ +package flag + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func printHelp(usage string, cmd *cobra.Command) { + fmt.Println(usage) + + flags := cmd.LocalFlags() + globalFlags := cmd.Parent().PersistentFlags() + + fmt.Println("Flags:") + fmt.Println(flags.FlagUsages()) + + fmt.Println("Global Flags:") + fmt.Println(globalFlags.FlagUsages()) +} diff --git a/pkg/flag/packages.go b/pkg/flag/packages.go new file mode 100644 index 0000000..8e0b493 --- /dev/null +++ b/pkg/flag/packages.go @@ -0,0 +1,181 @@ +package flag + +import ( + "fmt" + "log" + "os" + "path/filepath" + "strings" +) + +// GetPackagesDir parse [pacakges] part of args, it will fatal if error encountered +// +// Return 1: [packages] 所在的目录位置,供后续插桩使用。 +// +// Return 2: 如果参数是 *.go,第一个 .go 文件的文件名。go build 中,二进制名字既可能是目录名也可能是文件名,和参数类型有关。 +// +// 如果 [packages] 非法(即不符合 go 原生的定义),则返回对应错误 +// 这里只考虑 go mod 的方式 +func GetPackagesDir(patterns []string) (string, string) { + for _, p := range patterns { + // patterns 只支持两种格式 + // 1. 要么是直接指向某些 .go 文件的相对/绝对路径 + if strings.HasSuffix(p, ".go") { + if fi, err := os.Stat(p); err == nil && !fi.IsDir() { + // check if valid + if err := goFilesPackage(patterns); err != nil { + log.Fatalf("%v", err) + } + + // 获取绝对路径 + absp, err := filepath.Abs(p) + if err != nil { + log.Fatalf("%v", err) + } + return filepath.Dir(absp), filepath.Base(absp) + } + } + } + + // 2. 要么是 import path + coverWd, err := getDirFromImportPaths(patterns) + if err != nil { + log.Fatalf("%v", err) + } + + return coverWd, "" +} + +// goFilesPackage 对一组 go 文件解析,判断是否合法 +// go 本身还判断语法上是否是同一个 package,goc 这里不做解析 +// 1. 都是 *.go 文件? +// 2. *.go 文件都在同一个目录? +// 3. *.go 文件存在? +func goFilesPackage(gofiles []string) error { + // 1. 必须都是 *.go 结尾 + for _, f := range gofiles { + if !strings.HasSuffix(f, ".go") { + return fmt.Errorf("named files must be .go files: %s", f) + } + } + + var dir string + for _, file := range gofiles { + // 3. 文件都存在? + fi, err := os.Stat(file) + if err != nil { + return err + } + + // 2.1 有可能以 *.go 结尾的目录 + if fi.IsDir() { + return fmt.Errorf("%s is a directory, should be a Go file", file) + } + + // 2.2 所有 *.go 必须在同一个目录内 + dir1, _ := filepath.Split(file) + if dir1 == "" { + dir1 = "./" + } + + if dir == "" { + dir = dir1 + } else if dir != dir1 { + return fmt.Errorf("named files must all be in one directory: have %s and %s", dir, dir1) + } + } + + return nil +} + +// getDirFromImportPaths return the import path's real directory +// +// 该函数接收到的只有 dir 或 import path,file 在上一步已被排除 +// 只考虑 go modules 的情况 +func getDirFromImportPaths(patterns []string) (string, error) { + // no import path, pattern = current wd + if len(patterns) == 0 { + wd, err := os.Getwd() + if err != nil { + return "", fmt.Errorf("fail to parse import path: %w", err) + } + return wd, nil + } + + // 为了简化插桩的逻辑,goc 对 import path 要求必须都在同一个目录 + // 所以干脆只允许一个 pattern 得了 -_- + // 对于 goc build/run 来说本身就是只能在一个目录内 + // 对于 goc install 来讲,这个行为就和 go install 不同,不过多 import path 较少见 >_<,先忽略 + if len(patterns) > 1 { + return "", fmt.Errorf("goc only support one import path now") + } + + pattern := patterns[0] + switch { + // case isLocalImport(pattern) || filepath.IsAbs(pattern): + // dir1, err := filepath.Abs(pattern) + // if err != nil { + // return "", fmt.Errorf("error (%w) get directory from the import path: %v", err, pattern) + // } + // if _, err := os.Stat(dir1); err != nil { + // return "", fmt.Errorf("error (%w) get directory from the import path: %v", err, pattern) + // } + // return dir1, nil + + case strings.Contains(pattern, "..."): + i := strings.Index(pattern, "...") + dir, _ := filepath.Split(pattern[:i]) + if _, err := os.Stat(dir); err != nil { + return "", fmt.Errorf("error (%w) get directory from the import path: %v", err, pattern) + } + return dir, nil + + case strings.IndexByte(pattern, '@') > 0: + return "", fmt.Errorf("import path with @ version query is not supported in goc") + + case isMetaPackage(pattern): + return "", fmt.Errorf("`std`, `cmd`, `all` import path is not supported by goc") + + default: // 到这一步认为 pattern 是相对路径或者绝对路径 + dir1, err := filepath.Abs(pattern) + if err != nil { + return "", fmt.Errorf("error (%w) get directory from the import path: %v", err, pattern) + } + if _, err := os.Stat(dir1); err != nil { + return "", fmt.Errorf("error (%w) get directory from the import path: %v", err, pattern) + } + + return dir1, nil + } +} + +// isLocalImport reports whether the import path is +// a local import path, like ".", "..", "./foo", or "../foo" +func isLocalImport(path string) bool { + return path == "." || path == ".." || + strings.HasPrefix(path, "./") || strings.HasPrefix(path, "../") +} + +// isMetaPackage checks if the name is a reserved package name +func isMetaPackage(name string) bool { + return name == "std" || name == "cmd" || name == "all" +} + +// find direct path of current project which contains go.mod +func findModuleRoot(dir string) string { + dir = filepath.Clean(dir) + + // look for enclosing go.mod + for { + if fi, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil && !fi.IsDir() { + return dir + } + d := filepath.Dir(dir) + if d == dir { + break + } + dir = d + } + + return "" +} diff --git a/pkg/log/ci_logger.go b/pkg/log/ci_logger.go index e114931..e8486d0 100644 --- a/pkg/log/ci_logger.go +++ b/pkg/log/ci_logger.go @@ -8,6 +8,8 @@ type ciLogger struct { func newCiLogger() *ciLogger { logger, _ := zap.NewDevelopment() + // fix: increases the number of caller from always reporting the wrapper code as caller + logger = logger.WithOptions(zap.AddCallerSkip(2)) zap.ReplaceGlobals(logger) return &ciLogger{ logger: logger,