/* Copyright 2021 Qiniu Cloud (qiniu.com) Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package build import ( "flag" "fmt" "os" "path/filepath" "strings" "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] [build flags] are same with go official command, you can copy them here directly. The [goc flags] can be placed in anywhere in the command line. However, other flags' order are same with the go official command. ` var installUsage string = `Usage: goc install [-o output] [build flags] [packages] [goc flags] [build flags] are same with go official command, you can copy them here directly. The [goc flags] can be placed in anywhere in the command line. However, other flags' order are same with the go official command. ` const ( GO_BUILD = iota GO_INSTALL ) // CustomParseCmdAndArgs 因为关闭了 cobra 的解析功能,需要手动构造并解析 goc flags func CustomParseCmdAndArgs(cmd *cobra.Command, args []string) *pflag.FlagSet { // 首先解析 cobra 定义的 flag allFlagSets := cmd.Flags() // 因为 args 里面含有 go 的 flag,所以需要忽略解析 go flag 的错误 allFlagSets.Init("GOC", pflag.ContinueOnError) // 忽略 go flag 在 goc 中的解析错误 allFlagSets.ParseErrorsWhitelist = pflag.ParseErrorsWhitelist{ UnknownFlags: true, } allFlagSets.Parse(args) return allFlagSets } // buildCmdArgsParse parse both go flags and goc flags, it rewrite go flags if // necessary, and returns all non-flag arguments. // // 吞下 [packages] 之前所有的 flags. func (b *Build) buildCmdArgsParse() { args := b.Args cmdType := b.BuildType allFlagSets := b.FlagSets // 重写 help helpFlag := allFlagSets.Lookup("help") if helpFlag.Changed { if cmdType == GO_BUILD { printGoHelp(buildUsage) } else if cmdType == GO_INSTALL { printGoHelp(installUsage) } os.Exit(0) } // 删除 help flag args = findAndDelHelpFlag(args) // 必须手动调用 // 由于关闭了 cobra 的 flag parse,root PersistentPreRun 调用时,log.NewLogger 并没有拿到 debug 值 log.NewLogger(b.Debug) // 删除 cobra 定义的 flag allFlagSets.Visit(func(f *pflag.Flag) { args = findAndDelGocFlag(args, f.Name, f.Value.String()) }) // 然后解析 go 的 flag goFlagSets := flag.NewFlagSet("GO", flag.ContinueOnError) addBuildFlags(goFlagSets) addOutputFlags(goFlagSets) err := goFlagSets.Parse(args) if err != nil { log.Fatalf("%v", err) } // 找出设置的 go flag curWd, err := os.Getwd() if err != nil { log.Fatalf("fail to get current working directory: %v", err) } flags := make([]string, 0) goFlagSets.Visit(func(f *flag.Flag) { // 将用户指定 -o 改成绝对目录 if f.Name == "o" { outputDir := f.Value.String() outputDir, err := filepath.Abs(outputDir) if err != nil { log.Fatalf("output flag is not valid: %v", err) } flags = append(flags, "-o", outputDir) } else { flags = append(flags, "-"+f.Name, f.Value.String()) } }) b.Goflags = flags b.CurWd = curWd b.GoArgs = goFlagSets.Args() return } func findAndDelGocFlag(a []string, x string, v string) []string { new := make([]string, 0, len(a)) x = "--" + x x_v := x + "=" + v for i := 0; i < len(a); i++ { if a[i] == "--gocdebug" { // debug 是 bool,就一个元素 continue } else if a[i] == x { // 有 goc flag 长这样 --mode watch i++ continue } else if a[i] == x_v { // 有 goc flag 长这样 --mode=watch continue } else { // 剩下的是 go flag new = append(new, a[i]) } } 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 } 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 goflags goConfig func addBuildFlags(cmdSet *flag.FlagSet) { cmdSet.BoolVar(&goflags.BuildA, "a", false, "") cmdSet.BoolVar(&goflags.BuildN, "n", false, "") cmdSet.IntVar(&goflags.BuildP, "p", 4, "") cmdSet.BoolVar(&goflags.BuildV, "v", false, "") cmdSet.BoolVar(&goflags.BuildX, "x", false, "") cmdSet.StringVar(&goflags.BuildBuildmode, "buildmode", "default", "") cmdSet.StringVar(&goflags.BuildMod, "mod", "", "") cmdSet.StringVar(&goflags.Installsuffix, "installsuffix", "", "") // 类型和 go 原生的不一样,这里纯粹是为了 parse 并传递给 go cmdSet.StringVar(&goflags.BuildAsmflags, "asmflags", "", "") cmdSet.StringVar(&goflags.BuildCompiler, "compiler", "", "") cmdSet.StringVar(&goflags.BuildGcflags, "gcflags", "", "") cmdSet.StringVar(&goflags.BuildGccgoflags, "gccgoflags", "", "") // mod related cmdSet.BoolVar(&goflags.ModCacheRW, "modcacherw", false, "") cmdSet.StringVar(&goflags.ModFile, "modfile", "", "") cmdSet.StringVar(&goflags.BuildLdflags, "ldflags", "", "") cmdSet.BoolVar(&goflags.BuildLinkshared, "linkshared", false, "") cmdSet.StringVar(&goflags.BuildPkgdir, "pkgdir", "", "") cmdSet.BoolVar(&goflags.BuildRace, "race", false, "") cmdSet.BoolVar(&goflags.BuildMSan, "msan", false, "") cmdSet.StringVar(&goflags.BuildTags, "tags", "", "") cmdSet.StringVar(&goflags.BuildToolexec, "toolexec", "", "") cmdSet.BoolVar(&goflags.BuildTrimpath, "trimpath", false, "") cmdSet.BoolVar(&goflags.BuildWork, "work", false, "") } func addOutputFlags(cmdSet *flag.FlagSet) { cmdSet.StringVar(&goflags.BuildO, "o", "", "") } func printGoHelp(usage string) { fmt.Println(usage) } func printGocHelp(cmd *cobra.Command) { flags := cmd.LocalFlags() globalFlags := cmd.Parent().PersistentFlags() fmt.Println("Flags:") fmt.Println(flags.FlagUsages()) fmt.Println("Global Flags:") fmt.Println(globalFlags.FlagUsages()) } // GetPackagesDir parse [pacakges] part of args, it will fatal if error encountered // // 函数获取 1: [packages] 所在的目录位置,供后续插桩使用。 // // 函数获取 2: 如果参数是 *.go,第一个 .go 文件的文件名。go build 中,二进制名字既可能是目录名也可能是文件名,和参数类型有关。 // // 如果 [packages] 非法(即不符合 go 原生的定义),则返回对应错误 // 这里只考虑 go mod 的方式 func (b *Build) getPackagesDir() { patterns := b.GoArgs packages := make([]string, 0) 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) } // 获取相对于 current working directory 对路径 for _, p := range patterns { if filepath.IsAbs(p) { relPath, err := filepath.Rel(b.CurWd, p) if err != nil { log.Fatalf("fail to get [packages] relative path from current working directory: %v", err) } packages = append(packages, relPath) } else { packages = append(packages, p) } } // fix: go build ./xx/main.go 需要转换为 // go build ./xx/main.go ./xx/goc-cover-agent-apis-auto-generated-11111-22222-bridge.go dir := filepath.Dir(packages[0]) packages = append(packages, filepath.Join(dir, "goc-cover-agent-apis-auto-generated-11111-22222-bridge.go")) b.Packages = packages return } } } // 2. 要么是 import path b.Packages = patterns } // 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 abs 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]) dir, _ = filepath.Abs(dir) 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 "" }